editor.js/src/components/block.js
2018-07-20 23:03:37 +03:00

288 lines
6.9 KiB
JavaScript

/**
* @class Block
* @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool
*
* @property {Tool} tool — current block tool (Paragraph, for example)
* @property {Object} CSS — block`s css classes
*
*/
/** Import default tunes */
import MoveUpTune from './block-tunes/block-tune-move-up';
import DeleteTune from './block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down';
/**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
*
* @property tool - Tool instance
* @property html - Returns HTML content of plugin
* @property holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class
* @property pluginsContent - HTML content that returns by Tool's render function
*/
export default class Block {
/**
* @constructor
* @param {String} toolName - Tool name that passed on initialization
* @param {Object} toolInstance — passed Tool`s instance that rendered the Block
* @param {Object} settings - default settings
* @param {Object} apiMethods - Editor API
*/
constructor(toolName, toolInstance, settings, apiMethods) {
this.name = toolName;
this.tool = toolInstance;
this.settings = settings;
this.api = apiMethods;
this.holder = this.compose();
this.inputIndex = 0;
/**
* @type {IBlockTune[]}
*/
this.tunes = this.makeTunes();
}
/**
* CSS classes for the Block
* @return {{wrapper: string, content: string}}
*/
static get CSS() {
return {
wrapper: 'ce-block',
content: 'ce-block__content',
selected: 'ce-block--selected'
};
}
/**
* Make default Block wrappers and put Tool`s content there
* @returns {HTMLDivElement}
*/
compose() {
let wrapper = $.make('div', Block.CSS.wrapper),
contentNode = $.make('div', Block.CSS.content),
pluginsContent = this.tool.render();
contentNode.appendChild(pluginsContent);
wrapper.appendChild(contentNode);
return wrapper;
}
/**
* Calls Tool's method
*
* Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function
*
* @param {String} methodName
* @param {Object} params
*/
call(methodName, params) {
/**
* call Tool's method with the instance context
*/
if (this.tool[methodName] && this.tool[methodName] instanceof Function) {
this.tool[methodName].call(this.tool, params);
}
}
/**
* Returns Plugins content
* @return {Node}
*/
get pluginsContent() {
let pluginsContent = this.holder.querySelector(`.${Block.CSS.content}`);
if (pluginsContent && pluginsContent.childNodes.length) {
return pluginsContent.childNodes[0];
}
return null;
}
/**
* Get Block's JSON data
* @return {Object}
*/
get data() {
return this.save();
}
get inputs() {
const collection = this.holder.querySelectorAll('[contenteditable], input, textarea');
return _.array(collection);
}
get nextInput() {
const inputs = this.inputs;
this.inputIndex = Math.min(inputs.length - 1, this.inputIndex + 1);
return inputs[this.inputIndex];
}
get previousInput() {
this.inputIndex = Math.max(0, this.inputIndex - 1);
return this.inputs[this.inputIndex];
}
/**
* is block mergeable
* We plugin have merge function then we call it mergable
* @return {boolean}
*/
get mergeable() {
return typeof this.tool.merge === 'function';
}
/**
* Call plugins merge method
* @param {Object} data
*/
mergeWith(data) {
return Promise.resolve()
.then(() => {
this.tool.merge(data);
});
}
/**
* Extracts data from Block
* Groups Tool's save processing time
* @return {Object}
*/
save() {
let extractedBlock = this.tool.save(this.pluginsContent);
/** Measuring execution time*/
let measuringStart = window.performance.now(),
measuringEnd;
return Promise.resolve(extractedBlock)
.then((finishedExtraction) => {
/** measure promise execution */
measuringEnd = window.performance.now();
return {
tool: this.name,
data: finishedExtraction,
time : measuringEnd - measuringStart
};
})
.catch(function (error) {
_.log(`Saving proccess for ${this.tool.name} tool failed due to the ${error}`, 'log', 'red');
});
}
/**
* Uses Tool's validation method to check the correctness of output data
* Tool's validation method is optional
*
* @description Method also can return data if it passed the validation
*
* @param {Object} data
* @returns {Boolean|Object} valid
*/
validateData(data) {
let isValid = true;
if (this.tool.validate instanceof Function) {
isValid = this.tool.validate(data);
}
if (!isValid) {
return false;
}
return data;
}
/**
* Make an array with default settings
* Each block has default tune instance that have states
* @return {IBlockTune[]}
*/
makeTunes() {
let tunesList = [MoveUpTune, DeleteTune, MoveDownTune];
// Pluck tunes list and return tune instances with passed Editor API and settings
return tunesList.map( (tune) => {
return new tune({
api: this.api,
settings: this.settings,
});
});
}
/**
* Enumerates initialized tunes and returns fragment that can be appended to the toolbars area
* @return {DocumentFragment}
*/
renderTunes() {
let tunesElement = document.createDocumentFragment();
this.tunes.forEach( tune => {
$.append(tunesElement, tune.render());
});
return tunesElement;
}
/**
* Check block for emptiness
* @return {Boolean}
*/
get isEmpty() {
/**
* Allow Tool to represent decorative contentless blocks: for example "* * *"-tool
* That Tools are not empty
*/
if (this.tool.contentless) {
return false;
}
let emptyText = $.isEmpty(this.pluginsContent),
emptyMedia = !this.hasMedia;
return emptyText && emptyMedia;
}
/**
* Check if block has a media content such as images, iframes and other
* @return {Boolean}
*/
get hasMedia() {
/**
* This tags represents media-content
* @type {string[]}
*/
const mediaTags = [
'img',
'iframe',
'video',
'audio',
'source',
'input',
'textarea',
'twitterwidget'
];
return !!this.holder.querySelector(mediaTags.join(','));
}
/**
* Set selected state
* @param {Boolean} state - 'true' to select, 'false' to remove selection
*/
set selected(state) {
/**
* We don't need to mark Block as Selected when it is not empty
*/
if (state === true && !this.isEmpty) {
this.holder.classList.add(Block.CSS.selected);
} else {
this.holder.classList.remove(Block.CSS.selected);
}
}
}