mirror of
https://github.com/codex-team/editor.js
synced 2024-05-07 17:06:48 +02:00
2d89105670
* 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>
371 lines
9.6 KiB
TypeScript
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;
|
|
}
|
|
}
|