[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"]
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

View file

@ -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<void> {
await this.tool.merge(data);
await this.toolInstance.merge(data);
}
/**
@ -536,7 +525,7 @@ export default class Block {
* @returns {object}
*/
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
@ -572,8 +561,8 @@ export default class Block {
public async validate(data: BlockToolData): Promise<boolean> {
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);

View file

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

View file

@ -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<void> {
await Promise.all(this.blocks.map((block) => {
if (_.isFunction(block.tool.destroy)) {
return block.tool.destroy();
}
return block.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.
* 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;
}

View file

@ -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<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(
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<void> {
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<void> {
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<Pick<SavedData, 'data' | 'tool'>>): void {
const { BlockManager, Caret, Sanitizer, Tools } = this.Editor;
private insertEditorJSData(blocks: Pick<SavedData, 'data' | 'tool'>[]): 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());

View file

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

View file

@ -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({

View file

@ -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<Pick<SavedData, 'data' | 'tool'>>
): Array<Pick<SavedData, 'data' | 'tool'>> {
blocksData: Pick<SavedData, 'data' | 'tool'>[]
): Pick<SavedData, 'data' | 'tool'>[] {
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<object | string>, ruleForItem: SanitizerConfig): Array<object | string> {
private cleanArray(array: (object | string)[], ruleForItem: SanitizerConfig): (object | string)[] {
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
*/
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);
}
}

View file

@ -182,10 +182,9 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
*
* @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<ConversionToolbarNodes> {
*
* @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<ConversionToolbarNodes> {
* 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<ConversionToolbarNodes> {
*/
const cleaned: string = this.Editor.Sanitizer.clean(
exportData,
replacingTool.sanitize
replacingTool.sanitizeConfig
);
/**
@ -238,7 +237,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
* 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<ConversionToolbarNodes> {
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);
});
}
/**

View file

@ -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<InlineToolbarNodes> {
private readonly toolbarVerticalMargin: number = 5;
/**
* TODO: Get rid of this
*
* Currently visible tools instances
*/
private toolsInstances: Map<string, InlineTool>;
private toolsInstances: Map<string, IInlineTool>;
/**
* Buttons List
@ -89,38 +94,6 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
*/
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<InlineToolbarNodes> {
/**
* 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<InlineToolbarNodes> {
* 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<InlineToolbarNodes> {
/**
* 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<InlineToolbarNodes> {
* 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<InlineToolbarNodes> {
/**
* 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<InlineToolbarNodes> {
* 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<InlineToolbarNodes> {
/**
* 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<InlineToolbarNodes> {
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<InlineToolbarNodes> {
placement: 'top',
hidingDelay: 100,
});
instance.checkState(SelectionUtils.get());
}
/**
@ -704,21 +663,20 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
* 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<InlineToolbarNodes> {
* @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<InlineToolbarNodes> {
*/
// 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<InlineToolbarNodes> {
*
* @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<InlineToolbarNodes> {
* 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;
}

View file

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

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 * 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<Tool>}
*/
public get available(): { [name: string]: ToolConstructable } {
public get available(): Map<string, ToolClass> {
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<string, ToolClass> {
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<string, InlineTool> {
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<any>]) => {
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<string, InlineTool>;
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<string, BlockTool> {
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<string, BlockTool>;
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<string, ToolClass> = 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<string, ToolClass> = 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<string, InlineTool> = 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<string, BlockTool> = null;
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);
}
/**
* 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<void>;
data: { toolName: string };
}[] {
const toolPreparationList: {
function: (data: { toolName: string }) => void | Promise<void>;
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;
}
}
/**

View file

@ -702,7 +702,7 @@ export default class UI extends Module<UINodes> {
* - 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();

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 { API, BlockTool, BlockToolData, BlockToolConstructorOptions } from '../../../../types';
import $ from '../../components/dom';
import { API, BlockTool, BlockToolConstructorOptions, BlockToolData } from '../../../types';
export interface StubData extends BlockToolData {
title: string;

View file

@ -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
*/