From 4cfcb656a81dc9ae6daf0841ac094f68c4368e15 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Wed, 31 Mar 2021 23:29:41 +0300 Subject: [PATCH] [Refactoring] Tools (#1595) * 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 * Review changes * Fix * Fixes after review * Readonly fix --- .gitmodules | 4 +- src/components/block/index.ts | 87 ++-- src/components/modules/blockEvents.ts | 13 +- src/components/modules/blockManager.ts | 15 +- src/components/modules/caret.ts | 4 +- src/components/modules/paste.ts | 121 +++-- src/components/modules/readonly.ts | 12 +- src/components/modules/renderer.ts | 12 +- src/components/modules/sanitizer.ts | 34 +- .../modules/toolbar/blockSettings.ts | 6 +- src/components/modules/toolbar/conversion.ts | 58 +-- src/components/modules/toolbar/inline.ts | 150 +++--- src/components/modules/toolbar/toolbox.ts | 103 ++--- src/components/modules/tools.ts | 428 ++++++------------ src/components/modules/ui.ts | 2 +- src/components/tools/base.ts | 235 ++++++++++ src/components/tools/block.ts | 92 ++++ src/components/tools/factory.ts | 84 ++++ src/components/tools/inline.ts | 31 ++ src/components/tools/tune.ts | 21 + src/{components => }/tools/paragraph | 0 src/{components => }/tools/stub/index.ts | 4 +- types/tools/inline-tool.d.ts | 2 +- 23 files changed, 875 insertions(+), 643 deletions(-) create mode 100644 src/components/tools/base.ts create mode 100644 src/components/tools/block.ts create mode 100644 src/components/tools/factory.ts create mode 100644 src/components/tools/inline.ts create mode 100644 src/components/tools/tune.ts rename src/{components => }/tools/paragraph (100%) rename src/{components => }/tools/stub/index.ts (94%) diff --git a/.gitmodules b/.gitmodules index 2041e602..7dc8f02c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,8 +16,8 @@ [submodule "example/tools/simple-image"] path = example/tools/simple-image url = https://github.com/editor-js/simple-image -[submodule "src/components/tools/paragraph"] - path = src/components/tools/paragraph +[submodule "src/tools/paragraph"] + path = src/tools/paragraph url = https://github.com/editor-js/paragraph [submodule "example/tools/marker"] path = example/tools/marker diff --git a/src/components/block/index.ts b/src/components/block/index.ts index fef14126..9bdaaf08 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -1,6 +1,6 @@ import { BlockAPI as BlockAPIInterface, - BlockTool, + BlockTool as IBlockTool, BlockToolConstructable, BlockToolData, BlockTune, @@ -16,12 +16,13 @@ import * as _ from '../utils'; import ApiModules from '../modules/api'; import BlockAPI from './api'; import { ToolType } from '../modules/tools'; +import SelectionUtils from '../selection'; +import BlockTool from '../tools/block'; /** 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'; -import SelectionUtils from '../selection'; /** * Interface describes Block class constructor argument @@ -38,14 +39,9 @@ interface BlockConstructorOptions { data: BlockToolData; /** - * Tool's class or constructor function + * Tool object */ - Tool: BlockToolConstructable; - - /** - * Tool settings from initial config - */ - settings: ToolSettings; + tool: BlockTool; /** * Editor's API methods @@ -110,32 +106,27 @@ export default class Block { /** * Block Tool`s name */ - public name: string; + public readonly name: string; /** * Instance of the Tool Block represents */ - public tool: BlockTool; - - /** - * Class blueprint of the ool Block represents - */ - public class: BlockToolConstructable; + public readonly tool: BlockTool; /** * User Tool configuration */ - public settings: ToolConfig; + public readonly settings: ToolConfig; /** * Wrapper for Block`s content */ - public holder: HTMLDivElement; + public readonly holder: HTMLDivElement; /** * Tunes used by Tool */ - public tunes: BlockTune[]; + public readonly tunes: BlockTune[]; /** * Tool's user configuration @@ -149,6 +140,11 @@ export default class Block { */ private cachedInputs: HTMLElement[] = []; + /** + * Tool class instance + */ + private readonly toolInstance: IBlockTool; + /** * Editor`s API module */ @@ -209,27 +205,20 @@ export default class Block { constructor({ name, data, - Tool, - settings, + tool, api, readOnly, }: BlockConstructorOptions) { this.name = name; - this.class = Tool; - this.settings = settings; - this.config = settings.config || {}; + this.settings = tool.settings; + this.config = tool.settings.config || {}; this.api = api; this.blockAPI = new BlockAPI(this); this.mutationObserver = new MutationObserver(this.didMutated); - this.tool = new Tool({ - data, - config: this.config, - api: this.api.getMethodsForTool(name, ToolType.Block), - block: this.blockAPI, - readOnly, - }); + this.tool = tool; + this.toolInstance = tool.instance(data, this.blockAPI, readOnly); this.holder = this.compose(); /** @@ -349,7 +338,7 @@ export default class Block { * @returns {object} */ public get sanitize(): SanitizerConfig { - return this.tool.sanitize; + return this.tool.sanitizeConfig; } /** @@ -359,7 +348,7 @@ export default class Block { * @returns {boolean} */ public get mergeable(): boolean { - return _.isFunction(this.tool.merge); + return _.isFunction(this.toolInstance.merge); } /** @@ -502,7 +491,7 @@ export default class Block { /** * call Tool's method with the instance context */ - if (this.tool[methodName] && this.tool[methodName] instanceof Function) { + if (this.toolInstance[methodName] && this.toolInstance[methodName] instanceof Function) { if (methodName === BlockToolAPI.APPEND_CALLBACK) { _.log( '`appendCallback` hook is deprecated and will be removed in the next major release. ' + @@ -513,7 +502,7 @@ export default class Block { try { // eslint-disable-next-line no-useless-call - this.tool[methodName].call(this.tool, params); + this.toolInstance[methodName].call(this.toolInstance, params); } catch (e) { _.log(`Error during '${methodName}' call: ${e.message}`, 'error'); } @@ -526,7 +515,7 @@ export default class Block { * @param {BlockToolData} data - data to merge */ public async mergeWith(data: BlockToolData): Promise { - await this.tool.merge(data); + await this.toolInstance.merge(data); } /** @@ -536,7 +525,7 @@ export default class Block { * @returns {object} */ public async save(): Promise { - const extractedBlock = await this.tool.save(this.pluginsContent as HTMLElement); + const extractedBlock = await this.toolInstance.save(this.pluginsContent as HTMLElement); /** * Measuring execution time @@ -572,8 +561,8 @@ export default class Block { public async validate(data: BlockToolData): Promise { let isValid = true; - if (this.tool.validate instanceof Function) { - isValid = await this.tool.validate(data); + if (this.toolInstance.validate instanceof Function) { + isValid = await this.toolInstance.validate(data); } return isValid; @@ -672,6 +661,24 @@ export default class Block { this.removeInputEvents(); } + /** + * Call Tool instance destroy method + */ + public destroy(): void { + if (_.isFunction(this.toolInstance.destroy)) { + this.toolInstance.destroy(); + } + } + + /** + * Call Tool instance renderSettings method + */ + public renderSettings(): HTMLElement | undefined { + if (_.isFunction(this.toolInstance.renderSettings)) { + return this.toolInstance.renderSettings(); + } + } + /** * Make default Block wrappers and put Tool`s content there * @@ -680,7 +687,7 @@ export default class Block { private compose(): HTMLDivElement { const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement, contentNode = $.make('div', Block.CSS.content), - pluginsContent = this.tool.render(); + pluginsContent = this.toolInstance.render(); contentNode.appendChild(pluginsContent); wrapper.appendChild(contentNode); diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index a2b8d661..4fb43db7 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -125,7 +125,7 @@ export default class BlockEvents extends Module { return; } - const canOpenToolbox = Tools.isDefault(currentBlock.tool) && currentBlock.isEmpty; + const canOpenToolbox = currentBlock.tool.isDefault && currentBlock.isEmpty; const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened; const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened; @@ -206,15 +206,14 @@ export default class BlockEvents extends Module { * @param {KeyboardEvent} event - keydown */ private enter(event: KeyboardEvent): void { - const { BlockManager, Tools, UI } = this.Editor; + const { BlockManager, UI } = this.Editor; const currentBlock = BlockManager.currentBlock; - const tool = Tools.available[currentBlock.name]; /** * Don't handle Enter keydowns when Tool sets enableLineBreaks to true. * Uses for Tools like where line breaks should be handled by default behaviour. */ - if (tool && tool[Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS]) { + if (currentBlock.tool.isLineBreaksEnabled) { return; } @@ -253,7 +252,7 @@ export default class BlockEvents extends Module { /** * If new Block is empty */ - if (this.Editor.Tools.isDefault(newCurrent.tool) && newCurrent.isEmpty) { + if (newCurrent.tool.isDefault && newCurrent.isEmpty) { /** * Show Toolbar */ @@ -276,7 +275,7 @@ export default class BlockEvents extends Module { private backspace(event: KeyboardEvent): void { const { BlockManager, BlockSelection, Caret } = this.Editor; const currentBlock = BlockManager.currentBlock; - const tool = this.Editor.Tools.available[currentBlock.name]; + const tool = currentBlock.tool; /** * Check if Block should be removed by current Backspace keydown @@ -314,7 +313,7 @@ export default class BlockEvents extends Module { * * But if caret is at start of the block, we allow to remove it by backspaces */ - if (tool && tool[this.Editor.Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS] && !Caret.isAtStart) { + if (tool.isLineBreaksEnabled && !Caret.isAtStart) { return; } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 77c1447f..9027c0ea 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -12,6 +12,7 @@ import $ from '../dom'; import * as _ from '../utils'; import Blocks from '../blocks'; import { BlockToolConstructable, BlockToolData, PasteEvent } from '../../../types'; +import BlockTool from '../tools/block'; /** * @typedef {BlockManager} BlockManager @@ -219,15 +220,13 @@ export default class BlockManager extends Module { * * @returns {Block} */ - public composeBlock({ tool, data = {} }: {tool: string; data?: BlockToolData}): Block { + public composeBlock({ tool: name, data = {} }: {tool: string; data?: BlockToolData}): Block { const readOnly = this.Editor.ReadOnly.isEnabled; - const settings = this.Editor.Tools.getToolSettings(tool); - const Tool = this.Editor.Tools.available[tool] as BlockToolConstructable; + const tool = this.Editor.Tools.blockTools.get(name); const block = new Block({ - name: tool, + name, data, - Tool, - settings, + tool, api: this.Editor.API, readOnly, }); @@ -703,9 +702,7 @@ export default class BlockManager extends Module { */ public async destroy(): Promise { await Promise.all(this.blocks.map((block) => { - if (_.isFunction(block.tool.destroy)) { - return block.tool.destroy(); - } + return block.destroy(); })); } diff --git a/src/components/modules/caret.ts b/src/components/modules/caret.ts index 2f38dc7c..f02bd9fe 100644 --- a/src/components/modules/caret.ts +++ b/src/components/modules/caret.ts @@ -333,7 +333,7 @@ export default class Caret extends Module { * If last block is empty and it is an defaultBlock, set to that. * Otherwise, append new empty block and set to that */ - if (this.Editor.Tools.isDefault(lastBlock.tool) && lastBlock.isEmpty) { + if (lastBlock.tool.isDefault && lastBlock.isEmpty) { this.setToBlock(lastBlock); } else { const newBlock = this.Editor.BlockManager.insertAtEnd(); @@ -409,7 +409,7 @@ export default class Caret extends Module { * 2. If there is a last block and it is non-default --> and caret not at the end <--, do nothing * (https://github.com/codex-team/editor.js/issues/1414) */ - if (Tools.isDefault(currentBlock.tool) || !isAtEnd) { + if (currentBlock.tool.isDefault || !isAtEnd) { return false; } diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index 884cbdba..1b5e3562 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -2,14 +2,13 @@ import Module from '../__module'; import $ from '../dom'; import * as _ from '../utils'; import { - BlockTool, - BlockToolConstructable, - PasteConfig, + BlockAPI, PasteEvent, PasteEventDetail } from '../../../types'; import Block from '../block'; import { SavedData } from '../../../types/data-formats'; +import BlockTool from '../tools/block'; /** * Tag substitute object. @@ -18,9 +17,8 @@ interface TagSubstitute { /** * Name of related Tool * - * @type {string} */ - tool: string; + tool: BlockTool; } /** @@ -29,24 +27,18 @@ interface TagSubstitute { interface PatternSubstitute { /** * Pattern`s key - * - * @type {string} */ key: string; /** * Pattern regexp - * - * @type {RegExp} */ pattern: RegExp; /** * Name of related Tool - * - * @type {string} */ - tool: string; + tool: BlockTool; } /** @@ -247,7 +239,7 @@ export default class Paste extends Module { return; } - const isCurrentBlockDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool); + const isCurrentBlockDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault; const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty; dataToInsert.map( @@ -279,23 +271,22 @@ export default class Paste extends Module { private processTools(): void { const tools = this.Editor.Tools.blockTools; - Object.entries(tools).forEach(this.processTool); + Array + .from(tools.values()) + .forEach(this.processTool); } /** * Process paste config for each tool + * + * @param tool - BlockTool object */ - private processTool = ([name, tool]: [string, BlockToolConstructable]): void => { + private processTool = (tool: BlockTool): void => { try { - const toolInstance = new this.Editor.Tools.blockTools[name]({ - api: this.Editor.API.getMethodsForTool(name), - config: {}, - data: {}, - readOnly: false, - }) as BlockTool; + const toolInstance = tool.instance({}, {} as BlockAPI, false); if (tool.pasteConfig === false) { - this.exceptionList.push(name); + this.exceptionList.push(tool.name); return; } @@ -304,11 +295,9 @@ export default class Paste extends Module { return; } - const toolPasteConfig = tool.pasteConfig || {}; - - this.getTagsConfig(name, toolPasteConfig); - this.getFilesConfig(name, toolPasteConfig); - this.getPatternsConfig(name, toolPasteConfig); + this.getTagsConfig(tool); + this.getFilesConfig(tool); + this.getPatternsConfig(tool); } catch (e) { _.log( `Paste handling for «${name}» Tool hasn't been set up because of the error`, @@ -321,17 +310,16 @@ export default class Paste extends Module { /** * Get tags to substitute by Tool * - * @param {string} name - Tool name - * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration + * @param tool - BlockTool object */ - private getTagsConfig(name: string, toolPasteConfig: PasteConfig): void { - const tags = toolPasteConfig.tags || []; + private getTagsConfig(tool: BlockTool): void { + const tags = tool.pasteConfig.tags || []; tags.forEach((tag) => { if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) { _.log( - `Paste handler for «${name}» Tool on «${tag}» tag is skipped ` + - `because it is already used by «${this.toolsTags[tag].tool}» Tool.`, + `Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` + + `because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`, 'warn' ); @@ -339,21 +327,20 @@ export default class Paste extends Module { } this.toolsTags[tag.toUpperCase()] = { - tool: name, + tool, }; }); - this.tagsByTool[name] = tags.map((t) => t.toUpperCase()); + this.tagsByTool[tool.name] = tags.map((t) => t.toUpperCase()); } /** * Get files` types and extensions to substitute by Tool * - * @param {string} name - Tool name - * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration + * @param tool - BlockTool object */ - private getFilesConfig(name: string, toolPasteConfig: PasteConfig): void { - const { files = {} } = toolPasteConfig; + private getFilesConfig(tool: BlockTool): void { + const { files = {} } = tool.pasteConfig; let { extensions, mimeTypes } = files; if (!extensions && !mimeTypes) { @@ -361,19 +348,19 @@ export default class Paste extends Module { } if (extensions && !Array.isArray(extensions)) { - _.log(`«extensions» property of the onDrop config for «${name}» Tool should be an array`); + _.log(`«extensions» property of the onDrop config for «${tool.name}» Tool should be an array`); extensions = []; } if (mimeTypes && !Array.isArray(mimeTypes)) { - _.log(`«mimeTypes» property of the onDrop config for «${name}» Tool should be an array`); + _.log(`«mimeTypes» property of the onDrop config for «${tool.name}» Tool should be an array`); mimeTypes = []; } if (mimeTypes) { mimeTypes = mimeTypes.filter((type) => { if (!_.isValidMimeType(type)) { - _.log(`MIME type value «${type}» for the «${name}» Tool is not a valid MIME type`, 'warn'); + _.log(`MIME type value «${type}» for the «${tool.name}» Tool is not a valid MIME type`, 'warn'); return false; } @@ -382,7 +369,7 @@ export default class Paste extends Module { }); } - this.toolsFiles[name] = { + this.toolsFiles[tool.name] = { extensions: extensions || [], mimeTypes: mimeTypes || [], }; @@ -391,15 +378,14 @@ export default class Paste extends Module { /** * Get RegExp patterns to substitute by Tool * - * @param {string} name - Tool name - * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration + * @param tool - BlockTool object */ - private getPatternsConfig(name: string, toolPasteConfig: PasteConfig): void { - if (!toolPasteConfig.patterns || _.isEmpty(toolPasteConfig.patterns)) { + private getPatternsConfig(tool: BlockTool): void { + if (!tool.pasteConfig.patterns || _.isEmpty(tool.pasteConfig.patterns)) { return; } - Object.entries(toolPasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => { + Object.entries(tool.pasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => { /** Still need to validate pattern as it provided by user */ if (!(pattern instanceof RegExp)) { _.log( @@ -411,7 +397,7 @@ export default class Paste extends Module { this.toolsPatterns.push({ key, pattern, - tool: name, + tool, }); }); } @@ -462,9 +448,9 @@ export default class Paste extends Module { * @param {FileList} items - pasted or dropped items */ private async processFiles(items: FileList): Promise { - const { BlockManager, Tools } = this.Editor; + const { BlockManager } = this.Editor; - let dataToInsert: Array<{type: string; event: PasteEvent}>; + let dataToInsert: {type: string; event: PasteEvent}[]; dataToInsert = await Promise.all( Array @@ -473,7 +459,7 @@ export default class Paste extends Module { ); dataToInsert = dataToInsert.filter((data) => !!data); - const isCurrentBlockDefault = Tools.isDefault(BlockManager.currentBlock.tool); + const isCurrentBlockDefault = BlockManager.currentBlock.tool.isDefault; const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty; dataToInsert.forEach( @@ -530,7 +516,6 @@ export default class Paste extends Module { */ private processHTML(innerHTML: string): PasteData[] { const { Tools, Sanitizer } = this.Editor; - const initialTool = this.config.defaultBlock; const wrapper = $.make('DIV'); wrapper.innerHTML = innerHTML; @@ -539,7 +524,7 @@ export default class Paste extends Module { return nodes .map((node) => { - let content, tool = initialTool, isBlock = false; + let content, tool = Tools.defaultTool, isBlock = false; switch (node.nodeType) { /** If node is a document fragment, use temp wrapper to get innerHTML */ @@ -559,7 +544,7 @@ export default class Paste extends Module { break; } - const { tags } = Tools.blockTools[tool].pasteConfig as PasteConfig; + const { tags } = tool.pasteConfig; const toolTags = tags.reduce((result, tag) => { result[tag.toLowerCase()] = {}; @@ -577,7 +562,7 @@ export default class Paste extends Module { return { content, isBlock, - tool, + tool: tool.name, event, }; }) @@ -627,7 +612,7 @@ export default class Paste extends Module { * @param {PasteData} dataToInsert - data of Block to inseret */ private async processSingleBlock(dataToInsert: PasteData): Promise { - const { Caret, BlockManager, Tools } = this.Editor; + const { Caret, BlockManager } = this.Editor; const { currentBlock } = BlockManager; /** @@ -638,7 +623,7 @@ export default class Paste extends Module { dataToInsert.tool !== currentBlock.name || !$.containsOnlyInlineElements(dataToInsert.content.innerHTML) ) { - this.insertBlock(dataToInsert, currentBlock && Tools.isDefault(currentBlock.tool) && currentBlock.isEmpty); + this.insertBlock(dataToInsert, currentBlock?.tool.isDefault && currentBlock.isEmpty); return; } @@ -655,17 +640,17 @@ export default class Paste extends Module { * @param {PasteData} dataToInsert - data of Block to insert */ private async processInlinePaste(dataToInsert: PasteData): Promise { - const { BlockManager, Caret, Sanitizer, Tools } = this.Editor; + const { BlockManager, Caret, Sanitizer } = this.Editor; const { content } = dataToInsert; - const currentBlockIsDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool); + const currentBlockIsDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault; if (currentBlockIsDefault && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) { const blockData = await this.processPattern(content.textContent); if (blockData) { const needToReplaceCurrentBlock = BlockManager.currentBlock && - Tools.isDefault(BlockManager.currentBlock.tool) && + BlockManager.currentBlock.tool.isDefault && BlockManager.currentBlock.isEmpty; const insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock); @@ -678,7 +663,7 @@ export default class Paste extends Module { /** If there is no pattern substitute - insert string as it is */ if (BlockManager.currentBlock && BlockManager.currentBlock.currentInput) { - const currentToolSanitizeConfig = Sanitizer.getInlineToolsConfig(BlockManager.currentBlock.name); + const currentToolSanitizeConfig = Sanitizer.getInlineToolsConfig(BlockManager.currentBlock.tool); document.execCommand( 'insertHTML', @@ -719,7 +704,7 @@ export default class Paste extends Module { return { event, - tool: pattern.tool, + tool: pattern.tool.name, }; } @@ -755,15 +740,15 @@ export default class Paste extends Module { * * @returns {void} */ - private insertEditorJSData(blocks: Array>): void { - const { BlockManager, Caret, Sanitizer, Tools } = this.Editor; + private insertEditorJSData(blocks: Pick[]): void { + const { BlockManager, Caret, Sanitizer } = this.Editor; const sanitizedBlocks = Sanitizer.sanitizeBlocks(blocks); sanitizedBlocks.forEach(({ tool, data }, i) => { let needToReplaceCurrentBlock = false; if (i === 0) { - const isCurrentBlockDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool); + const isCurrentBlockDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault; needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty; } @@ -792,8 +777,8 @@ export default class Paste extends Module { const element = node as HTMLElement; - const { tool = '' } = this.toolsTags[element.tagName] || {}; - const toolTags = this.tagsByTool[tool] || []; + const { tool } = this.toolsTags[element.tagName] || {}; + const toolTags = this.tagsByTool[tool?.name] || []; const isSubstitutable = tags.includes(element.tagName); const isBlockElement = $.blockElements.includes(element.tagName.toLowerCase()); diff --git a/src/components/modules/readonly.ts b/src/components/modules/readonly.ts index 544d1d7d..77a5fc8c 100644 --- a/src/components/modules/readonly.ts +++ b/src/components/modules/readonly.ts @@ -40,11 +40,13 @@ export default class ReadOnly extends Module { const { blockTools } = Tools; const toolsDontSupportReadOnly: string[] = []; - Object.entries(blockTools).forEach(([name, tool]) => { - if (!Tools.isReadOnlySupported(tool)) { - toolsDontSupportReadOnly.push(name); - } - }); + Array + .from(blockTools.entries()) + .forEach(([name, tool]) => { + if (!tool.isReadOnlySupported) { + toolsDontSupportReadOnly.push(name); + } + }); this.toolsDontSupportReadOnly = toolsDontSupportReadOnly; diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index d94efb8b..6b68b2ba 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -1,6 +1,7 @@ import Module from '../__module'; import * as _ from '../utils'; -import { BlockToolConstructable, OutputBlockData } from '../../../types'; +import { OutputBlockData } from '../../../types'; +import BlockTool from '../tools/block'; /** * Editor.js Renderer Module @@ -66,7 +67,7 @@ export default class Renderer extends Module { const tool = item.type; const data = item.data; - if (tool in Tools.available) { + if (Tools.available.has(tool)) { try { BlockManager.insert({ tool, @@ -86,11 +87,10 @@ export default class Renderer extends Module { title: tool, }; - if (tool in Tools.unavailable) { - const toolToolboxSettings = (Tools.unavailable[tool] as BlockToolConstructable).toolbox; - const userToolboxSettings = Tools.getToolSettings(tool).toolbox; + if (Tools.unavailable.has(tool)) { + const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox; - stubData.title = toolToolboxSettings.title || (userToolboxSettings && userToolboxSettings.title) || stubData.title; + stubData.title = toolboxSettings?.title || stubData.title; } const stub = BlockManager.insert({ diff --git a/src/components/modules/sanitizer.ts b/src/components/modules/sanitizer.ts index f3bc086f..0892fb98 100644 --- a/src/components/modules/sanitizer.ts +++ b/src/components/modules/sanitizer.ts @@ -36,8 +36,10 @@ import * as _ from '../utils'; */ import HTMLJanitor from 'html-janitor'; -import { BlockToolData, InlineToolConstructable, SanitizerConfig } from '../../../types'; +import { BlockToolData, SanitizerConfig } from '../../../types'; import { SavedData } from '../../../types/data-formats'; +import InlineTool from '../tools/inline'; +import BlockTool from '../tools/block'; /** * @@ -61,8 +63,8 @@ export default class Sanitizer extends Module { * @param {Array<{tool, data: BlockToolData}>} blocksData - blocks' data to sanitize */ public sanitizeBlocks( - blocksData: Array> - ): Array> { + blocksData: Pick[] + ): Pick[] { return blocksData.map((block) => { const toolConfig = this.composeToolConfig(block.tool); @@ -150,18 +152,17 @@ export default class Sanitizer extends Module { return this.configCache[toolName]; } - const sanitizeGetter = this.Editor.Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG; - const toolClass = this.Editor.Tools.available[toolName]; - const baseConfig = this.getInlineToolsConfig(toolName); + const tool = this.Editor.Tools.available.get(toolName); + const baseConfig = this.getInlineToolsConfig(tool as BlockTool); /** * If Tools doesn't provide sanitizer config or it is empty */ - if (!toolClass.sanitize || (toolClass[sanitizeGetter] && _.isEmpty(toolClass[sanitizeGetter]))) { + if (!tool.sanitizeConfig || (tool.sanitizeConfig && _.isEmpty(tool.sanitizeConfig))) { return baseConfig; } - const toolRules = toolClass.sanitize; + const toolRules = tool.sanitizeConfig; const toolConfig = {} as SanitizerConfig; @@ -186,12 +187,11 @@ export default class Sanitizer extends Module { * When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools, * otherwise get only enabled * - * @param {string} name - Inline Tool name + * @param tool - BlockTool object */ - public getInlineToolsConfig(name: string): SanitizerConfig { + public getInlineToolsConfig(tool: BlockTool): SanitizerConfig { const { Tools } = this.Editor; - const toolsConfig = Tools.getToolSettings(name); - const enableInlineTools = toolsConfig.inlineToolbar || []; + const enableInlineTools = tool.enabledInlineTools || []; let config = {} as SanitizerConfig; @@ -207,7 +207,7 @@ export default class Sanitizer extends Module { (enableInlineTools as string[]).map((inlineToolName) => { config = Object.assign( config, - Tools.inline[inlineToolName][Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG] + Tools.inlineTools.get(inlineToolName).sanitizeConfig ) as SanitizerConfig; }); } @@ -233,9 +233,9 @@ export default class Sanitizer extends Module { const config: SanitizerConfig = {} as SanitizerConfig; - Object.entries(Tools.inline) - .forEach(([, inlineTool]: [string, InlineToolConstructable]) => { - Object.assign(config, inlineTool[Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]); + Object.entries(Tools.inlineTools) + .forEach(([, inlineTool]: [string, InlineTool]) => { + Object.assign(config, inlineTool.sanitizeConfig); }); this.inlineToolsConfigCache = config; @@ -249,7 +249,7 @@ export default class Sanitizer extends Module { * @param {Array} array - [1, 2, {}, []] * @param {SanitizerConfig} ruleForItem - sanitizer config for array */ - private cleanArray(array: Array, ruleForItem: SanitizerConfig): Array { + private cleanArray(array: (object | string)[], ruleForItem: SanitizerConfig): (object | string)[] { return array.map((arrayItem) => this.deepSanitize(arrayItem, ruleForItem)); } diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index a3a61c54..8cdda140 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -229,8 +229,10 @@ export default class BlockSettings extends Module { * Add Tool's settings */ private addToolSettings(): void { - if (_.isFunction(this.Editor.BlockManager.currentBlock.tool.renderSettings)) { - $.append(this.nodes.toolSettings, this.Editor.BlockManager.currentBlock.tool.renderSettings()); + const settingsElement = this.Editor.BlockManager.currentBlock.renderSettings(); + + if (settingsElement) { + $.append(this.nodes.toolSettings, settingsElement); } } diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index a258bf2d..f49d86c1 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -182,10 +182,9 @@ export default class ConversionToolbar extends Module { * * @type {BlockToolConstructable} */ - const currentBlockClass = this.Editor.BlockManager.currentBlock.class; + const currentBlockTool = this.Editor.BlockManager.currentBlock.tool; const currentBlockName = this.Editor.BlockManager.currentBlock.name; const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData; - const { INTERNAL_SETTINGS } = this.Editor.Tools; const blockData = savedBlock.data; /** @@ -201,7 +200,7 @@ export default class ConversionToolbar extends Module { * * @type {BlockToolConstructable} */ - const replacingTool = this.Editor.Tools.toolsClasses[replacingToolName] as BlockToolConstructable; + const replacingTool = this.Editor.Tools.blockTools.get(replacingToolName); /** * Export property can be: @@ -211,7 +210,7 @@ export default class ConversionToolbar extends Module { * In both cases returning value must be a string */ let exportData = ''; - const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export; + const exportProp = currentBlockTool.conversionConfig.export; if (_.isFunction(exportProp)) { exportData = exportProp(blockData); @@ -229,7 +228,7 @@ export default class ConversionToolbar extends Module { */ const cleaned: string = this.Editor.Sanitizer.clean( exportData, - replacingTool.sanitize + replacingTool.sanitizeConfig ); /** @@ -238,7 +237,7 @@ export default class ConversionToolbar extends Module { * string — the name of data field to import */ let newBlockData = {}; - const importProp = replacingTool[INTERNAL_SETTINGS.CONVERSION_CONFIG].import; + const importProp = replacingTool.conversionConfig.import; if (_.isFunction(importProp)) { newBlockData = importProp(cleaned); @@ -272,37 +271,28 @@ export default class ConversionToolbar extends Module { private addTools(): void { const tools = this.Editor.Tools.blockTools; - for (const toolName in tools) { - if (!Object.prototype.hasOwnProperty.call(tools, toolName)) { - continue; - } + Array + .from(tools.entries()) + .forEach(([name, tool]) => { + const toolboxSettings = tool.toolbox; + const conversionConfig = tool.conversionConfig; - const internalSettings = this.Editor.Tools.INTERNAL_SETTINGS; - const toolClass = tools[toolName] as BlockToolConstructable; - const toolToolboxSettings = toolClass[internalSettings.TOOLBOX]; - const conversionConfig = toolClass[internalSettings.CONVERSION_CONFIG]; + /** + * Skip tools that don't pass 'toolbox' property + */ + if (_.isEmpty(toolboxSettings) || !toolboxSettings.icon) { + return; + } - const userSettings = this.Editor.Tools.USER_SETTINGS; - const userToolboxSettings = this.Editor.Tools.getToolSettings(toolName)[userSettings.TOOLBOX]; + /** + * Skip tools without «import» rule specified + */ + if (!conversionConfig || !conversionConfig.import) { + return; + } - const toolboxSettings = userToolboxSettings ?? toolToolboxSettings; - - /** - * Skip tools that don't pass 'toolbox' property - */ - if (_.isEmpty(toolboxSettings) || !toolboxSettings.icon) { - continue; - } - - /** - * Skip tools without «import» rule specified - */ - if (!conversionConfig || !conversionConfig.import) { - continue; - } - - this.addTool(toolName, toolboxSettings.icon, toolboxSettings.title); - } + this.addTool(name, toolboxSettings.icon, toolboxSettings.title); + }); } /** diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 79630309..7c8c77cc 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -2,12 +2,15 @@ import Module from '../../__module'; import $ from '../../dom'; import SelectionUtils from '../../selection'; import * as _ from '../../utils'; -import { InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings } from '../../../../types'; +import { InlineTool as IInlineTool } from '../../../../types'; import Flipper from '../../flipper'; import I18n from '../../i18n'; import { I18nInternalNS } from '../../i18n/namespace-internal'; import Shortcuts from '../../utils/shortcuts'; -import { EditorModules } from '../../../types-internal/editor-modules'; +import { ToolType } from '../tools'; +import InlineTool from '../../tools/inline'; +import { CommonInternalSettings } from '../../tools/base'; +import BlockTool from '../../tools/block'; /** * Inline Toolbar elements @@ -66,9 +69,11 @@ export default class InlineToolbar extends Module { private readonly toolbarVerticalMargin: number = 5; /** + * TODO: Get rid of this + * * Currently visible tools instances */ - private toolsInstances: Map; + private toolsInstances: Map; /** * Buttons List @@ -89,38 +94,6 @@ export default class InlineToolbar extends Module { */ private flipper: Flipper = null; - /** - * Internal inline tools: Link, Bold, Italic - */ - private internalTools: {[name: string]: InlineToolConstructable} = {}; - - /** - * Editor modules setter - * - * @param {EditorModules} Editor - Editor's Modules - */ - public set state(Editor: EditorModules) { - this.Editor = Editor; - - const { Tools } = Editor; - - /** - * Set internal inline tools - */ - Object - .entries(Tools.internalTools) - .filter(([, toolClass]: [string, ToolConstructable | ToolSettings]) => { - if (_.isFunction(toolClass)) { - return toolClass[Tools.INTERNAL_SETTINGS.IS_INLINE]; - } - - return (toolClass as ToolSettings).class[Tools.INTERNAL_SETTINGS.IS_INLINE]; - }) - .map(([name, toolClass]: [string, InlineToolConstructable | ToolSettings]) => { - this.internalTools[name] = _.isFunction(toolClass) ? toolClass : (toolClass as ToolSettings).class; - }); - } - /** * Toggles read-only mode * @@ -310,16 +283,14 @@ export default class InlineToolbar extends Module { /** * Returns inline toolbar settings for a particular tool * - * @param {string} toolName - user specified name of tool + * @param tool - BlockTool object * @returns {string[] | boolean} array of ordered tool names or false */ - private getInlineToolbarSettings(toolName): string[] | boolean { - const toolSettings = this.Editor.Tools.getToolSettings(toolName); - + private getInlineToolbarSettings(tool: BlockTool): string[] | boolean { /** * InlineToolbar property of a particular tool */ - const settingsForTool = toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS]; + const settingsForTool = tool.enabledInlineTools; /** * Whether to enable IT for a particular tool is the decision of the editor user. @@ -367,15 +338,7 @@ export default class InlineToolbar extends Module { * If common settings is 'true' or not specified (will be set as true at core.ts), get the default order */ if (commonInlineToolbarSettings === true) { - const defaultToolsOrder: string[] = Object.entries(this.Editor.Tools.available) - .filter(([name, tool]) => { - return tool[this.Editor.Tools.INTERNAL_SETTINGS.IS_INLINE]; - }) - .map(([name, tool]) => { - return name; - }); - - return defaultToolsOrder; + return Array.from(this.Editor.Tools.inlineTools.keys()); } return false; @@ -492,7 +455,7 @@ export default class InlineToolbar extends Module { /** * getInlineToolbarSettings could return an string[] (order of tools) or false (Inline Toolbar disabled). */ - const inlineToolbarSettings = this.getInlineToolbarSettings(currentBlock.name); + const inlineToolbarSettings = this.getInlineToolbarSettings(currentBlock.tool); return inlineToolbarSettings !== false; } @@ -548,13 +511,14 @@ export default class InlineToolbar extends Module { * Changes Conversion Dropdown content for current block's Tool */ private setConversionTogglerContent(): void { - const { BlockManager, Tools } = this.Editor; - const toolName = BlockManager.currentBlock.name; + const { BlockManager } = this.Editor; + const { currentBlock } = BlockManager; + const toolName = currentBlock.name; /** * If tool does not provide 'export' rule, hide conversion dropdown */ - const conversionConfig = Tools.available[toolName][Tools.INTERNAL_SETTINGS.CONVERSION_CONFIG] || {}; + const conversionConfig = currentBlock.tool.conversionConfig; const exportRuleDefined = conversionConfig && conversionConfig.export; this.nodes.conversionToggler.hidden = !exportRuleDefined; @@ -563,14 +527,10 @@ export default class InlineToolbar extends Module { /** * Get icon or title for dropdown */ - const toolSettings = Tools.getToolSettings(toolName); - const toolboxSettings = Tools.available[toolName][Tools.INTERNAL_SETTINGS.TOOLBOX] || {}; - const userToolboxSettings = toolSettings.toolbox || {}; + const toolboxSettings = currentBlock.tool.toolbox || {}; this.nodes.conversionTogglerContent.innerHTML = - userToolboxSettings.icon || toolboxSettings.icon || - userToolboxSettings.title || toolboxSettings.title || _.capitalize(toolName); } @@ -610,14 +570,12 @@ export default class InlineToolbar extends Module { * For this moment, inlineToolbarOrder could not be 'false' * because this method will be called only if the Inline Toolbar is enabled */ - const inlineToolbarOrder = this.getInlineToolbarSettings(currentBlock.name) as string[]; + const inlineToolbarOrder = this.getInlineToolbarSettings(currentBlock.tool) as string[]; inlineToolbarOrder.forEach((toolName) => { - const toolSettings = this.Editor.Tools.getToolSettings(toolName); - const tool = this.Editor.Tools.constructInline(this.Editor.Tools.inline[toolName], toolName, toolSettings); + const tool = this.Editor.Tools.inlineTools.get(toolName); - this.addTool(toolName, tool); - tool.checkState(SelectionUtils.get()); + this.addTool(tool); }); /** @@ -629,43 +587,42 @@ export default class InlineToolbar extends Module { /** * Add tool button and activate clicks * - * @param {string} toolName - name of Tool to add - * @param {InlineTool} tool - Tool class instance + * @param {InlineTool} tool - InlineTool object */ - private addTool(toolName: string, tool: InlineTool): void { + private addTool(tool: InlineTool): void { const { - Tools, Tooltip, } = this.Editor; - const button = tool.render(); + const instance = tool.instance(); + const button = instance.render(); if (!button) { - _.log('Render method must return an instance of Node', 'warn', toolName); + _.log('Render method must return an instance of Node', 'warn', tool.name); return; } - button.dataset.tool = toolName; + button.dataset.tool = tool.name; this.nodes.buttons.appendChild(button); - this.toolsInstances.set(toolName, tool); + this.toolsInstances.set(tool.name, instance); - if (_.isFunction(tool.renderActions)) { - const actions = tool.renderActions(); + if (_.isFunction(instance.renderActions)) { + const actions = instance.renderActions(); this.nodes.actions.appendChild(actions); } this.listeners.on(button, 'click', (event) => { - this.toolClicked(tool); + this.toolClicked(instance); event.preventDefault(); }); - const shortcut = this.getToolShortcut(toolName); + const shortcut = this.getToolShortcut(tool.name); if (shortcut) { try { - this.enableShortcuts(tool, shortcut); + this.enableShortcuts(instance, shortcut); } catch (e) {} } @@ -675,7 +632,7 @@ export default class InlineToolbar extends Module { const tooltipContent = $.make('div'); const toolTitle = I18n.t( I18nInternalNS.toolNames, - Tools.toolsClasses[toolName][Tools.INTERNAL_SETTINGS.TITLE] || _.capitalize(toolName) + tool.title || _.capitalize(tool.name) ); tooltipContent.appendChild($.text(toolTitle)); @@ -690,6 +647,8 @@ export default class InlineToolbar extends Module { placement: 'top', hidingDelay: 100, }); + + instance.checkState(SelectionUtils.get()); } /** @@ -704,21 +663,20 @@ export default class InlineToolbar extends Module { * Enable shortcuts * Ignore tool that doesn't have shortcut or empty string */ - const toolSettings = Tools.getToolSettings(toolName); - const tool = this.toolsInstances.get(toolName); + const tool = Tools.inlineTools.get(toolName); /** * 1) For internal tools, check public getter 'shortcut' * 2) For external tools, check tool's settings * 3) If shortcut is not set in settings, check Tool's public property */ - if (Object.keys(this.internalTools).includes(toolName)) { - return this.inlineTools[toolName][Tools.INTERNAL_SETTINGS.SHORTCUT]; - } else if (toolSettings && toolSettings[Tools.USER_SETTINGS.SHORTCUT]) { - return toolSettings[Tools.USER_SETTINGS.SHORTCUT]; - } else if (tool.shortcut) { - return tool.shortcut; + const internalTools = Tools.getInternal(ToolType.Inline); + + if (Array.from(internalTools.keys()).includes(toolName)) { + return this.inlineTools[toolName][CommonInternalSettings.Shortcut]; } + + return tool.shortcut; } /** @@ -727,7 +685,7 @@ export default class InlineToolbar extends Module { * @param {InlineTool} tool - Tool instance * @param {string} shortcut - shortcut according to the ShortcutData Module format */ - private enableShortcuts(tool: InlineTool, shortcut: string): void { + private enableShortcuts(tool: IInlineTool, shortcut: string): void { Shortcuts.add({ name: shortcut, handler: (event) => { @@ -747,9 +705,7 @@ export default class InlineToolbar extends Module { */ // if (SelectionUtils.isCollapsed) return; - const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name); - - if (!toolSettings || !toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS]) { + if (!currentBlock.tool.enabledInlineTools) { return; } @@ -765,7 +721,7 @@ export default class InlineToolbar extends Module { * * @param {InlineTool} tool - Tool's instance */ - private toolClicked(tool: InlineTool): void { + private toolClicked(tool: IInlineTool): void { const range = SelectionUtils.range; tool.surround(range); @@ -785,16 +741,14 @@ export default class InlineToolbar extends Module { * Get inline tools tools * Tools that has isInline is true */ - private get inlineTools(): { [name: string]: InlineTool } { + private get inlineTools(): { [name: string]: IInlineTool } { const result = {}; - for (const tool in this.Editor.Tools.inline) { - if (Object.prototype.hasOwnProperty.call(this.Editor.Tools.inline, tool)) { - const toolSettings = this.Editor.Tools.getToolSettings(tool); - - result[tool] = this.Editor.Tools.constructInline(this.Editor.Tools.inline[tool], tool, toolSettings); - } - } + Array + .from(this.Editor.Tools.inlineTools.entries()) + .forEach(([name, tool]) => { + result[name] = tool.instance(); + }); return result; } diff --git a/src/components/modules/toolbar/toolbox.ts b/src/components/modules/toolbar/toolbox.ts index add3e9e7..765696f1 100644 --- a/src/components/modules/toolbar/toolbox.ts +++ b/src/components/modules/toolbar/toolbox.ts @@ -1,12 +1,13 @@ import Module from '../../__module'; import $ from '../../dom'; import * as _ from '../../utils'; -import { BlockToolConstructable, ToolConstructable } from '../../../../types'; +import { BlockToolConstructable } from '../../../../types'; import Flipper from '../../flipper'; import { BlockToolAPI } from '../../block'; import I18n from '../../i18n'; import { I18nInternalNS } from '../../i18n/namespace-internal'; import Shortcuts from '../../utils/shortcuts'; +import BlockTool from '../../tools/block'; /** * HTMLElements used for Toolbox UI @@ -116,9 +117,7 @@ export default class Toolbox extends Module { * @param {string} toolName - button to activate */ public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void { - const tool = this.Editor.Tools.toolsClasses[toolName] as BlockToolConstructable; - - this.insertNewBlock(tool, toolName); + this.insertNewBlock(toolName); } /** @@ -162,36 +161,30 @@ export default class Toolbox extends Module { * Iterates available tools and appends them to the Toolbox */ private addTools(): void { - const tools = this.Editor.Tools.available; + const tools = this.Editor.Tools.blockTools; - for (const toolName in tools) { - if (Object.prototype.hasOwnProperty.call(tools, toolName)) { - this.addTool(toolName, tools[toolName] as BlockToolConstructable); - } - } + Array + .from(tools.values()) + .forEach((tool) => this.addTool(tool)); } /** * Append Tool to the Toolbox * - * @param {string} toolName - tool name - * @param {BlockToolConstructable} tool - tool class + * @param {BlockToolConstructable} tool - BlockTool object */ - private addTool(toolName: string, tool: BlockToolConstructable): void { - const internalSettings = this.Editor.Tools.INTERNAL_SETTINGS; - const userSettings = this.Editor.Tools.USER_SETTINGS; - - const toolToolboxSettings = tool[internalSettings.TOOLBOX]; + private addTool(tool: BlockTool): void { + const toolToolboxSettings = tool.toolbox; /** * Skip tools that don't pass 'toolbox' property */ - if (_.isEmpty(toolToolboxSettings)) { + if (!toolToolboxSettings) { return; } if (toolToolboxSettings && !toolToolboxSettings.icon) { - _.log('Toolbar icon is missed. Tool %o skipped', 'warn', toolName); + _.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name); return; } @@ -204,19 +197,10 @@ export default class Toolbox extends Module { // return; // } - const userToolboxSettings = this.Editor.Tools.getToolSettings(toolName)[userSettings.TOOLBOX]; - - /** - * Hide Toolbox button if Toolbox settings is false - */ - if ((userToolboxSettings ?? toolToolboxSettings) === false) { - return; - } - const button = $.make('li', [ this.CSS.toolboxButton ]); - button.dataset.tool = toolName; - button.innerHTML = (userToolboxSettings && userToolboxSettings.icon) || toolToolboxSettings.icon; + button.dataset.tool = tool.name; + button.innerHTML = toolToolboxSettings.icon; $.append(this.nodes.toolbox, button); @@ -227,61 +211,40 @@ export default class Toolbox extends Module { * Add click listener */ this.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => { - this.toolButtonActivate(event, toolName); + this.toolButtonActivate(event, tool.name); }); /** * Add listeners to show/hide toolbox tooltip */ - const tooltipContent = this.drawTooltip(toolName); + const tooltipContent = this.drawTooltip(tool); this.Editor.Tooltip.onHover(button, tooltipContent, { placement: 'bottom', hidingDelay: 200, }); - const shortcut = this.getToolShortcut(toolName, tool); + const shortcut = tool.shortcut; if (shortcut) { - this.enableShortcut(tool, toolName, shortcut); + this.enableShortcut(tool.name, shortcut); } /** Increment Tools count */ this.displayedToolsCount++; } - /** - * Returns tool's shortcut - * It can be specified via internal 'shortcut' static getter or by user settings for tool - * - * @param {string} toolName - tool's name - * @param {ToolConstructable} tool - tool's class (not instance) - */ - private getToolShortcut(toolName: string, tool: ToolConstructable): string|null { - /** - * Enable shortcut - */ - const toolSettings = this.Editor.Tools.getToolSettings(toolName); - const internalToolShortcut = tool[this.Editor.Tools.INTERNAL_SETTINGS.SHORTCUT]; - const userSpecifiedShortcut = toolSettings ? toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT] : null; - - return userSpecifiedShortcut || internalToolShortcut; - } - /** * Draw tooltip for toolbox tools * - * @param {string} toolName - toolbox tool name + * @param tool - BlockTool object * @returns {HTMLElement} */ - private drawTooltip(toolName: string): HTMLElement { - const tool = this.Editor.Tools.available[toolName]; - const toolSettings = this.Editor.Tools.getToolSettings(toolName); - const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {}; - const userToolboxSettings = toolSettings.toolbox || {}; - const name = I18n.t(I18nInternalNS.toolNames, userToolboxSettings.title || toolboxSettings.title || toolName); + private drawTooltip(tool: BlockTool): HTMLElement { + const toolboxSettings = tool.toolbox || {}; + const name = I18n.t(I18nInternalNS.toolNames, toolboxSettings.title || tool.name); - let shortcut = this.getToolShortcut(toolName, tool); + let shortcut = tool.shortcut; const tooltip = $.make('div', this.CSS.buttonTooltip); const hint = document.createTextNode(_.capitalize(name)); @@ -302,16 +265,15 @@ export default class Toolbox extends Module { /** * Enable shortcut Block Tool implemented shortcut * - * @param {BlockToolConstructable} tool - Tool class * @param {string} toolName - Tool name * @param {string} shortcut - shortcut according to the ShortcutData Module format */ - private enableShortcut(tool: BlockToolConstructable, toolName: string, shortcut: string): void { + private enableShortcut(toolName: string, shortcut: string): void { Shortcuts.add({ name: shortcut, handler: (event: KeyboardEvent) => { event.preventDefault(); - this.insertNewBlock(tool, toolName); + this.insertNewBlock(toolName); }, on: this.Editor.UI.nodes.redactor, }); @@ -322,17 +284,17 @@ export default class Toolbox extends Module { * Fired when the Read-Only mode is activated */ private removeAllShortcuts(): void { - const tools = this.Editor.Tools.available; + const tools = this.Editor.Tools.blockTools; - for (const toolName in tools) { - if (Object.prototype.hasOwnProperty.call(tools, toolName)) { - const shortcut = this.getToolShortcut(toolName, tools[toolName]); + Array + .from(tools.values()) + .forEach((tool) => { + const shortcut = tool.shortcut; if (shortcut) { Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut); } - } - } + }); } /** @@ -351,10 +313,9 @@ export default class Toolbox extends Module { * Inserts new block * Can be called when button clicked on Toolbox or by ShortcutData * - * @param {BlockToolConstructable} tool - Tool Class * @param {string} toolName - Tool name */ - private insertNewBlock(tool: BlockToolConstructable, toolName: string): void { + private insertNewBlock(toolName: string): void { const { BlockManager, Caret } = this.Editor; const { currentBlock } = BlockManager; diff --git a/src/components/modules/tools.ts b/src/components/modules/tools.ts index ddcf66b1..5c4842b6 100644 --- a/src/components/modules/tools.ts +++ b/src/components/modules/tools.ts @@ -1,21 +1,21 @@ -import Paragraph from '../tools/paragraph/dist/bundle'; +import Paragraph from '../../tools/paragraph/dist/bundle'; import Module from '../__module'; import * as _ from '../utils'; import { - BlockToolConstructable, EditorConfig, - InlineTool, - InlineToolConstructable, Tool, - ToolConfig, + 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 { ModuleConfig } from '../../types-internal/module-config'; -import EventsDispatcher from '../utils/events'; +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 BaseTool from '../tools/base'; /** * @module Editor.js Tools Submodule @@ -23,6 +23,8 @@ import EventsDispatcher from '../utils/events'; * Creates Instances from Plugins and binds external config to the instances */ +type ToolClass = BlockTool | InlineTool | BlockTune; + /** * Class properties: * @@ -47,7 +49,7 @@ export default class Tools extends Module { * * @returns {object} */ - public get available(): { [name: string]: ToolConstructable } { + public get available(): Map { return this.toolsAvailable; } @@ -56,7 +58,7 @@ export default class Tools extends Module { * * @returns {Tool[]} */ - public get unavailable(): { [name: string]: ToolConstructable } { + public get unavailable(): Map { return this.toolsUnavailable; } @@ -65,48 +67,40 @@ export default class Tools extends Module { * * @returns {object} - object of Inline Tool's classes */ - public get inline(): { [name: string]: InlineToolConstructable } { + public get inlineTools(): Map { if (this._inlineTools) { return this._inlineTools; } - const tools = Object.entries(this.available).filter(([name, tool]) => { - if (!tool[this.INTERNAL_SETTINGS.IS_INLINE]) { - return false; - } + const tools = Array + .from(this.available.entries()) + .filter(([name, tool]: [string, BaseTool]) => { + if (tool.type !== ToolType.Inline) { + return false; + } + /** + * Some Tools validation + */ + const inlineToolRequiredMethods = ['render', 'surround', 'checkState']; + const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !tool.instance()[method]); - /** - * Some Tools validation - */ - const inlineToolRequiredMethods = ['render', 'surround', 'checkState']; - const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !this.constructInline(tool, name)[method]); + if (notImplementedMethods.length) { + _.log( + `Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`, + 'warn', + notImplementedMethods + ); - if (notImplementedMethods.length) { - _.log( - `Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`, - 'warn', - notImplementedMethods - ); + return false; + } - return false; - } - - return true; - }); - - /** - * collected inline tools with key of tool name - */ - const result = {}; - - tools.forEach(([name, tool]) => { - result[name] = tool; - }); + return true; + }); /** * Cache prepared Tools */ - this._inlineTools = result; + this._inlineTools = new Map(tools) as Map; return this._inlineTools; } @@ -114,79 +108,43 @@ export default class Tools extends Module { /** * Return editor block tools */ - public get blockTools(): { [name: string]: BlockToolConstructable } { - const tools = Object.entries(this.available).filter(([, tool]) => { - return !tool[this.INTERNAL_SETTINGS.IS_INLINE]; - }); + public get blockTools(): Map { + if (this._blockTools) { + return this._blockTools; + } - /** - * collected block tools with key of tool name - */ - const result = {}; + const tools = Array + .from(this.available.entries()) + .filter(([, tool]) => { + return tool.type === ToolType.Block; + }); - tools.forEach(([name, tool]) => { - result[name] = tool; - }); + this._blockTools = new Map(tools) as Map; - return result; + return this._blockTools; } /** - * Constant for available Tools internal settings provided by Tool developer - * - * @returns {object} + * Returns default Tool object */ - public get INTERNAL_SETTINGS(): { [name: string]: string } { - return { - IS_ENABLED_LINE_BREAKS: 'enableLineBreaks', - IS_INLINE: 'isInline', - TITLE: 'title', // for Inline Tools. Block Tools can pass title along with icon through the 'toolbox' static prop. - SHORTCUT: 'shortcut', - TOOLBOX: 'toolbox', - SANITIZE_CONFIG: 'sanitize', - CONVERSION_CONFIG: 'conversionConfig', - IS_READ_ONLY_SUPPORTED: 'isReadOnlySupported', - }; + public get defaultTool(): BlockTool { + return this.blockTools.get(this.config.defaultBlock); } /** - * Constant for available Tools settings provided by user - * - * return {object} + * Tools objects factory */ - public get USER_SETTINGS(): { [name: string]: string } { - return { - SHORTCUT: 'shortcut', - TOOLBOX: 'toolbox', - ENABLED_INLINE_TOOLS: 'inlineToolbar', - CONFIG: 'config', - }; - } - - /** - * Map {name: Class, ...} where: - * name — block type name in JSON. Got from EditorConfig.tools keys - * - * @type {object} - */ - public readonly toolsClasses: { [name: string]: ToolConstructable } = {}; + private factory: ToolsFactory; /** * Tools` classes available to use */ - private readonly toolsAvailable: { [name: string]: ToolConstructable } = {}; + private readonly toolsAvailable: Map = new Map(); /** * Tools` classes not available to use because of preparation failure */ - private readonly toolsUnavailable: { [name: string]: ToolConstructable } = {}; - - /** - * Tools settings in a map {name: settings, ...} - * - * @type {object} - */ - private readonly toolsSettings: { [name: string]: ToolSettings } = {}; + private readonly toolsUnavailable: Map = new Map(); /** * Cache for the prepared inline tools @@ -194,41 +152,30 @@ export default class Tools extends Module { * @type {null|object} * @private */ - private _inlineTools: { [name: string]: ToolConstructable } = {}; + private _inlineTools: Map = null; /** - * @class - * - * @param {EditorConfig} config - Editor's configuration - * @param {EventsDispatcher} eventsDispatcher - Editor's event dispatcher + * Cache for the prepared block tools */ - constructor({ config, eventsDispatcher }: ModuleConfig) { - super({ - config, - eventsDispatcher, - }); + private _blockTools: Map = null; - this.toolsClasses = {}; + /** + * Returns internal tools + * + * @param type - if passed, Tools will be filtered by type + */ + public getInternal(type?: ToolType): Map { + let tools = Array + .from(this.available.entries()) + .filter(([, tool]) => { + return tool.isInternal; + }); - this.toolsSettings = {}; + if (type) { + tools = tools.filter(([, tool]) => tool.type === type); + } - /** - * Available tools list - * {name: Class, ...} - * - * @type {object} - */ - this.toolsAvailable = {}; - - /** - * Tools that rejected a prepare method - * {name: Class, ... } - * - * @type {object} - */ - this.toolsUnavailable = {}; - - this._inlineTools = null; + return new Map(tools); } /** @@ -248,54 +195,14 @@ export default class Tools extends Module { throw Error('Can\'t start without tools'); } - /** - * 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])) { - /** - * Save Tool's class from 'class' field - * - * @type {Tool} - */ - this.toolsClasses[toolName] = (this.config.tools[toolName] as ToolSettings).class; + const config = this.prepareConfig(); - /** - * Save Tool's settings - * - * @type {ToolSettings} - */ - this.toolsSettings[toolName] = this.config.tools[toolName] as ToolSettings; - - /** - * Remove Tool's class from settings - */ - delete this.toolsSettings[toolName].class; - } else { - /** - * Save Tool's class - * - * @type {Tool} - */ - this.toolsClasses[toolName] = this.config.tools[toolName] as ToolConstructable; - - /** - * Set empty settings for Block by default - * - * @type {{}} - */ - this.toolsSettings[toolName] = { class: this.config.tools[toolName] as ToolConstructable }; - } - } + this.factory = new ToolsFactory(config, this.config, this.Editor.API); /** * getting classes that has prepare method */ - const sequenceData = this.getListOfPrepareFunctions(); + const sequenceData = this.getListOfPrepareFunctions(config); /** * if sequence data contains nothing then resolve current chain and run other module prepare @@ -308,110 +215,42 @@ export default class Tools extends Module { * to see how it works {@link '../utils.ts#sequence'} */ return _.sequence(sequenceData, (data: { toolName: string }) => { - this.success(data); + this.toolPrepareMethodSuccess(data); }, (data: { toolName: string }) => { - this.fallback(data); + this.toolPrepareMethodFallback(data); }); } - /** - * Success callback - * - * @param {object} data - append tool to available list - */ - public success(data: { toolName: string }): void { - this.toolsAvailable[data.toolName] = this.toolsClasses[data.toolName]; - } - - /** - * Fail callback - * - * @param {object} data - append tool to unavailable list - */ - public fallback(data: { toolName: string }): void { - this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName]; - } - - /** - * Return Inline Tool's instance - * - * @param {InlineTool} tool - Inline Tool instance - * @param {string} name - tool name - * @param {ToolSettings} toolSettings - tool settings - * - * @returns {InlineTool} — instance - */ - public constructInline( - tool: InlineToolConstructable, - name: string, - toolSettings: ToolSettings = {} as ToolSettings - ): InlineTool { - const constructorOptions = { - api: this.Editor.API.getMethodsForTool(name), - config: (toolSettings[this.USER_SETTINGS.CONFIG] || {}) as ToolSettings, - }; - - // eslint-disable-next-line new-cap - return new tool(constructorOptions) as InlineTool; - } - - /** - * Check if passed Tool is an instance of Default Block Tool - * - * @param {Tool} tool - Tool to check - * - * @returns {boolean} - */ - public isDefault(tool): boolean { - return tool instanceof this.available[this.config.defaultBlock]; - } - - /** - * Return Tool's config by name - * - * @param {string} toolName - name of tool - * - * @returns {ToolSettings} - */ - public getToolSettings(toolName): ToolSettings { - const settings = this.toolsSettings[toolName]; - const config = settings[this.USER_SETTINGS.CONFIG] || {}; - - // Pass placeholder to default Block config - if (toolName === this.config.defaultBlock && !config.placeholder) { - config.placeholder = this.config.placeholder; - settings[this.USER_SETTINGS.CONFIG] = config; - } - - return settings; - } - /** * Returns internal tools * Includes Bold, Italic, Link and Paragraph */ - public get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings } { + public get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings & { isInternal?: boolean } } { return { - bold: { class: BoldInlineTool }, - italic: { class: ItalicInlineTool }, - link: { class: LinkInlineTool }, + 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, }, - stub: { class: Stub }, }; } - /** - * Returns true if tool supports read-only mode - * - * @param tool - tool to check - */ - public isReadOnlySupported(tool: BlockToolConstructable): boolean { - return tool[this.INTERNAL_SETTINGS.IS_READ_ONLY_SUPPORTED] === true; - } - /** * Calls each Tool reset method to clean up anything set by Tool */ @@ -423,41 +262,50 @@ export default class Tools extends Module { }); } + /** + * Tool prepare method success callback + * + * @param {object} data - append tool to available list + */ + private toolPrepareMethodSuccess(data: { toolName: string }): void { + this.toolsAvailable.set(data.toolName, this.factory.get(data.toolName)); + } + + /** + * 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(): Array<{ - function: (data: { toolName: string; config: ToolConfig }) => void; - data: { toolName: string; config: ToolConfig }; - }> { - const toolPreparationList: Array<{ - function: (data: { toolName: string; config: ToolConfig }) => void; - data: { toolName: string; config: ToolConfig }; - } - > = []; + private getListOfPrepareFunctions(config: {[name: string]: ToolSettings}): { + function: (data: { toolName: string }) => void | Promise; + data: { toolName: string }; + }[] { + const toolPreparationList: { + function: (data: { toolName: string }) => void | Promise; + data: { toolName: string }; + }[] = []; - for (const toolName in this.toolsClasses) { - if (Object.prototype.hasOwnProperty.call(this.toolsClasses, toolName)) { - const toolClass = this.toolsClasses[toolName]; - const toolConfig = this.toolsSettings[toolName][this.USER_SETTINGS.CONFIG]; - - /** - * If Tool hasn't a prepare method, - * still push it to tool preparation list to save tools order in Toolbox. - * As Tool's prepare method might be async, _.sequence util helps to save the order. - */ + Object + .entries(config) + .forEach(([toolName, settings]) => { toolPreparationList.push({ // eslint-disable-next-line @typescript-eslint/no-empty-function - function: _.isFunction(toolClass.prepare) ? toolClass.prepare : (): void => { }, + function: _.isFunction(settings.class.prepare) ? settings.class.prepare : (): void => {}, data: { toolName, - config: toolConfig, }, }); - } - } + }); return toolPreparationList; } @@ -485,6 +333,30 @@ export default class Tools extends Module { } } } + + /** + * 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; + } } /** diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index 1d9874b3..bc81516c 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -702,7 +702,7 @@ export default class UI extends Module { * - Block is an default-block (Text) * - Block is empty */ - const isDefaultBlock = this.Editor.Tools.isDefault(this.Editor.BlockManager.currentBlock.tool); + const isDefaultBlock = this.Editor.BlockManager.currentBlock.tool.isDefault; if (isDefaultBlock) { stopPropagation(); diff --git a/src/components/tools/base.ts b/src/components/tools/base.ts new file mode 100644 index 00000000..179c44e6 --- /dev/null +++ b/src/components/tools/base.ts @@ -0,0 +1,235 @@ +import { ToolType } from '../modules/tools'; +import { Tool, ToolConstructable, ToolSettings } from '../../../types/tools'; +import { API, SanitizerConfig } from '../../../types'; +import * as _ from '../utils'; + +/** + * Enum of Tool options provided by user + */ +export enum UserSettings { + /** + * Shortcut for Tool + */ + Shortcut = 'shortcut', + /** + * Toolbox config for Tool + */ + Toolbox = 'toolbox', + /** + * Enabled Inline Tools for Block Tool + */ + EnabledInlineTools = 'inlineToolbar', + /** + * Tool configuration + */ + Config = 'config', +} + +/** + * Enum of Tool options provided by Tool + */ +export enum CommonInternalSettings { + /** + * Shortcut for Tool + */ + Shortcut = 'shortcut', + /** + * Sanitize configuration for Tool + */ + SanitizeConfig = 'sanitize', + +} + +/** + * Enum of Tool optoins provided by Block Tool + */ +export enum InternalBlockToolSettings { + /** + * Is linebreaks enabled for Tool + */ + IsEnabledLineBreaks = 'enableLineBreaks', + /** + * Tool Toolbox config + */ + Toolbox = 'toolbox', + /** + * Tool conversion config + */ + ConversionConfig = 'conversionConfig', + /** + * Is readonly mode supported for Tool + */ + IsReadOnlySupported = 'isReadOnlySupported', + /** + * Tool paste config + */ + PasteConfig = 'pasteConfig' +} + +/** + * Enum of Tool options provided by Inline Tool + */ +export enum InternalInlineToolSettings { + /** + * Flag specifies Tool is inline + */ + IsInline = 'isInline', + /** + * Inline Tool title for toolbar + */ + Title = 'title', // for Inline Tools. Block Tools can pass title along with icon through the 'toolbox' static prop. +} + +/** + * Enum of Tool options provided by Block Tune + */ +export enum InternalTuneSettings { + /** + * Flag specifies Tool is Block Tune + */ + IsTune = 'isTune', +} + +export type ToolOptions = Omit + +interface ConstructorOptions { + name: string; + constructable: ToolConstructable; + config: ToolOptions; + api: API; + isDefault: boolean; + isInternal: boolean; + defaultPlaceholder?: string | false; +} + +/** + * Base abstract class for Tools + */ +export default abstract class BaseTool { + /** + * Tool type: Block, Inline or Tune + */ + public type: ToolType; + + /** + * Tool name specified in EditorJS config + */ + public name: string; + + /** + * Flag show is current Tool internal (bundled with EditorJS core) or not + */ + public readonly isInternal: boolean; + + /** + * Flag show is current Tool default or not + */ + public readonly isDefault: boolean; + + /** + * EditorJS API for current Tool + */ + protected api: API; + + /** + * Current tool user configuration + */ + protected config: ToolOptions; + + /** + * Tool's constructable blueprint + */ + protected constructable: ToolConstructable; + + /** + * Default placeholder specified in EditorJS user configuration + */ + protected defaultPlaceholder?: string | false; + + /** + * @class + * + * @param name - Tool name + * @param constructable - Tool constructable blueprint + * @param config - user specified Tool config + * @param api - EditorJS API module + * @param defaultTool - default Tool name + * @param isInternal - is current Tool internal + * @param defaultPlaceholder - default user specified placeholder + */ + constructor({ + name, + constructable, + config, + api, + isDefault, + isInternal = false, + defaultPlaceholder, + }: ConstructorOptions) { + this.api = api; + this.name = name; + this.constructable = constructable; + this.config = config; + this.isDefault = isDefault; + this.isInternal = isInternal; + this.defaultPlaceholder = defaultPlaceholder; + } + + /** + * Returns Tool user configuration + */ + public get settings(): ToolOptions { + const config = this.config[UserSettings.Config] || {}; + + if (this.isDefault && !('placeholder' in config) && this.defaultPlaceholder) { + config.placeholder = this.defaultPlaceholder; + } + + return config; + } + + /** + * Calls Tool's reset method + */ + public reset(): void | Promise { + if (_.isFunction(this.constructable.reset)) { + return this.constructable.reset(); + } + } + + /** + * Calls Tool's prepare method + */ + public prepare(): void | Promise { + if (_.isFunction(this.constructable.prepare)) { + return this.constructable.prepare({ + toolName: this.name, + config: this.settings, + }); + } + } + + /** + * Returns shortcut for Tool (internal or specified by user) + */ + public get shortcut(): string | undefined { + const toolShortcut = this.constructable[CommonInternalSettings.Shortcut]; + const userShortcut = this.settings[UserSettings.Shortcut]; + + return userShortcut || toolShortcut; + } + + /** + * Returns Tool's sanitizer configuration + */ + public get sanitizeConfig(): SanitizerConfig { + return this.constructable[CommonInternalSettings.SanitizeConfig]; + } + + /** + * Constructs new Tool instance from constructable blueprint + * + * @param args + */ + public abstract instance(...args: any[]): Type; +} diff --git a/src/components/tools/block.ts b/src/components/tools/block.ts new file mode 100644 index 00000000..3e20e98d --- /dev/null +++ b/src/components/tools/block.ts @@ -0,0 +1,92 @@ +import BaseTool, { InternalBlockToolSettings, UserSettings } from './base'; +import { ToolType } from '../modules/tools'; +import { + BlockAPI, + BlockTool as IBlockTool, + BlockToolData, + ConversionConfig, + PasteConfig, + ToolboxConfig +} from '../../../types'; +import * as _ from '../utils'; + +/** + * Class to work with Block tools constructables + */ +export default class BlockTool extends BaseTool { + /** + * Tool type — Block + */ + public type = ToolType.Block; + + /** + * Creates new Tool instance + * + * @param data - Tool data + * @param block - BlockAPI for current Block + * @param readOnly - True if Editor is in read-only mode + */ + public instance(data: BlockToolData, block: BlockAPI, readOnly: boolean): IBlockTool { + // eslint-disable-next-line new-cap + return new this.constructable({ + data, + block, + readOnly, + api: this.api, + config: this.settings, + }) as IBlockTool; + } + + /** + * Returns true if read-only mode is supported by Tool + */ + public get isReadOnlySupported(): boolean { + return this.constructable[InternalBlockToolSettings.IsReadOnlySupported] === true; + } + + /** + * Returns true if Tool supports linebreaks + */ + public get isLineBreaksEnabled(): boolean { + return this.constructable[InternalBlockToolSettings.IsEnabledLineBreaks]; + } + + /** + * Returns Tool toolbox configuration (internal or user-specified) + */ + public get toolbox(): ToolboxConfig { + const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig; + const userToolboxSettings = this.settings[UserSettings.Toolbox]; + + if (_.isEmpty(toolToolboxSettings)) { + return; + } + + if ((userToolboxSettings ?? toolToolboxSettings) === false) { + return; + } + + return Object.assign({}, toolToolboxSettings, userToolboxSettings); + } + + /** + * Returns Tool conversion configuration + */ + public get conversionConfig(): ConversionConfig { + return this.constructable[InternalBlockToolSettings.ConversionConfig]; + } + + /** + * Returns enabled inline tools for Tool + */ + public get enabledInlineTools(): boolean | string[] { + return this.config[UserSettings.EnabledInlineTools]; + } + + /** + * Returns Tool paste configuration + */ + public get pasteConfig(): PasteConfig { + return this.constructable[InternalBlockToolSettings.PasteConfig] || {}; + } +} diff --git a/src/components/tools/factory.ts b/src/components/tools/factory.ts new file mode 100644 index 00000000..d0d96a36 --- /dev/null +++ b/src/components/tools/factory.ts @@ -0,0 +1,84 @@ +import { ToolConstructable, ToolSettings } from '../../../types/tools'; +import { InternalInlineToolSettings, InternalTuneSettings } from './base'; +import InlineTool from './inline'; +import BlockTune from './tune'; +import BlockTool from './block'; +import API from '../modules/api'; +import { ToolType } from '../modules/tools'; +import { EditorConfig } from '../../../types/configs'; + +type ToolConstructor = typeof InlineTool | typeof BlockTool | typeof BlockTune; + +/** + * Factory to construct classes to work with tools + */ +export default class ToolsFactory { + /** + * Tools configuration specified by user + */ + private config: {[name: string]: ToolSettings & { isInternal?: boolean }}; + + /** + * EditorJS API Module + */ + private api: API; + + /** + * EditorJS configuration + */ + private editorConfig: EditorConfig; + + /** + * @class + * + * @param config - tools config + * @param editorConfig - EditorJS config + * @param api - EditorJS API module + */ + constructor( + config: {[name: string]: ToolSettings & { isInternal?: boolean }}, + editorConfig: EditorConfig, + api: API + ) { + this.api = api; + this.config = config; + this.editorConfig = editorConfig; + } + + /** + * Returns Tool object based on it's type + * + * @param name - tool name + */ + public get(name: string): InlineTool | BlockTool | BlockTune { + const { class: constructable, isInternal = false, ...config } = this.config[name]; + + const [Constructor, type] = this.getConstructor(constructable); + + return new Constructor({ + name, + constructable, + config, + api: this.api.getMethodsForTool(name, type), + isDefault: name === this.editorConfig.defaultBlock, + defaultPlaceholder: this.editorConfig.placeholder, + isInternal, + }); + } + + /** + * Find appropriate Tool object constructor for Tool constructable + * + * @param constructable - Tools constructable + */ + private getConstructor(constructable: ToolConstructable): [ToolConstructor, ToolType] { + switch (true) { + case constructable[InternalInlineToolSettings.IsInline]: + return [InlineTool, ToolType.Inline]; + case constructable[InternalTuneSettings.IsTune]: + return [BlockTune, ToolType.Tune]; + default: + return [BlockTool, ToolType.Block]; + } + } +} diff --git a/src/components/tools/inline.ts b/src/components/tools/inline.ts new file mode 100644 index 00000000..257866cc --- /dev/null +++ b/src/components/tools/inline.ts @@ -0,0 +1,31 @@ +import BaseTool, { InternalInlineToolSettings } from './base'; +import { ToolType } from '../modules/tools'; +import { InlineTool as IInlineTool } from '../../../types'; + +/** + * InlineTool object to work with Inline Tools constructables + */ +export default class InlineTool extends BaseTool { + /** + * Tool type — Inline + */ + public type = ToolType.Inline; + + /** + * Returns title for Inline Tool if specified by user + */ + public get title(): string { + return this.constructable[InternalInlineToolSettings.Title]; + } + + /** + * Constructs new InlineTool instance from constructable + */ + public instance(): IInlineTool { + // eslint-disable-next-line new-cap + return new this.constructable({ + api: this.api, + config: this.settings, + }) as IInlineTool; + } +} diff --git a/src/components/tools/tune.ts b/src/components/tools/tune.ts new file mode 100644 index 00000000..799dafe0 --- /dev/null +++ b/src/components/tools/tune.ts @@ -0,0 +1,21 @@ +import BaseTool from './base'; +import { ToolType } from '../modules/tools'; + +/** + * Stub class for BlockTunes + * + * @todo Implement + */ +export default class BlockTune extends BaseTool { + /** + * Tool type — Tune + */ + public type = ToolType.Tune; + + /** + * @todo implement + */ + public instance(): any { + return undefined; + } +} diff --git a/src/components/tools/paragraph b/src/tools/paragraph similarity index 100% rename from src/components/tools/paragraph rename to src/tools/paragraph diff --git a/src/components/tools/stub/index.ts b/src/tools/stub/index.ts similarity index 94% rename from src/components/tools/stub/index.ts rename to src/tools/stub/index.ts index 38f98b15..025066c6 100644 --- a/src/components/tools/stub/index.ts +++ b/src/tools/stub/index.ts @@ -1,5 +1,5 @@ -import $ from '../../dom'; -import { API, BlockTool, BlockToolData, BlockToolConstructorOptions } from '../../../../types'; +import $ from '../../components/dom'; +import { API, BlockTool, BlockToolConstructorOptions, BlockToolData } from '../../../types'; export interface StubData extends BlockToolData { title: string; diff --git a/types/tools/inline-tool.d.ts b/types/tools/inline-tool.d.ts index 705a1491..00b96e27 100644 --- a/types/tools/inline-tool.d.ts +++ b/types/tools/inline-tool.d.ts @@ -1,5 +1,5 @@ import {BaseTool, BaseToolConstructable} from './tool'; -import {API, ToolConfig} from "../index"; +import {API, ToolConfig} from '../index'; /** * Base structure for the Inline Toolbar Tool */