[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
This commit is contained in:
George Berezhnoy 2021-03-31 23:29:41 +03:00 committed by GitHub
parent 51a1b48abb
commit 4cfcb656a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 875 additions and 643 deletions

4
.gitmodules vendored
View file

@ -16,8 +16,8 @@
[submodule "example/tools/simple-image"] [submodule "example/tools/simple-image"]
path = example/tools/simple-image path = example/tools/simple-image
url = https://github.com/editor-js/simple-image url = https://github.com/editor-js/simple-image
[submodule "src/components/tools/paragraph"] [submodule "src/tools/paragraph"]
path = src/components/tools/paragraph path = src/tools/paragraph
url = https://github.com/editor-js/paragraph url = https://github.com/editor-js/paragraph
[submodule "example/tools/marker"] [submodule "example/tools/marker"]
path = example/tools/marker path = example/tools/marker

View file

@ -1,6 +1,6 @@
import { import {
BlockAPI as BlockAPIInterface, BlockAPI as BlockAPIInterface,
BlockTool, BlockTool as IBlockTool,
BlockToolConstructable, BlockToolConstructable,
BlockToolData, BlockToolData,
BlockTune, BlockTune,
@ -16,12 +16,13 @@ import * as _ from '../utils';
import ApiModules from '../modules/api'; import ApiModules from '../modules/api';
import BlockAPI from './api'; import BlockAPI from './api';
import { ToolType } from '../modules/tools'; import { ToolType } from '../modules/tools';
import SelectionUtils from '../selection';
import BlockTool from '../tools/block';
/** Import default tunes */ /** Import default tunes */
import MoveUpTune from '../block-tunes/block-tune-move-up'; import MoveUpTune from '../block-tunes/block-tune-move-up';
import DeleteTune from '../block-tunes/block-tune-delete'; import DeleteTune from '../block-tunes/block-tune-delete';
import MoveDownTune from '../block-tunes/block-tune-move-down'; import MoveDownTune from '../block-tunes/block-tune-move-down';
import SelectionUtils from '../selection';
/** /**
* Interface describes Block class constructor argument * Interface describes Block class constructor argument
@ -38,14 +39,9 @@ interface BlockConstructorOptions {
data: BlockToolData; data: BlockToolData;
/** /**
* Tool's class or constructor function * Tool object
*/ */
Tool: BlockToolConstructable; tool: BlockTool;
/**
* Tool settings from initial config
*/
settings: ToolSettings;
/** /**
* Editor's API methods * Editor's API methods
@ -110,32 +106,27 @@ export default class Block {
/** /**
* Block Tool`s name * Block Tool`s name
*/ */
public name: string; public readonly name: string;
/** /**
* Instance of the Tool Block represents * Instance of the Tool Block represents
*/ */
public tool: BlockTool; public readonly tool: BlockTool;
/**
* Class blueprint of the ool Block represents
*/
public class: BlockToolConstructable;
/** /**
* User Tool configuration * User Tool configuration
*/ */
public settings: ToolConfig; public readonly settings: ToolConfig;
/** /**
* Wrapper for Block`s content * Wrapper for Block`s content
*/ */
public holder: HTMLDivElement; public readonly holder: HTMLDivElement;
/** /**
* Tunes used by Tool * Tunes used by Tool
*/ */
public tunes: BlockTune[]; public readonly tunes: BlockTune[];
/** /**
* Tool's user configuration * Tool's user configuration
@ -149,6 +140,11 @@ export default class Block {
*/ */
private cachedInputs: HTMLElement[] = []; private cachedInputs: HTMLElement[] = [];
/**
* Tool class instance
*/
private readonly toolInstance: IBlockTool;
/** /**
* Editor`s API module * Editor`s API module
*/ */
@ -209,27 +205,20 @@ export default class Block {
constructor({ constructor({
name, name,
data, data,
Tool, tool,
settings,
api, api,
readOnly, readOnly,
}: BlockConstructorOptions) { }: BlockConstructorOptions) {
this.name = name; this.name = name;
this.class = Tool; this.settings = tool.settings;
this.settings = settings; this.config = tool.settings.config || {};
this.config = settings.config || {};
this.api = api; this.api = api;
this.blockAPI = new BlockAPI(this); this.blockAPI = new BlockAPI(this);
this.mutationObserver = new MutationObserver(this.didMutated); this.mutationObserver = new MutationObserver(this.didMutated);
this.tool = new Tool({ this.tool = tool;
data, this.toolInstance = tool.instance(data, this.blockAPI, readOnly);
config: this.config,
api: this.api.getMethodsForTool(name, ToolType.Block),
block: this.blockAPI,
readOnly,
});
this.holder = this.compose(); this.holder = this.compose();
/** /**
@ -349,7 +338,7 @@ export default class Block {
* @returns {object} * @returns {object}
*/ */
public get sanitize(): SanitizerConfig { public get sanitize(): SanitizerConfig {
return this.tool.sanitize; return this.tool.sanitizeConfig;
} }
/** /**
@ -359,7 +348,7 @@ export default class Block {
* @returns {boolean} * @returns {boolean}
*/ */
public get mergeable(): 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 * 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) { if (methodName === BlockToolAPI.APPEND_CALLBACK) {
_.log( _.log(
'`appendCallback` hook is deprecated and will be removed in the next major release. ' + '`appendCallback` hook is deprecated and will be removed in the next major release. ' +
@ -513,7 +502,7 @@ export default class Block {
try { try {
// eslint-disable-next-line no-useless-call // eslint-disable-next-line no-useless-call
this.tool[methodName].call(this.tool, params); this.toolInstance[methodName].call(this.toolInstance, params);
} catch (e) { } catch (e) {
_.log(`Error during '${methodName}' call: ${e.message}`, 'error'); _.log(`Error during '${methodName}' call: ${e.message}`, 'error');
} }
@ -526,7 +515,7 @@ export default class Block {
* @param {BlockToolData} data - data to merge * @param {BlockToolData} data - data to merge
*/ */
public async mergeWith(data: BlockToolData): Promise<void> { public async mergeWith(data: BlockToolData): Promise<void> {
await this.tool.merge(data); await this.toolInstance.merge(data);
} }
/** /**
@ -536,7 +525,7 @@ export default class Block {
* @returns {object} * @returns {object}
*/ */
public async save(): Promise<void|SavedData> { public async save(): Promise<void|SavedData> {
const extractedBlock = await this.tool.save(this.pluginsContent as HTMLElement); const extractedBlock = await this.toolInstance.save(this.pluginsContent as HTMLElement);
/** /**
* Measuring execution time * Measuring execution time
@ -572,8 +561,8 @@ export default class Block {
public async validate(data: BlockToolData): Promise<boolean> { public async validate(data: BlockToolData): Promise<boolean> {
let isValid = true; let isValid = true;
if (this.tool.validate instanceof Function) { if (this.toolInstance.validate instanceof Function) {
isValid = await this.tool.validate(data); isValid = await this.toolInstance.validate(data);
} }
return isValid; return isValid;
@ -672,6 +661,24 @@ export default class Block {
this.removeInputEvents(); 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 * Make default Block wrappers and put Tool`s content there
* *
@ -680,7 +687,7 @@ export default class Block {
private compose(): HTMLDivElement { private compose(): HTMLDivElement {
const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement, const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement,
contentNode = $.make('div', Block.CSS.content), contentNode = $.make('div', Block.CSS.content),
pluginsContent = this.tool.render(); pluginsContent = this.toolInstance.render();
contentNode.appendChild(pluginsContent); contentNode.appendChild(pluginsContent);
wrapper.appendChild(contentNode); wrapper.appendChild(contentNode);

View file

@ -125,7 +125,7 @@ export default class BlockEvents extends Module {
return; return;
} }
const canOpenToolbox = Tools.isDefault(currentBlock.tool) && currentBlock.isEmpty; const canOpenToolbox = currentBlock.tool.isDefault && currentBlock.isEmpty;
const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened; const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened;
const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened; const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
@ -206,15 +206,14 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keydown * @param {KeyboardEvent} event - keydown
*/ */
private enter(event: KeyboardEvent): void { private enter(event: KeyboardEvent): void {
const { BlockManager, Tools, UI } = this.Editor; const { BlockManager, UI } = this.Editor;
const currentBlock = BlockManager.currentBlock; const currentBlock = BlockManager.currentBlock;
const tool = Tools.available[currentBlock.name];
/** /**
* Don't handle Enter keydowns when Tool sets enableLineBreaks to true. * Don't handle Enter keydowns when Tool sets enableLineBreaks to true.
* Uses for Tools like <code> where line breaks should be handled by default behaviour. * Uses for Tools like <code> where line breaks should be handled by default behaviour.
*/ */
if (tool && tool[Tools.INTERNAL_SETTINGS.IS_ENABLED_LINE_BREAKS]) { if (currentBlock.tool.isLineBreaksEnabled) {
return; return;
} }
@ -253,7 +252,7 @@ export default class BlockEvents extends Module {
/** /**
* If new Block is empty * If new Block is empty
*/ */
if (this.Editor.Tools.isDefault(newCurrent.tool) && newCurrent.isEmpty) { if (newCurrent.tool.isDefault && newCurrent.isEmpty) {
/** /**
* Show Toolbar * Show Toolbar
*/ */
@ -276,7 +275,7 @@ export default class BlockEvents extends Module {
private backspace(event: KeyboardEvent): void { private backspace(event: KeyboardEvent): void {
const { BlockManager, BlockSelection, Caret } = this.Editor; const { BlockManager, BlockSelection, Caret } = this.Editor;
const currentBlock = BlockManager.currentBlock; 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 * 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 * 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; return;
} }

View file

@ -12,6 +12,7 @@ import $ from '../dom';
import * as _ from '../utils'; import * as _ from '../utils';
import Blocks from '../blocks'; import Blocks from '../blocks';
import { BlockToolConstructable, BlockToolData, PasteEvent } from '../../../types'; import { BlockToolConstructable, BlockToolData, PasteEvent } from '../../../types';
import BlockTool from '../tools/block';
/** /**
* @typedef {BlockManager} BlockManager * @typedef {BlockManager} BlockManager
@ -219,15 +220,13 @@ export default class BlockManager extends Module {
* *
* @returns {Block} * @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 readOnly = this.Editor.ReadOnly.isEnabled;
const settings = this.Editor.Tools.getToolSettings(tool); const tool = this.Editor.Tools.blockTools.get(name);
const Tool = this.Editor.Tools.available[tool] as BlockToolConstructable;
const block = new Block({ const block = new Block({
name: tool, name,
data, data,
Tool, tool,
settings,
api: this.Editor.API, api: this.Editor.API,
readOnly, readOnly,
}); });
@ -703,9 +702,7 @@ export default class BlockManager extends Module {
*/ */
public async destroy(): Promise<void> { public async destroy(): Promise<void> {
await Promise.all(this.blocks.map((block) => { await Promise.all(this.blocks.map((block) => {
if (_.isFunction(block.tool.destroy)) { return block.destroy();
return block.tool.destroy();
}
})); }));
} }

View file

@ -333,7 +333,7 @@ export default class Caret extends Module {
* If last block is empty and it is an defaultBlock, set to that. * If last block is empty and it is an defaultBlock, set to that.
* Otherwise, append new empty block and 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); this.setToBlock(lastBlock);
} else { } else {
const newBlock = this.Editor.BlockManager.insertAtEnd(); 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 * 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) * (https://github.com/codex-team/editor.js/issues/1414)
*/ */
if (Tools.isDefault(currentBlock.tool) || !isAtEnd) { if (currentBlock.tool.isDefault || !isAtEnd) {
return false; return false;
} }

View file

@ -2,14 +2,13 @@ import Module from '../__module';
import $ from '../dom'; import $ from '../dom';
import * as _ from '../utils'; import * as _ from '../utils';
import { import {
BlockTool, BlockAPI,
BlockToolConstructable,
PasteConfig,
PasteEvent, PasteEvent,
PasteEventDetail PasteEventDetail
} from '../../../types'; } from '../../../types';
import Block from '../block'; import Block from '../block';
import { SavedData } from '../../../types/data-formats'; import { SavedData } from '../../../types/data-formats';
import BlockTool from '../tools/block';
/** /**
* Tag substitute object. * Tag substitute object.
@ -18,9 +17,8 @@ interface TagSubstitute {
/** /**
* Name of related Tool * Name of related Tool
* *
* @type {string}
*/ */
tool: string; tool: BlockTool;
} }
/** /**
@ -29,24 +27,18 @@ interface TagSubstitute {
interface PatternSubstitute { interface PatternSubstitute {
/** /**
* Pattern`s key * Pattern`s key
*
* @type {string}
*/ */
key: string; key: string;
/** /**
* Pattern regexp * Pattern regexp
*
* @type {RegExp}
*/ */
pattern: RegExp; pattern: RegExp;
/** /**
* Name of related Tool * Name of related Tool
*
* @type {string}
*/ */
tool: string; tool: BlockTool;
} }
/** /**
@ -247,7 +239,7 @@ export default class Paste extends Module {
return; return;
} }
const isCurrentBlockDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool); const isCurrentBlockDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault;
const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty; const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;
dataToInsert.map( dataToInsert.map(
@ -279,23 +271,22 @@ export default class Paste extends Module {
private processTools(): void { private processTools(): void {
const tools = this.Editor.Tools.blockTools; 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 * Process paste config for each tool
*
* @param tool - BlockTool object
*/ */
private processTool = ([name, tool]: [string, BlockToolConstructable]): void => { private processTool = (tool: BlockTool): void => {
try { try {
const toolInstance = new this.Editor.Tools.blockTools[name]({ const toolInstance = tool.instance({}, {} as BlockAPI, false);
api: this.Editor.API.getMethodsForTool(name),
config: {},
data: {},
readOnly: false,
}) as BlockTool;
if (tool.pasteConfig === false) { if (tool.pasteConfig === false) {
this.exceptionList.push(name); this.exceptionList.push(tool.name);
return; return;
} }
@ -304,11 +295,9 @@ export default class Paste extends Module {
return; return;
} }
const toolPasteConfig = tool.pasteConfig || {}; this.getTagsConfig(tool);
this.getFilesConfig(tool);
this.getTagsConfig(name, toolPasteConfig); this.getPatternsConfig(tool);
this.getFilesConfig(name, toolPasteConfig);
this.getPatternsConfig(name, toolPasteConfig);
} catch (e) { } catch (e) {
_.log( _.log(
`Paste handling for «${name}» Tool hasn't been set up because of the error`, `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 * Get tags to substitute by Tool
* *
* @param {string} name - Tool name * @param tool - BlockTool object
* @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
*/ */
private getTagsConfig(name: string, toolPasteConfig: PasteConfig): void { private getTagsConfig(tool: BlockTool): void {
const tags = toolPasteConfig.tags || []; const tags = tool.pasteConfig.tags || [];
tags.forEach((tag) => { tags.forEach((tag) => {
if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) { if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
_.log( _.log(
`Paste handler for «${name}» Tool on «${tag}» tag is skipped ` + `Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` +
`because it is already used by «${this.toolsTags[tag].tool}» Tool.`, `because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`,
'warn' 'warn'
); );
@ -339,21 +327,20 @@ export default class Paste extends Module {
} }
this.toolsTags[tag.toUpperCase()] = { 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 * Get files` types and extensions to substitute by Tool
* *
* @param {string} name - Tool name * @param tool - BlockTool object
* @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
*/ */
private getFilesConfig(name: string, toolPasteConfig: PasteConfig): void { private getFilesConfig(tool: BlockTool): void {
const { files = {} } = toolPasteConfig; const { files = {} } = tool.pasteConfig;
let { extensions, mimeTypes } = files; let { extensions, mimeTypes } = files;
if (!extensions && !mimeTypes) { if (!extensions && !mimeTypes) {
@ -361,19 +348,19 @@ export default class Paste extends Module {
} }
if (extensions && !Array.isArray(extensions)) { 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 = []; extensions = [];
} }
if (mimeTypes && !Array.isArray(mimeTypes)) { 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 = []; mimeTypes = [];
} }
if (mimeTypes) { if (mimeTypes) {
mimeTypes = mimeTypes.filter((type) => { mimeTypes = mimeTypes.filter((type) => {
if (!_.isValidMimeType(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; return false;
} }
@ -382,7 +369,7 @@ export default class Paste extends Module {
}); });
} }
this.toolsFiles[name] = { this.toolsFiles[tool.name] = {
extensions: extensions || [], extensions: extensions || [],
mimeTypes: mimeTypes || [], mimeTypes: mimeTypes || [],
}; };
@ -391,15 +378,14 @@ export default class Paste extends Module {
/** /**
* Get RegExp patterns to substitute by Tool * Get RegExp patterns to substitute by Tool
* *
* @param {string} name - Tool name * @param tool - BlockTool object
* @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
*/ */
private getPatternsConfig(name: string, toolPasteConfig: PasteConfig): void { private getPatternsConfig(tool: BlockTool): void {
if (!toolPasteConfig.patterns || _.isEmpty(toolPasteConfig.patterns)) { if (!tool.pasteConfig.patterns || _.isEmpty(tool.pasteConfig.patterns)) {
return; 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 */ /** Still need to validate pattern as it provided by user */
if (!(pattern instanceof RegExp)) { if (!(pattern instanceof RegExp)) {
_.log( _.log(
@ -411,7 +397,7 @@ export default class Paste extends Module {
this.toolsPatterns.push({ this.toolsPatterns.push({
key, key,
pattern, pattern,
tool: name, tool,
}); });
}); });
} }
@ -462,9 +448,9 @@ export default class Paste extends Module {
* @param {FileList} items - pasted or dropped items * @param {FileList} items - pasted or dropped items
*/ */
private async processFiles(items: FileList): Promise<void> { private async processFiles(items: FileList): Promise<void> {
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( dataToInsert = await Promise.all(
Array Array
@ -473,7 +459,7 @@ export default class Paste extends Module {
); );
dataToInsert = dataToInsert.filter((data) => !!data); dataToInsert = dataToInsert.filter((data) => !!data);
const isCurrentBlockDefault = Tools.isDefault(BlockManager.currentBlock.tool); const isCurrentBlockDefault = BlockManager.currentBlock.tool.isDefault;
const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty; const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;
dataToInsert.forEach( dataToInsert.forEach(
@ -530,7 +516,6 @@ export default class Paste extends Module {
*/ */
private processHTML(innerHTML: string): PasteData[] { private processHTML(innerHTML: string): PasteData[] {
const { Tools, Sanitizer } = this.Editor; const { Tools, Sanitizer } = this.Editor;
const initialTool = this.config.defaultBlock;
const wrapper = $.make('DIV'); const wrapper = $.make('DIV');
wrapper.innerHTML = innerHTML; wrapper.innerHTML = innerHTML;
@ -539,7 +524,7 @@ export default class Paste extends Module {
return nodes return nodes
.map((node) => { .map((node) => {
let content, tool = initialTool, isBlock = false; let content, tool = Tools.defaultTool, isBlock = false;
switch (node.nodeType) { switch (node.nodeType) {
/** If node is a document fragment, use temp wrapper to get innerHTML */ /** If node is a document fragment, use temp wrapper to get innerHTML */
@ -559,7 +544,7 @@ export default class Paste extends Module {
break; break;
} }
const { tags } = Tools.blockTools[tool].pasteConfig as PasteConfig; const { tags } = tool.pasteConfig;
const toolTags = tags.reduce((result, tag) => { const toolTags = tags.reduce((result, tag) => {
result[tag.toLowerCase()] = {}; result[tag.toLowerCase()] = {};
@ -577,7 +562,7 @@ export default class Paste extends Module {
return { return {
content, content,
isBlock, isBlock,
tool, tool: tool.name,
event, event,
}; };
}) })
@ -627,7 +612,7 @@ export default class Paste extends Module {
* @param {PasteData} dataToInsert - data of Block to inseret * @param {PasteData} dataToInsert - data of Block to inseret
*/ */
private async processSingleBlock(dataToInsert: PasteData): Promise<void> { private async processSingleBlock(dataToInsert: PasteData): Promise<void> {
const { Caret, BlockManager, Tools } = this.Editor; const { Caret, BlockManager } = this.Editor;
const { currentBlock } = BlockManager; const { currentBlock } = BlockManager;
/** /**
@ -638,7 +623,7 @@ export default class Paste extends Module {
dataToInsert.tool !== currentBlock.name || dataToInsert.tool !== currentBlock.name ||
!$.containsOnlyInlineElements(dataToInsert.content.innerHTML) !$.containsOnlyInlineElements(dataToInsert.content.innerHTML)
) { ) {
this.insertBlock(dataToInsert, currentBlock && Tools.isDefault(currentBlock.tool) && currentBlock.isEmpty); this.insertBlock(dataToInsert, currentBlock?.tool.isDefault && currentBlock.isEmpty);
return; return;
} }
@ -655,17 +640,17 @@ export default class Paste extends Module {
* @param {PasteData} dataToInsert - data of Block to insert * @param {PasteData} dataToInsert - data of Block to insert
*/ */
private async processInlinePaste(dataToInsert: PasteData): Promise<void> { private async processInlinePaste(dataToInsert: PasteData): Promise<void> {
const { BlockManager, Caret, Sanitizer, Tools } = this.Editor; const { BlockManager, Caret, Sanitizer } = this.Editor;
const { content } = dataToInsert; 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) { if (currentBlockIsDefault && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {
const blockData = await this.processPattern(content.textContent); const blockData = await this.processPattern(content.textContent);
if (blockData) { if (blockData) {
const needToReplaceCurrentBlock = BlockManager.currentBlock && const needToReplaceCurrentBlock = BlockManager.currentBlock &&
Tools.isDefault(BlockManager.currentBlock.tool) && BlockManager.currentBlock.tool.isDefault &&
BlockManager.currentBlock.isEmpty; BlockManager.currentBlock.isEmpty;
const insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock); 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 there is no pattern substitute - insert string as it is */
if (BlockManager.currentBlock && BlockManager.currentBlock.currentInput) { if (BlockManager.currentBlock && BlockManager.currentBlock.currentInput) {
const currentToolSanitizeConfig = Sanitizer.getInlineToolsConfig(BlockManager.currentBlock.name); const currentToolSanitizeConfig = Sanitizer.getInlineToolsConfig(BlockManager.currentBlock.tool);
document.execCommand( document.execCommand(
'insertHTML', 'insertHTML',
@ -719,7 +704,7 @@ export default class Paste extends Module {
return { return {
event, event,
tool: pattern.tool, tool: pattern.tool.name,
}; };
} }
@ -755,15 +740,15 @@ export default class Paste extends Module {
* *
* @returns {void} * @returns {void}
*/ */
private insertEditorJSData(blocks: Array<Pick<SavedData, 'data' | 'tool'>>): void { private insertEditorJSData(blocks: Pick<SavedData, 'data' | 'tool'>[]): void {
const { BlockManager, Caret, Sanitizer, Tools } = this.Editor; const { BlockManager, Caret, Sanitizer } = this.Editor;
const sanitizedBlocks = Sanitizer.sanitizeBlocks(blocks); const sanitizedBlocks = Sanitizer.sanitizeBlocks(blocks);
sanitizedBlocks.forEach(({ tool, data }, i) => { sanitizedBlocks.forEach(({ tool, data }, i) => {
let needToReplaceCurrentBlock = false; let needToReplaceCurrentBlock = false;
if (i === 0) { 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; needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;
} }
@ -792,8 +777,8 @@ export default class Paste extends Module {
const element = node as HTMLElement; const element = node as HTMLElement;
const { tool = '' } = this.toolsTags[element.tagName] || {}; const { tool } = this.toolsTags[element.tagName] || {};
const toolTags = this.tagsByTool[tool] || []; const toolTags = this.tagsByTool[tool?.name] || [];
const isSubstitutable = tags.includes(element.tagName); const isSubstitutable = tags.includes(element.tagName);
const isBlockElement = $.blockElements.includes(element.tagName.toLowerCase()); const isBlockElement = $.blockElements.includes(element.tagName.toLowerCase());

View file

@ -40,11 +40,13 @@ export default class ReadOnly extends Module {
const { blockTools } = Tools; const { blockTools } = Tools;
const toolsDontSupportReadOnly: string[] = []; const toolsDontSupportReadOnly: string[] = [];
Object.entries(blockTools).forEach(([name, tool]) => { Array
if (!Tools.isReadOnlySupported(tool)) { .from(blockTools.entries())
toolsDontSupportReadOnly.push(name); .forEach(([name, tool]) => {
} if (!tool.isReadOnlySupported) {
}); toolsDontSupportReadOnly.push(name);
}
});
this.toolsDontSupportReadOnly = toolsDontSupportReadOnly; this.toolsDontSupportReadOnly = toolsDontSupportReadOnly;

View file

@ -1,6 +1,7 @@
import Module from '../__module'; import Module from '../__module';
import * as _ from '../utils'; import * as _ from '../utils';
import { BlockToolConstructable, OutputBlockData } from '../../../types'; import { OutputBlockData } from '../../../types';
import BlockTool from '../tools/block';
/** /**
* Editor.js Renderer Module * Editor.js Renderer Module
@ -66,7 +67,7 @@ export default class Renderer extends Module {
const tool = item.type; const tool = item.type;
const data = item.data; const data = item.data;
if (tool in Tools.available) { if (Tools.available.has(tool)) {
try { try {
BlockManager.insert({ BlockManager.insert({
tool, tool,
@ -86,11 +87,10 @@ export default class Renderer extends Module {
title: tool, title: tool,
}; };
if (tool in Tools.unavailable) { if (Tools.unavailable.has(tool)) {
const toolToolboxSettings = (Tools.unavailable[tool] as BlockToolConstructable).toolbox; const toolboxSettings = (Tools.unavailable.get(tool) as BlockTool).toolbox;
const userToolboxSettings = Tools.getToolSettings(tool).toolbox;
stubData.title = toolToolboxSettings.title || (userToolboxSettings && userToolboxSettings.title) || stubData.title; stubData.title = toolboxSettings?.title || stubData.title;
} }
const stub = BlockManager.insert({ const stub = BlockManager.insert({

View file

@ -36,8 +36,10 @@ import * as _ from '../utils';
*/ */
import HTMLJanitor from 'html-janitor'; import HTMLJanitor from 'html-janitor';
import { BlockToolData, InlineToolConstructable, SanitizerConfig } from '../../../types'; import { BlockToolData, SanitizerConfig } from '../../../types';
import { SavedData } from '../../../types/data-formats'; 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 * @param {Array<{tool, data: BlockToolData}>} blocksData - blocks' data to sanitize
*/ */
public sanitizeBlocks( public sanitizeBlocks(
blocksData: Array<Pick<SavedData, 'data' | 'tool'>> blocksData: Pick<SavedData, 'data' | 'tool'>[]
): Array<Pick<SavedData, 'data' | 'tool'>> { ): Pick<SavedData, 'data' | 'tool'>[] {
return blocksData.map((block) => { return blocksData.map((block) => {
const toolConfig = this.composeToolConfig(block.tool); const toolConfig = this.composeToolConfig(block.tool);
@ -150,18 +152,17 @@ export default class Sanitizer extends Module {
return this.configCache[toolName]; return this.configCache[toolName];
} }
const sanitizeGetter = this.Editor.Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG; const tool = this.Editor.Tools.available.get(toolName);
const toolClass = this.Editor.Tools.available[toolName]; const baseConfig = this.getInlineToolsConfig(tool as BlockTool);
const baseConfig = this.getInlineToolsConfig(toolName);
/** /**
* If Tools doesn't provide sanitizer config or it is empty * 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; return baseConfig;
} }
const toolRules = toolClass.sanitize; const toolRules = tool.sanitizeConfig;
const toolConfig = {} as SanitizerConfig; 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, * When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools,
* otherwise get only enabled * 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 { Tools } = this.Editor;
const toolsConfig = Tools.getToolSettings(name); const enableInlineTools = tool.enabledInlineTools || [];
const enableInlineTools = toolsConfig.inlineToolbar || [];
let config = {} as SanitizerConfig; let config = {} as SanitizerConfig;
@ -207,7 +207,7 @@ export default class Sanitizer extends Module {
(enableInlineTools as string[]).map((inlineToolName) => { (enableInlineTools as string[]).map((inlineToolName) => {
config = Object.assign( config = Object.assign(
config, config,
Tools.inline[inlineToolName][Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG] Tools.inlineTools.get(inlineToolName).sanitizeConfig
) as SanitizerConfig; ) as SanitizerConfig;
}); });
} }
@ -233,9 +233,9 @@ export default class Sanitizer extends Module {
const config: SanitizerConfig = {} as SanitizerConfig; const config: SanitizerConfig = {} as SanitizerConfig;
Object.entries(Tools.inline) Object.entries(Tools.inlineTools)
.forEach(([, inlineTool]: [string, InlineToolConstructable]) => { .forEach(([, inlineTool]: [string, InlineTool]) => {
Object.assign(config, inlineTool[Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]); Object.assign(config, inlineTool.sanitizeConfig);
}); });
this.inlineToolsConfigCache = config; this.inlineToolsConfigCache = config;
@ -249,7 +249,7 @@ export default class Sanitizer extends Module {
* @param {Array} array - [1, 2, {}, []] * @param {Array} array - [1, 2, {}, []]
* @param {SanitizerConfig} ruleForItem - sanitizer config for array * @param {SanitizerConfig} ruleForItem - sanitizer config for array
*/ */
private cleanArray(array: Array<object | string>, ruleForItem: SanitizerConfig): Array<object | string> { private cleanArray(array: (object | string)[], ruleForItem: SanitizerConfig): (object | string)[] {
return array.map((arrayItem) => this.deepSanitize(arrayItem, ruleForItem)); return array.map((arrayItem) => this.deepSanitize(arrayItem, ruleForItem));
} }

View file

@ -229,8 +229,10 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
* Add Tool's settings * Add Tool's settings
*/ */
private addToolSettings(): void { private addToolSettings(): void {
if (_.isFunction(this.Editor.BlockManager.currentBlock.tool.renderSettings)) { const settingsElement = this.Editor.BlockManager.currentBlock.renderSettings();
$.append(this.nodes.toolSettings, this.Editor.BlockManager.currentBlock.tool.renderSettings());
if (settingsElement) {
$.append(this.nodes.toolSettings, settingsElement);
} }
} }

View file

@ -182,10 +182,9 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
* *
* @type {BlockToolConstructable} * @type {BlockToolConstructable}
*/ */
const currentBlockClass = this.Editor.BlockManager.currentBlock.class; const currentBlockTool = this.Editor.BlockManager.currentBlock.tool;
const currentBlockName = this.Editor.BlockManager.currentBlock.name; const currentBlockName = this.Editor.BlockManager.currentBlock.name;
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData; const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
const { INTERNAL_SETTINGS } = this.Editor.Tools;
const blockData = savedBlock.data; const blockData = savedBlock.data;
/** /**
@ -201,7 +200,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
* *
* @type {BlockToolConstructable} * @type {BlockToolConstructable}
*/ */
const replacingTool = this.Editor.Tools.toolsClasses[replacingToolName] as BlockToolConstructable; const replacingTool = this.Editor.Tools.blockTools.get(replacingToolName);
/** /**
* Export property can be: * Export property can be:
@ -211,7 +210,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
* In both cases returning value must be a string * In both cases returning value must be a string
*/ */
let exportData = ''; let exportData = '';
const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export; const exportProp = currentBlockTool.conversionConfig.export;
if (_.isFunction(exportProp)) { if (_.isFunction(exportProp)) {
exportData = exportProp(blockData); exportData = exportProp(blockData);
@ -229,7 +228,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
*/ */
const cleaned: string = this.Editor.Sanitizer.clean( const cleaned: string = this.Editor.Sanitizer.clean(
exportData, exportData,
replacingTool.sanitize replacingTool.sanitizeConfig
); );
/** /**
@ -238,7 +237,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
* string the name of data field to import * string the name of data field to import
*/ */
let newBlockData = {}; let newBlockData = {};
const importProp = replacingTool[INTERNAL_SETTINGS.CONVERSION_CONFIG].import; const importProp = replacingTool.conversionConfig.import;
if (_.isFunction(importProp)) { if (_.isFunction(importProp)) {
newBlockData = importProp(cleaned); newBlockData = importProp(cleaned);
@ -272,37 +271,28 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
private addTools(): void { private addTools(): void {
const tools = this.Editor.Tools.blockTools; const tools = this.Editor.Tools.blockTools;
for (const toolName in tools) { Array
if (!Object.prototype.hasOwnProperty.call(tools, toolName)) { .from(tools.entries())
continue; .forEach(([name, tool]) => {
} const toolboxSettings = tool.toolbox;
const conversionConfig = tool.conversionConfig;
const internalSettings = this.Editor.Tools.INTERNAL_SETTINGS; /**
const toolClass = tools[toolName] as BlockToolConstructable; * Skip tools that don't pass 'toolbox' property
const toolToolboxSettings = toolClass[internalSettings.TOOLBOX]; */
const conversionConfig = toolClass[internalSettings.CONVERSION_CONFIG]; 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; this.addTool(name, toolboxSettings.icon, toolboxSettings.title);
});
/**
* 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);
}
} }
/** /**

View file

@ -2,12 +2,15 @@ import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import SelectionUtils from '../../selection'; import SelectionUtils from '../../selection';
import * as _ from '../../utils'; import * as _ from '../../utils';
import { InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings } from '../../../../types'; import { InlineTool as IInlineTool } from '../../../../types';
import Flipper from '../../flipper'; import Flipper from '../../flipper';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal'; import { I18nInternalNS } from '../../i18n/namespace-internal';
import Shortcuts from '../../utils/shortcuts'; 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 * Inline Toolbar elements
@ -66,9 +69,11 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
private readonly toolbarVerticalMargin: number = 5; private readonly toolbarVerticalMargin: number = 5;
/** /**
* TODO: Get rid of this
*
* Currently visible tools instances * Currently visible tools instances
*/ */
private toolsInstances: Map<string, InlineTool>; private toolsInstances: Map<string, IInlineTool>;
/** /**
* Buttons List * Buttons List
@ -89,38 +94,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
*/ */
private flipper: Flipper = null; 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 * Toggles read-only mode
* *
@ -310,16 +283,14 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
/** /**
* Returns inline toolbar settings for a particular tool * 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 * @returns {string[] | boolean} array of ordered tool names or false
*/ */
private getInlineToolbarSettings(toolName): string[] | boolean { private getInlineToolbarSettings(tool: BlockTool): string[] | boolean {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
/** /**
* InlineToolbar property of a particular tool * 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. * 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<InlineToolbarNodes> {
* If common settings is 'true' or not specified (will be set as true at core.ts), get the default order * If common settings is 'true' or not specified (will be set as true at core.ts), get the default order
*/ */
if (commonInlineToolbarSettings === true) { if (commonInlineToolbarSettings === true) {
const defaultToolsOrder: string[] = Object.entries(this.Editor.Tools.available) return Array.from(this.Editor.Tools.inlineTools.keys());
.filter(([name, tool]) => {
return tool[this.Editor.Tools.INTERNAL_SETTINGS.IS_INLINE];
})
.map(([name, tool]) => {
return name;
});
return defaultToolsOrder;
} }
return false; return false;
@ -492,7 +455,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
/** /**
* getInlineToolbarSettings could return an string[] (order of tools) or false (Inline Toolbar disabled). * 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; return inlineToolbarSettings !== false;
} }
@ -548,13 +511,14 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* Changes Conversion Dropdown content for current block's Tool * Changes Conversion Dropdown content for current block's Tool
*/ */
private setConversionTogglerContent(): void { private setConversionTogglerContent(): void {
const { BlockManager, Tools } = this.Editor; const { BlockManager } = this.Editor;
const toolName = BlockManager.currentBlock.name; const { currentBlock } = BlockManager;
const toolName = currentBlock.name;
/** /**
* If tool does not provide 'export' rule, hide conversion dropdown * 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; const exportRuleDefined = conversionConfig && conversionConfig.export;
this.nodes.conversionToggler.hidden = !exportRuleDefined; this.nodes.conversionToggler.hidden = !exportRuleDefined;
@ -563,14 +527,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
/** /**
* Get icon or title for dropdown * Get icon or title for dropdown
*/ */
const toolSettings = Tools.getToolSettings(toolName); const toolboxSettings = currentBlock.tool.toolbox || {};
const toolboxSettings = Tools.available[toolName][Tools.INTERNAL_SETTINGS.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {};
this.nodes.conversionTogglerContent.innerHTML = this.nodes.conversionTogglerContent.innerHTML =
userToolboxSettings.icon ||
toolboxSettings.icon || toolboxSettings.icon ||
userToolboxSettings.title ||
toolboxSettings.title || toolboxSettings.title ||
_.capitalize(toolName); _.capitalize(toolName);
} }
@ -610,14 +570,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* For this moment, inlineToolbarOrder could not be 'false' * For this moment, inlineToolbarOrder could not be 'false'
* because this method will be called only if the Inline Toolbar is enabled * 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) => { inlineToolbarOrder.forEach((toolName) => {
const toolSettings = this.Editor.Tools.getToolSettings(toolName); const tool = this.Editor.Tools.inlineTools.get(toolName);
const tool = this.Editor.Tools.constructInline(this.Editor.Tools.inline[toolName], toolName, toolSettings);
this.addTool(toolName, tool); this.addTool(tool);
tool.checkState(SelectionUtils.get());
}); });
/** /**
@ -629,43 +587,42 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
/** /**
* Add tool button and activate clicks * Add tool button and activate clicks
* *
* @param {string} toolName - name of Tool to add * @param {InlineTool} tool - InlineTool object
* @param {InlineTool} tool - Tool class instance
*/ */
private addTool(toolName: string, tool: InlineTool): void { private addTool(tool: InlineTool): void {
const { const {
Tools,
Tooltip, Tooltip,
} = this.Editor; } = this.Editor;
const button = tool.render(); const instance = tool.instance();
const button = instance.render();
if (!button) { 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; return;
} }
button.dataset.tool = toolName; button.dataset.tool = tool.name;
this.nodes.buttons.appendChild(button); this.nodes.buttons.appendChild(button);
this.toolsInstances.set(toolName, tool); this.toolsInstances.set(tool.name, instance);
if (_.isFunction(tool.renderActions)) { if (_.isFunction(instance.renderActions)) {
const actions = tool.renderActions(); const actions = instance.renderActions();
this.nodes.actions.appendChild(actions); this.nodes.actions.appendChild(actions);
} }
this.listeners.on(button, 'click', (event) => { this.listeners.on(button, 'click', (event) => {
this.toolClicked(tool); this.toolClicked(instance);
event.preventDefault(); event.preventDefault();
}); });
const shortcut = this.getToolShortcut(toolName); const shortcut = this.getToolShortcut(tool.name);
if (shortcut) { if (shortcut) {
try { try {
this.enableShortcuts(tool, shortcut); this.enableShortcuts(instance, shortcut);
} catch (e) {} } catch (e) {}
} }
@ -675,7 +632,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
const tooltipContent = $.make('div'); const tooltipContent = $.make('div');
const toolTitle = I18n.t( const toolTitle = I18n.t(
I18nInternalNS.toolNames, I18nInternalNS.toolNames,
Tools.toolsClasses[toolName][Tools.INTERNAL_SETTINGS.TITLE] || _.capitalize(toolName) tool.title || _.capitalize(tool.name)
); );
tooltipContent.appendChild($.text(toolTitle)); tooltipContent.appendChild($.text(toolTitle));
@ -690,6 +647,8 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
placement: 'top', placement: 'top',
hidingDelay: 100, hidingDelay: 100,
}); });
instance.checkState(SelectionUtils.get());
} }
/** /**
@ -704,21 +663,20 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* Enable shortcuts * Enable shortcuts
* Ignore tool that doesn't have shortcut or empty string * Ignore tool that doesn't have shortcut or empty string
*/ */
const toolSettings = Tools.getToolSettings(toolName); const tool = Tools.inlineTools.get(toolName);
const tool = this.toolsInstances.get(toolName);
/** /**
* 1) For internal tools, check public getter 'shortcut' * 1) For internal tools, check public getter 'shortcut'
* 2) For external tools, check tool's settings * 2) For external tools, check tool's settings
* 3) If shortcut is not set in settings, check Tool's public property * 3) If shortcut is not set in settings, check Tool's public property
*/ */
if (Object.keys(this.internalTools).includes(toolName)) { const internalTools = Tools.getInternal(ToolType.Inline);
return this.inlineTools[toolName][Tools.INTERNAL_SETTINGS.SHORTCUT];
} else if (toolSettings && toolSettings[Tools.USER_SETTINGS.SHORTCUT]) { if (Array.from(internalTools.keys()).includes(toolName)) {
return toolSettings[Tools.USER_SETTINGS.SHORTCUT]; return this.inlineTools[toolName][CommonInternalSettings.Shortcut];
} else if (tool.shortcut) {
return tool.shortcut;
} }
return tool.shortcut;
} }
/** /**
@ -727,7 +685,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* @param {InlineTool} tool - Tool instance * @param {InlineTool} tool - Tool instance
* @param {string} shortcut - shortcut according to the ShortcutData Module format * @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({ Shortcuts.add({
name: shortcut, name: shortcut,
handler: (event) => { handler: (event) => {
@ -747,9 +705,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
*/ */
// if (SelectionUtils.isCollapsed) return; // if (SelectionUtils.isCollapsed) return;
const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name); if (!currentBlock.tool.enabledInlineTools) {
if (!toolSettings || !toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS]) {
return; return;
} }
@ -765,7 +721,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* *
* @param {InlineTool} tool - Tool's instance * @param {InlineTool} tool - Tool's instance
*/ */
private toolClicked(tool: InlineTool): void { private toolClicked(tool: IInlineTool): void {
const range = SelectionUtils.range; const range = SelectionUtils.range;
tool.surround(range); tool.surround(range);
@ -785,16 +741,14 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* Get inline tools tools * Get inline tools tools
* Tools that has isInline is true * Tools that has isInline is true
*/ */
private get inlineTools(): { [name: string]: InlineTool } { private get inlineTools(): { [name: string]: IInlineTool } {
const result = {}; const result = {};
for (const tool in this.Editor.Tools.inline) { Array
if (Object.prototype.hasOwnProperty.call(this.Editor.Tools.inline, tool)) { .from(this.Editor.Tools.inlineTools.entries())
const toolSettings = this.Editor.Tools.getToolSettings(tool); .forEach(([name, tool]) => {
result[name] = tool.instance();
result[tool] = this.Editor.Tools.constructInline(this.Editor.Tools.inline[tool], tool, toolSettings); });
}
}
return result; return result;
} }

View file

@ -1,12 +1,13 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import * as _ from '../../utils'; import * as _ from '../../utils';
import { BlockToolConstructable, ToolConstructable } from '../../../../types'; import { BlockToolConstructable } from '../../../../types';
import Flipper from '../../flipper'; import Flipper from '../../flipper';
import { BlockToolAPI } from '../../block'; import { BlockToolAPI } from '../../block';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal'; import { I18nInternalNS } from '../../i18n/namespace-internal';
import Shortcuts from '../../utils/shortcuts'; import Shortcuts from '../../utils/shortcuts';
import BlockTool from '../../tools/block';
/** /**
* HTMLElements used for Toolbox UI * HTMLElements used for Toolbox UI
@ -116,9 +117,7 @@ export default class Toolbox extends Module<ToolboxNodes> {
* @param {string} toolName - button to activate * @param {string} toolName - button to activate
*/ */
public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void { public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void {
const tool = this.Editor.Tools.toolsClasses[toolName] as BlockToolConstructable; this.insertNewBlock(toolName);
this.insertNewBlock(tool, toolName);
} }
/** /**
@ -162,36 +161,30 @@ export default class Toolbox extends Module<ToolboxNodes> {
* Iterates available tools and appends them to the Toolbox * Iterates available tools and appends them to the Toolbox
*/ */
private addTools(): void { private addTools(): void {
const tools = this.Editor.Tools.available; const tools = this.Editor.Tools.blockTools;
for (const toolName in tools) { Array
if (Object.prototype.hasOwnProperty.call(tools, toolName)) { .from(tools.values())
this.addTool(toolName, tools[toolName] as BlockToolConstructable); .forEach((tool) => this.addTool(tool));
}
}
} }
/** /**
* Append Tool to the Toolbox * Append Tool to the Toolbox
* *
* @param {string} toolName - tool name * @param {BlockToolConstructable} tool - BlockTool object
* @param {BlockToolConstructable} tool - tool class
*/ */
private addTool(toolName: string, tool: BlockToolConstructable): void { private addTool(tool: BlockTool): void {
const internalSettings = this.Editor.Tools.INTERNAL_SETTINGS; const toolToolboxSettings = tool.toolbox;
const userSettings = this.Editor.Tools.USER_SETTINGS;
const toolToolboxSettings = tool[internalSettings.TOOLBOX];
/** /**
* Skip tools that don't pass 'toolbox' property * Skip tools that don't pass 'toolbox' property
*/ */
if (_.isEmpty(toolToolboxSettings)) { if (!toolToolboxSettings) {
return; return;
} }
if (toolToolboxSettings && !toolToolboxSettings.icon) { 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; return;
} }
@ -204,19 +197,10 @@ export default class Toolbox extends Module<ToolboxNodes> {
// return; // 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 ]); const button = $.make('li', [ this.CSS.toolboxButton ]);
button.dataset.tool = toolName; button.dataset.tool = tool.name;
button.innerHTML = (userToolboxSettings && userToolboxSettings.icon) || toolToolboxSettings.icon; button.innerHTML = toolToolboxSettings.icon;
$.append(this.nodes.toolbox, button); $.append(this.nodes.toolbox, button);
@ -227,61 +211,40 @@ export default class Toolbox extends Module<ToolboxNodes> {
* Add click listener * Add click listener
*/ */
this.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => { this.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.toolButtonActivate(event, toolName); this.toolButtonActivate(event, tool.name);
}); });
/** /**
* Add listeners to show/hide toolbox tooltip * Add listeners to show/hide toolbox tooltip
*/ */
const tooltipContent = this.drawTooltip(toolName); const tooltipContent = this.drawTooltip(tool);
this.Editor.Tooltip.onHover(button, tooltipContent, { this.Editor.Tooltip.onHover(button, tooltipContent, {
placement: 'bottom', placement: 'bottom',
hidingDelay: 200, hidingDelay: 200,
}); });
const shortcut = this.getToolShortcut(toolName, tool); const shortcut = tool.shortcut;
if (shortcut) { if (shortcut) {
this.enableShortcut(tool, toolName, shortcut); this.enableShortcut(tool.name, shortcut);
} }
/** Increment Tools count */ /** Increment Tools count */
this.displayedToolsCount++; 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 * Draw tooltip for toolbox tools
* *
* @param {string} toolName - toolbox tool name * @param tool - BlockTool object
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
private drawTooltip(toolName: string): HTMLElement { private drawTooltip(tool: BlockTool): HTMLElement {
const tool = this.Editor.Tools.available[toolName]; const toolboxSettings = tool.toolbox || {};
const toolSettings = this.Editor.Tools.getToolSettings(toolName); const name = I18n.t(I18nInternalNS.toolNames, toolboxSettings.title || tool.name);
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);
let shortcut = this.getToolShortcut(toolName, tool); let shortcut = tool.shortcut;
const tooltip = $.make('div', this.CSS.buttonTooltip); const tooltip = $.make('div', this.CSS.buttonTooltip);
const hint = document.createTextNode(_.capitalize(name)); const hint = document.createTextNode(_.capitalize(name));
@ -302,16 +265,15 @@ export default class Toolbox extends Module<ToolboxNodes> {
/** /**
* Enable shortcut Block Tool implemented shortcut * Enable shortcut Block Tool implemented shortcut
* *
* @param {BlockToolConstructable} tool - Tool class
* @param {string} toolName - Tool name * @param {string} toolName - Tool name
* @param {string} shortcut - shortcut according to the ShortcutData Module format * @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({ Shortcuts.add({
name: shortcut, name: shortcut,
handler: (event: KeyboardEvent) => { handler: (event: KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
this.insertNewBlock(tool, toolName); this.insertNewBlock(toolName);
}, },
on: this.Editor.UI.nodes.redactor, on: this.Editor.UI.nodes.redactor,
}); });
@ -322,17 +284,17 @@ export default class Toolbox extends Module<ToolboxNodes> {
* Fired when the Read-Only mode is activated * Fired when the Read-Only mode is activated
*/ */
private removeAllShortcuts(): void { private removeAllShortcuts(): void {
const tools = this.Editor.Tools.available; const tools = this.Editor.Tools.blockTools;
for (const toolName in tools) { Array
if (Object.prototype.hasOwnProperty.call(tools, toolName)) { .from(tools.values())
const shortcut = this.getToolShortcut(toolName, tools[toolName]); .forEach((tool) => {
const shortcut = tool.shortcut;
if (shortcut) { if (shortcut) {
Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut); Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut);
} }
} });
}
} }
/** /**
@ -351,10 +313,9 @@ export default class Toolbox extends Module<ToolboxNodes> {
* Inserts new block * Inserts new block
* Can be called when button clicked on Toolbox or by ShortcutData * Can be called when button clicked on Toolbox or by ShortcutData
* *
* @param {BlockToolConstructable} tool - Tool Class
* @param {string} toolName - Tool name * @param {string} toolName - Tool name
*/ */
private insertNewBlock(tool: BlockToolConstructable, toolName: string): void { private insertNewBlock(toolName: string): void {
const { BlockManager, Caret } = this.Editor; const { BlockManager, Caret } = this.Editor;
const { currentBlock } = BlockManager; const { currentBlock } = BlockManager;

View file

@ -1,21 +1,21 @@
import Paragraph from '../tools/paragraph/dist/bundle'; import Paragraph from '../../tools/paragraph/dist/bundle';
import Module from '../__module'; import Module from '../__module';
import * as _ from '../utils'; import * as _ from '../utils';
import { import {
BlockToolConstructable,
EditorConfig, EditorConfig,
InlineTool, Tool,
InlineToolConstructable, Tool,
ToolConfig,
ToolConstructable, ToolConstructable,
ToolSettings ToolSettings
} from '../../../types'; } from '../../../types';
import BoldInlineTool from '../inline-tools/inline-tool-bold'; import BoldInlineTool from '../inline-tools/inline-tool-bold';
import ItalicInlineTool from '../inline-tools/inline-tool-italic'; import ItalicInlineTool from '../inline-tools/inline-tool-italic';
import LinkInlineTool from '../inline-tools/inline-tool-link'; import LinkInlineTool from '../inline-tools/inline-tool-link';
import Stub from '../tools/stub'; import Stub from '../../tools/stub';
import { ModuleConfig } from '../../types-internal/module-config'; import ToolsFactory from '../tools/factory';
import EventsDispatcher from '../utils/events'; 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 * @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 * Creates Instances from Plugins and binds external config to the instances
*/ */
type ToolClass = BlockTool | InlineTool | BlockTune;
/** /**
* Class properties: * Class properties:
* *
@ -47,7 +49,7 @@ export default class Tools extends Module {
* *
* @returns {object<Tool>} * @returns {object<Tool>}
*/ */
public get available(): { [name: string]: ToolConstructable } { public get available(): Map<string, ToolClass> {
return this.toolsAvailable; return this.toolsAvailable;
} }
@ -56,7 +58,7 @@ export default class Tools extends Module {
* *
* @returns {Tool[]} * @returns {Tool[]}
*/ */
public get unavailable(): { [name: string]: ToolConstructable } { public get unavailable(): Map<string, ToolClass> {
return this.toolsUnavailable; return this.toolsUnavailable;
} }
@ -65,48 +67,40 @@ export default class Tools extends Module {
* *
* @returns {object} - object of Inline Tool's classes * @returns {object} - object of Inline Tool's classes
*/ */
public get inline(): { [name: string]: InlineToolConstructable } { public get inlineTools(): Map<string, InlineTool> {
if (this._inlineTools) { if (this._inlineTools) {
return this._inlineTools; return this._inlineTools;
} }
const tools = Object.entries(this.available).filter(([name, tool]) => { const tools = Array
if (!tool[this.INTERNAL_SETTINGS.IS_INLINE]) { .from(this.available.entries())
return false; .filter(([name, tool]: [string, BaseTool<any>]) => {
} if (tool.type !== ToolType.Inline) {
return false;
}
/**
* Some Tools validation
*/
const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];
const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !tool.instance()[method]);
/** if (notImplementedMethods.length) {
* Some Tools validation _.log(
*/ `Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,
const inlineToolRequiredMethods = ['render', 'surround', 'checkState']; 'warn',
const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !this.constructInline(tool, name)[method]); notImplementedMethods
);
if (notImplementedMethods.length) { return false;
_.log( }
`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,
'warn',
notImplementedMethods
);
return false; return true;
} });
return true;
});
/**
* collected inline tools with key of tool name
*/
const result = {};
tools.forEach(([name, tool]) => {
result[name] = tool;
});
/** /**
* Cache prepared Tools * Cache prepared Tools
*/ */
this._inlineTools = result; this._inlineTools = new Map(tools) as Map<string, InlineTool>;
return this._inlineTools; return this._inlineTools;
} }
@ -114,79 +108,43 @@ export default class Tools extends Module {
/** /**
* Return editor block tools * Return editor block tools
*/ */
public get blockTools(): { [name: string]: BlockToolConstructable } { public get blockTools(): Map<string, BlockTool> {
const tools = Object.entries(this.available).filter(([, tool]) => { if (this._blockTools) {
return !tool[this.INTERNAL_SETTINGS.IS_INLINE]; return this._blockTools;
}); }
/** const tools = Array
* collected block tools with key of tool name .from(this.available.entries())
*/ .filter(([, tool]) => {
const result = {}; return tool.type === ToolType.Block;
});
tools.forEach(([name, tool]) => { this._blockTools = new Map(tools) as Map<string, BlockTool>;
result[name] = tool;
});
return result; return this._blockTools;
} }
/** /**
* Constant for available Tools internal settings provided by Tool developer * Returns default Tool object
*
* @returns {object}
*/ */
public get INTERNAL_SETTINGS(): { [name: string]: string } { public get defaultTool(): BlockTool {
return { return this.blockTools.get(this.config.defaultBlock);
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',
};
} }
/** /**
* Constant for available Tools settings provided by user * Tools objects factory
*
* return {object}
*/ */
public get USER_SETTINGS(): { [name: string]: string } { private factory: ToolsFactory;
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 } = {};
/** /**
* Tools` classes available to use * Tools` classes available to use
*/ */
private readonly toolsAvailable: { [name: string]: ToolConstructable } = {}; private readonly toolsAvailable: Map<string, ToolClass> = new Map();
/** /**
* Tools` classes not available to use because of preparation failure * Tools` classes not available to use because of preparation failure
*/ */
private readonly toolsUnavailable: { [name: string]: ToolConstructable } = {}; private readonly toolsUnavailable: Map<string, ToolClass> = new Map();
/**
* Tools settings in a map {name: settings, ...}
*
* @type {object}
*/
private readonly toolsSettings: { [name: string]: ToolSettings } = {};
/** /**
* Cache for the prepared inline tools * Cache for the prepared inline tools
@ -194,41 +152,30 @@ export default class Tools extends Module {
* @type {null|object} * @type {null|object}
* @private * @private
*/ */
private _inlineTools: { [name: string]: ToolConstructable } = {}; private _inlineTools: Map<string, InlineTool> = null;
/** /**
* @class * Cache for the prepared block tools
*
* @param {EditorConfig} config - Editor's configuration
* @param {EventsDispatcher} eventsDispatcher - Editor's event dispatcher
*/ */
constructor({ config, eventsDispatcher }: ModuleConfig) { private _blockTools: Map<string, BlockTool> = null;
super({
config,
eventsDispatcher,
});
this.toolsClasses = {}; /**
* Returns internal tools
*
* @param type - if passed, Tools will be filtered by type
*/
public getInternal(type?: ToolType): Map<string, ToolClass> {
let tools = Array
.from(this.available.entries())
.filter(([, tool]) => {
return tool.isInternal;
});
this.toolsSettings = {}; if (type) {
tools = tools.filter(([, tool]) => tool.type === type);
}
/** return new Map(tools);
* Available tools list
* {name: Class, ...}
*
* @type {object}
*/
this.toolsAvailable = {};
/**
* Tools that rejected a prepare method
* {name: Class, ... }
*
* @type {object}
*/
this.toolsUnavailable = {};
this._inlineTools = null;
} }
/** /**
@ -248,54 +195,14 @@ export default class Tools extends Module {
throw Error('Can\'t start without tools'); throw Error('Can\'t start without tools');
} }
/** const config = this.prepareConfig();
* 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;
/** this.factory = new ToolsFactory(config, this.config, this.Editor.API);
* 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 };
}
}
/** /**
* getting classes that has prepare method * 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 * 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'} * to see how it works {@link '../utils.ts#sequence'}
*/ */
return _.sequence(sequenceData, (data: { toolName: string }) => { return _.sequence(sequenceData, (data: { toolName: string }) => {
this.success(data); this.toolPrepareMethodSuccess(data);
}, (data: { toolName: string }) => { }, (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 * Returns internal tools
* Includes Bold, Italic, Link and Paragraph * Includes Bold, Italic, Link and Paragraph
*/ */
public get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings } { public get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings & { isInternal?: boolean } } {
return { return {
bold: { class: BoldInlineTool }, bold: {
italic: { class: ItalicInlineTool }, class: BoldInlineTool,
link: { class: LinkInlineTool }, isInternal: true,
},
italic: {
class: ItalicInlineTool,
isInternal: true,
},
link: {
class: LinkInlineTool,
isInternal: true,
},
paragraph: { paragraph: {
class: Paragraph, class: Paragraph,
inlineToolbar: true, 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 * 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 * Binds prepare function of plugins with user or default config
* *
* @returns {Array} list of functions that needs to be fired sequentially * @returns {Array} list of functions that needs to be fired sequentially
* @param config - tools config
*/ */
private getListOfPrepareFunctions(): Array<{ private getListOfPrepareFunctions(config: {[name: string]: ToolSettings}): {
function: (data: { toolName: string; config: ToolConfig }) => void; function: (data: { toolName: string }) => void | Promise<void>;
data: { toolName: string; config: ToolConfig }; data: { toolName: string };
}> { }[] {
const toolPreparationList: Array<{ const toolPreparationList: {
function: (data: { toolName: string; config: ToolConfig }) => void; function: (data: { toolName: string }) => void | Promise<void>;
data: { toolName: string; config: ToolConfig }; data: { toolName: string };
} }[] = [];
> = [];
for (const toolName in this.toolsClasses) { Object
if (Object.prototype.hasOwnProperty.call(this.toolsClasses, toolName)) { .entries(config)
const toolClass = this.toolsClasses[toolName]; .forEach(([toolName, settings]) => {
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.
*/
toolPreparationList.push({ toolPreparationList.push({
// eslint-disable-next-line @typescript-eslint/no-empty-function // 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: { data: {
toolName, toolName,
config: toolConfig,
}, },
}); });
} });
}
return toolPreparationList; 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;
}
} }
/** /**

View file

@ -702,7 +702,7 @@ export default class UI extends Module<UINodes> {
* - Block is an default-block (Text) * - Block is an default-block (Text)
* - Block is empty * - Block is empty
*/ */
const isDefaultBlock = this.Editor.Tools.isDefault(this.Editor.BlockManager.currentBlock.tool); const isDefaultBlock = this.Editor.BlockManager.currentBlock.tool.isDefault;
if (isDefaultBlock) { if (isDefaultBlock) {
stopPropagation(); stopPropagation();

View file

@ -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<ToolSettings, 'class'>
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<Type extends Tool> {
/**
* 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<void> {
if (_.isFunction(this.constructable.reset)) {
return this.constructable.reset();
}
}
/**
* Calls Tool's prepare method
*/
public prepare(): void | Promise<void> {
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;
}

View file

@ -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<IBlockTool> {
/**
* 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] || {};
}
}

View file

@ -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];
}
}
}

View file

@ -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<IInlineTool> {
/**
* 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;
}
}

View file

@ -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<any> {
/**
* Tool type Tune
*/
public type = ToolType.Tune;
/**
* @todo implement
*/
public instance(): any {
return undefined;
}
}

View file

@ -1,5 +1,5 @@
import $ from '../../dom'; import $ from '../../components/dom';
import { API, BlockTool, BlockToolData, BlockToolConstructorOptions } from '../../../../types'; import { API, BlockTool, BlockToolConstructorOptions, BlockToolData } from '../../../types';
export interface StubData extends BlockToolData { export interface StubData extends BlockToolData {
title: string; title: string;

View file

@ -1,5 +1,5 @@
import {BaseTool, BaseToolConstructable} from './tool'; import {BaseTool, BaseToolConstructable} from './tool';
import {API, ToolConfig} from "../index"; import {API, ToolConfig} from '../index';
/** /**
* Base structure for the Inline Toolbar Tool * Base structure for the Inline Toolbar Tool
*/ */