editor.js/src/components/modules/tools.ts
George Berezhnoy 2d89105670
[Feature] Block Tunes API (#1596)
* Add internal wrappers for tools classes

* FIx lint

* Change tools collections to map

* Apply some more refactoring

* Make tool instance private field

* Add some docs

* Fix eslint

* Basic implementation for Block Tunes

* Small fix for demo

* Review changes

* Fix

* Add common tunes and ToolsCollection class

* Fixes after review

* Rename tools collections

* Readonly fix

* Some fixes after review

* Apply suggestions from code review

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Fixes after review

* Add docs and changelog

* Update docs/block-tunes.md

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Apply suggestions from code review

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Update src/components/block/index.ts

Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>

* [Dev] Tools utils tests (#1602)

* Add tests for tools utils and coverage report

* Fix eslint

* Adjust test

* Add more tests

* Update after code review

* Fix test & bump version

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>
2021-04-04 15:10:26 +03:00

371 lines
9.6 KiB
TypeScript

import Paragraph from '../../tools/paragraph/dist/bundle';
import Module from '../__module';
import * as _ from '../utils';
import {
EditorConfig,
Tool,
ToolConstructable,
ToolSettings
} from '../../../types';
import BoldInlineTool from '../inline-tools/inline-tool-bold';
import ItalicInlineTool from '../inline-tools/inline-tool-italic';
import LinkInlineTool from '../inline-tools/inline-tool-link';
import Stub from '../../tools/stub';
import ToolsFactory from '../tools/factory';
import InlineTool from '../tools/inline';
import BlockTool from '../tools/block';
import BlockTune from '../tools/tune';
import MoveDownTune from '../block-tunes/block-tune-move-down';
import DeleteTune from '../block-tunes/block-tune-delete';
import MoveUpTune from '../block-tunes/block-tune-move-up';
import ToolsCollection from '../tools/collection';
/**
* @module Editor.js Tools Submodule
*
* Creates Instances from Plugins and binds external config to the instances
*/
type ToolClass = BlockTool | InlineTool | BlockTune;
/**
* Class properties:
*
* @typedef {Tools} Tools
* @property {Tools[]} toolsAvailable - available Tools
* @property {Tools[]} toolsUnavailable - unavailable Tools
* @property {object} toolsClasses - all classes
* @property {object} toolsSettings - Tools settings
* @property {EditorConfig} config - Editor config
*/
export default class Tools extends Module {
/**
* Name of Stub Tool
* Stub Tool is used to substitute unavailable block Tools and store their data
*
* @type {string}
*/
public stubTool = 'stub';
/**
* Returns available Tools
*
* @returns {object<Tool>}
*/
public get available(): ToolsCollection {
return this.toolsAvailable;
}
/**
* Returns unavailable Tools
*
* @returns {Tool[]}
*/
public get unavailable(): ToolsCollection {
return this.toolsUnavailable;
}
/**
* Return Tools for the Inline Toolbar
*
* @returns {object} - object of Inline Tool's classes
*/
public get inlineTools(): ToolsCollection<InlineTool> {
return this.available.inlineTools;
}
/**
* Return editor block tools
*/
public get blockTools(): ToolsCollection<BlockTool> {
return this.available.blockTools;
}
/**
* Return available Block Tunes
*
* @returns {object} - object of Inline Tool's classes
*/
public get blockTunes(): ToolsCollection<BlockTune> {
return this.available.blockTunes;
}
/**
* Returns default Tool object
*/
public get defaultTool(): BlockTool {
return this.blockTools.get(this.config.defaultBlock);
}
/**
* Tools objects factory
*/
private factory: ToolsFactory;
/**
* Tools` classes available to use
*/
private readonly toolsAvailable: ToolsCollection = new ToolsCollection();
/**
* Tools` classes not available to use because of preparation failure
*/
private readonly toolsUnavailable: ToolsCollection = new ToolsCollection();
/**
* Returns internal tools
*/
public get internal(): ToolsCollection {
return this.available.internalTools;
}
/**
* Creates instances via passed or default configuration
*
* @returns {Promise<void>}
*/
public prepare(): Promise<void> {
this.validateTools();
/**
* Assign internal tools
*/
this.config.tools = _.deepMerge({}, this.internalTools, this.config.tools);
if (!Object.prototype.hasOwnProperty.call(this.config, 'tools') || Object.keys(this.config.tools).length === 0) {
throw Error('Can\'t start without tools');
}
const config = this.prepareConfig();
this.factory = new ToolsFactory(config, this.config, this.Editor.API);
/**
* getting classes that has prepare method
*/
const sequenceData = this.getListOfPrepareFunctions(config);
/**
* if sequence data contains nothing then resolve current chain and run other module prepare
*/
if (sequenceData.length === 0) {
return Promise.resolve();
}
/**
* to see how it works {@link '../utils.ts#sequence'}
*/
return _.sequence(sequenceData, (data: { toolName: string }) => {
this.toolPrepareMethodSuccess(data);
}, (data: { toolName: string }) => {
this.toolPrepareMethodFallback(data);
});
}
/**
* Returns Block Tunes for passed Tool
*
* @param tool - Tool object
*/
public getTunesForTool(tool: BlockTool): ToolsCollection<BlockTune> {
const names = tool.enabledBlockTunes;
if (names === false) {
return new ToolsCollection<BlockTune>();
}
if (Array.isArray(names)) {
return new ToolsCollection<BlockTune>(
Array
.from(this.blockTunes.entries())
.filter(([, tune]) => names.includes(tune.name))
.concat([ ...this.blockTunes.internalTools.entries() ])
);
}
const defaultTuneNames = this.config.tunes;
if (Array.isArray(defaultTuneNames)) {
return new ToolsCollection<BlockTune>(
Array
.from(this.blockTunes.entries())
.filter(([, tune]) => defaultTuneNames.includes(tune.name))
.concat([ ...this.blockTunes.internalTools.entries() ])
);
}
return this.blockTunes.internalTools;
}
/**
* Calls each Tool reset method to clean up anything set by Tool
*/
public destroy(): void {
Object.values(this.available).forEach(async tool => {
if (_.isFunction(tool.reset)) {
await tool.reset();
}
});
}
/**
* Returns internal tools
* Includes Bold, Italic, Link and Paragraph
*/
private get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings & { isInternal?: boolean } } {
return {
bold: {
class: BoldInlineTool,
isInternal: true,
},
italic: {
class: ItalicInlineTool,
isInternal: true,
},
link: {
class: LinkInlineTool,
isInternal: true,
},
paragraph: {
class: Paragraph,
inlineToolbar: true,
isInternal: true,
},
stub: {
class: Stub,
isInternal: true,
},
moveUpTune: {
class: MoveUpTune,
isInternal: true,
},
deleteTune: {
class: DeleteTune,
isInternal: true,
},
moveDownTune: {
class: MoveDownTune,
isInternal: true,
},
};
}
/**
* Tool prepare method success callback
*
* @param {object} data - append tool to available list
*/
private toolPrepareMethodSuccess(data: { toolName: string }): void {
const tool = this.factory.get(data.toolName);
if (tool.isInline()) {
/**
* Some Tools validation
*/
const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];
const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !tool.create()[method]);
if (notImplementedMethods.length) {
_.log(
`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,
'warn',
notImplementedMethods
);
this.toolsUnavailable.set(tool.name, tool);
return;
}
}
this.toolsAvailable.set(tool.name, tool);
}
/**
* Tool prepare method fail callback
*
* @param {object} data - append tool to unavailable list
*/
private toolPrepareMethodFallback(data: { toolName: string }): void {
this.toolsUnavailable.set(data.toolName, this.factory.get(data.toolName));
}
/**
* Binds prepare function of plugins with user or default config
*
* @returns {Array} list of functions that needs to be fired sequentially
* @param config - tools config
*/
private getListOfPrepareFunctions(config: {[name: string]: ToolSettings}): {
function: (data: { toolName: string }) => void | Promise<void>;
data: { toolName: string };
}[] {
const toolPreparationList: {
function: (data: { toolName: string }) => void | Promise<void>;
data: { toolName: string };
}[] = [];
Object
.entries(config)
.forEach(([toolName, settings]) => {
toolPreparationList.push({
// eslint-disable-next-line @typescript-eslint/no-empty-function
function: _.isFunction(settings.class.prepare) ? settings.class.prepare : (): void => {},
data: {
toolName,
},
});
});
return toolPreparationList;
}
/**
* Validate Tools configuration objects and throw Error for user if it is invalid
*/
private validateTools(): void {
/**
* Check Tools for a class containing
*/
for (const toolName in this.config.tools) {
if (Object.prototype.hasOwnProperty.call(this.config.tools, toolName)) {
if (toolName in this.internalTools) {
return;
}
const tool = this.config.tools[toolName];
if (!_.isFunction(tool) && !_.isFunction((tool as ToolSettings).class)) {
throw Error(
`Tool «${toolName}» must be a constructor function or an object with function in the «class» property`
);
}
}
}
}
/**
* Unify tools config
*/
private prepareConfig(): {[name: string]: ToolSettings} {
const config: {[name: string]: ToolSettings} = {};
/**
* Save Tools settings to a map
*/
for (const toolName in this.config.tools) {
/**
* If Tool is an object not a Tool's class then
* save class and settings separately
*/
if (_.isObject(this.config.tools[toolName])) {
config[toolName] = this.config.tools[toolName] as ToolSettings;
} else {
config[toolName] = { class: this.config.tools[toolName] as ToolConstructable };
}
}
return config;
}
}