From 2a88a52a381b5ff691c07e0962d409cd505cb507 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Thu, 29 Aug 2024 21:20:52 +0300 Subject: [PATCH 1/8] Introduce DI & migrate tools builders --- packages/core/package.json | 5 +- packages/core/src/entities/BlockTool.ts | 4 +- packages/core/src/index.ts | 31 +- packages/core/src/tools/ToolsManager.ts | 208 +++++++++++++- .../src/tools/builders/BaseToolBuilder.ts | 266 ++++++++++++++++++ .../src/tools/builders/BlockToolBuilder.ts | 217 ++++++++++++++ .../src/tools/builders/BlockTuneBuilder.ts | 34 +++ .../src/tools/builders/InlineToolBuilder.ts | 35 +++ packages/core/src/tools/builders/ToolType.ts | 18 ++ .../src/tools/builders/ToolsCollection.ts | 65 +++++ .../core/src/tools/builders/ToolsFactory.ts | 94 +++++++ packages/core/src/tools/builders/index.ts | 6 + packages/core/tsconfig.json | 1 + yarn.lock | 17 ++ 14 files changed, 979 insertions(+), 22 deletions(-) create mode 100644 packages/core/src/tools/builders/BaseToolBuilder.ts create mode 100644 packages/core/src/tools/builders/BlockToolBuilder.ts create mode 100644 packages/core/src/tools/builders/BlockTuneBuilder.ts create mode 100644 packages/core/src/tools/builders/InlineToolBuilder.ts create mode 100644 packages/core/src/tools/builders/ToolType.ts create mode 100644 packages/core/src/tools/builders/ToolsCollection.ts create mode 100644 packages/core/src/tools/builders/ToolsFactory.ts create mode 100644 packages/core/src/tools/builders/index.ts diff --git a/packages/core/package.json b/packages/core/package.json index 093c3709..91e81aab 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,6 +20,9 @@ "dependencies": { "@editorjs/dom-adapters": "workspace:^", "@editorjs/editorjs": "^2.30.5", - "@editorjs/model": "workspace:^" + "@editorjs/helpers": "^1.0.0", + "@editorjs/model": "workspace:^", + "reflect-metadata": "^0.2.2", + "typedi": "^0.10.0" } } diff --git a/packages/core/src/entities/BlockTool.ts b/packages/core/src/entities/BlockTool.ts index 9381abc7..d4e8a429 100644 --- a/packages/core/src/entities/BlockTool.ts +++ b/packages/core/src/entities/BlockTool.ts @@ -1,5 +1,5 @@ import type { BlockToolAdapter } from '@editorjs/dom-adapters'; -import type { BlockTool as BlockToolVersion2 } from '@editorjs/editorjs'; +import type { BlockTool as BlockToolVersion2, BlockToolConstructable as BlockToolConstructableV2 } from '@editorjs/editorjs'; import type { BlockToolConstructorOptions as BlockToolConstructorOptionsVersion2 } from '@editorjs/editorjs'; /** @@ -24,4 +24,4 @@ export interface BlockTool extends Omit { /** * Block Tool constructor class */ -export type BlockToolConstructor = new (options: BlockToolConstructorOptions) => BlockTool; +export type BlockToolConstructor = BlockToolConstructableV2 & (new (options: BlockToolConstructorOptions) => BlockTool); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9e3e908b..751b3e84 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,7 @@ import type { ModelEvents } from '@editorjs/model'; import { BlockAddedEvent, EditorJSModel, EventType } from '@editorjs/model'; +import type { ContainerInstance } from 'typedi'; +import { Container } from 'typedi'; import type { CoreConfig, CoreConfigValidated } from './entities/Config.js'; import { composeDataFromVersion2 } from './utils/composeDataFromVersion2.js'; import ToolsManager from './tools/ToolsManager.js'; @@ -21,6 +23,8 @@ const DEFAULT_HOLDER_ID = 'editorjs'; * - adds Blocks accodring to model updates */ export default class Core { + #id = Math.floor(Math.random() * 1e10).toString(); + /** * Editor's Document Model */ @@ -41,21 +45,33 @@ export default class Core { */ #caretAdapter: CaretAdapter; + #iocContainer: ContainerInstance; + /** * @param config - Editor configuration */ constructor(config: CoreConfig) { + this.#iocContainer = Container.of(this.#id); + this.validateConfig(config); this.#config = config as CoreConfigValidated; + this.#iocContainer.set('EditorConfig', this.#config); + const { blocks } = composeDataFromVersion2(config.data ?? { blocks: [] }); this.#model = new EditorJSModel(); + + this.#iocContainer.set(EditorJSModel, this.#model); + this.#model.addEventListener(EventType.Changed, (event: ModelEvents) => this.handleModelUpdate(event)); - this.#toolsManager = new ToolsManager(this.#config.tools); + this.#toolsManager = this.#iocContainer.get(ToolsManager); + this.#caretAdapter = new CaretAdapter(this.#config.holder, this.#model); + this.#iocContainer.set(CaretAdapter, this.#caretAdapter); + this.#model.initializeDocument({ blocks }); } @@ -141,14 +157,15 @@ export default class Core { */ data: BlockToolData>; }, blockToolAdapter: BlockToolAdapter): BlockTool { - const tool = this.#toolsManager.resolveBlockTool(name); - const block = new tool({ + const tool = this.#toolsManager.blockTools.get(name); + + if (!tool) { + throw new Error(`Block Tool ${name} not found`); + } + + const block = tool.create({ adapter: blockToolAdapter, data: data, - - // @todo - api: {} as EditorjsApi, - config: {} as ToolConfig>, block: {} as BlockAPI, readOnly: false, }); diff --git a/packages/core/src/tools/ToolsManager.ts b/packages/core/src/tools/ToolsManager.ts index 48db7d04..094ac954 100644 --- a/packages/core/src/tools/ToolsManager.ts +++ b/packages/core/src/tools/ToolsManager.ts @@ -1,27 +1,211 @@ -import type { BlockToolConstructor } from '../entities/BlockTool.js'; +import 'reflect-metadata'; +import { deepMerge, isFunction, isObject, PromiseQueue } from '@editorjs/helpers'; +import { Inject, Service } from 'typedi'; +import type { BlockToolConstructor } from "../entities/BlockTool.js"; +import { + BlockToolBuilder, BlockTuneBuilder, + InlineToolBuilder, + ToolsCollection, + ToolsFactory, + UnifiedToolConfig +} from "./builders/index.js"; import { Paragraph } from './internal/paragraph/index.js'; -import type { EditorConfig } from '@editorjs/editorjs'; +import type { + EditorConfig, + InlineTool, + ToolConfig, + ToolConstructable, + ToolSettings +} from '@editorjs/editorjs'; /** * Works with tools + * @todo - validate tools configurations + * @todo - merge internal tools */ +@Service() export default class ToolsManager { + #factory: ToolsFactory; + #config: UnifiedToolConfig; + #availableTools = new ToolsCollection(); + #unavailableTools = new ToolsCollection(); + + /** + * Returns available Tools + */ + public get available(): ToolsCollection { + return this.#availableTools; + } + + /** + * Returns unavailable Tools + */ + public get unavailable(): ToolsCollection { + return this.#unavailableTools; + } + + /** + * Return Tools for the Inline Toolbar + */ + public get inlineTools(): ToolsCollection { + return this.available.inlineTools; + } + + /** + * Return editor block tools + */ + public get blockTools(): ToolsCollection { + return this.available.blockTools; + } + + /** + * Return available Block Tunes + * @returns - object of Inline Tool's classes + */ + public get blockTunes(): ToolsCollection { + return this.available.blockTunes; + } + + /** + * Returns internal tools + */ + public get internal(): ToolsCollection { + return this.available.internalTools; + } + + /** + * @param editorConfig - EditorConfig object + * @param editorConfig.tools - Tools configuration passed by user + */ + constructor(@Inject('EditorConfig') editorConfig: EditorConfig) { + this.#config = this.#prepareConfig(editorConfig.tools ?? {}); + + this.#validateTools(); + + this.#factory = new ToolsFactory(this.#config, editorConfig, {}); + + void this.#prepareTools(editorConfig.tools); + } + + #prepareTools(config: ToolConfig): Promise { + const promiseQueue = new PromiseQueue(); + + Object.entries(this.#config).forEach(([toolName, config]) => { + if (isFunction(config.class.prepare)) { + promiseQueue.add(async () => { + try { + await config.class.prepare!({ + toolName: toolName, + config: config, + }); + + const tool = this.#factory.get(toolName); + + if (tool.isInline()) { + /** + * Some Tools validation + */ + const inlineToolRequiredMethods = ['render']; + const notImplementedMethods = inlineToolRequiredMethods.filter(method => !tool.create()[method as keyof InlineTool]); + + if (notImplementedMethods.length) { + /** + * @todo implement logger + */ + console.log( + `Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`, + 'warn', + notImplementedMethods + ); + + this.#unavailableTools.set(tool.name, tool); + + return; + } + } + + this.#availableTools.set(toolName, tool); + + console.log(this.#availableTools.entries()); + } catch (e) { + console.error(`Tool ${toolName} failed to prepare`, e); + + this.#unavailableTools.set(toolName, this.#factory.get(toolName)); + } + }); + } else { + this.#availableTools.set(toolName, this.#factory.get(toolName)); + } + }); + + return promiseQueue.completed; + } + /** - * @param tools - Tools configuration passed by user + * Unify tools config + * @param config */ - constructor(private tools: EditorConfig['tools']) { + #prepareConfig(config: EditorConfig['tools']): UnifiedToolConfig { + const unifiedConfig: UnifiedToolConfig = {} as UnifiedToolConfig; + + /** + * Save Tools settings to a map + */ + for (const toolName in config) { + /** + * If Tool is an object not a Tool's class then + * save class and settings separately + */ + if (isObject(config)) { + unifiedConfig[toolName] = config[toolName] as UnifiedToolConfig[string]; + } else { + unifiedConfig[toolName] = { class: config[toolName] as ToolConstructable }; + } + } + + deepMerge(unifiedConfig, this.#internalTools); + + return unifiedConfig; } /** - * Returns a block tool by its name - * @param toolName - name of a tool to resolve + * Validate Tools configuration objects and throw Error for user if it is invalid */ - public resolveBlockTool(toolName: string): BlockToolConstructor { - switch (toolName) { - case 'paragraph': - return Paragraph; - default: - throw new Error(`Unknown tool: ${toolName}`); + #validateTools(): void { + /** + * Check Tools for a class containing + */ + for (const toolName in this.#config) { + if (Object.prototype.hasOwnProperty.call(this.#config, toolName)) { + // if (toolName in this.internalTools) { + // return; + // } + + const tool = this.#config[toolName]; + + if (!isFunction(tool) && !isFunction((tool as ToolSettings).class)) { + throw Error( + `Tool «${toolName}» must be a constructor function or an object with function in the «class» property` + ); + } + } } } + + /** + * Returns internal tools + * Includes Bold, Italic, Link and Paragraph + */ + get #internalTools(): UnifiedToolConfig { + return { + paragraph: { + /** + * @todo solve problems with types + */ + class: Paragraph as any, + inlineToolbar: true, + isInternal: true, + }, + }; + } } diff --git a/packages/core/src/tools/builders/BaseToolBuilder.ts b/packages/core/src/tools/builders/BaseToolBuilder.ts new file mode 100644 index 00000000..9781df09 --- /dev/null +++ b/packages/core/src/tools/builders/BaseToolBuilder.ts @@ -0,0 +1,266 @@ +import type { + SanitizerConfig, + API as ApiMethods, + Tool, + ToolConstructable as ToolConstructableV2, + ToolSettings, +} from '@editorjs/editorjs'; +import { isFunction } from '@editorjs/helpers'; +import { type BlockToolBuilder } from './BlockToolBuilder.js'; +import { type InlineToolBuilder } from './InlineToolBuilder.js'; +import { ToolType } from './ToolType.js'; +import { type BlockTuneBuilder } from './BlockTuneBuilder.js'; +import { BlockToolConstructor as BlockToolConstructable } from "../../entities/BlockTool.js"; + +export type ToolConstructable = ToolConstructableV2 | BlockToolConstructable; + +/** + * 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', + /** + * Enabled Block Tunes for Block Tool + */ + EnabledBlockTunes = 'tunes', + /** + * 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 options provided by Block Tool + */ +export enum InternalBlockToolSettings { + /** + * Is line breaks enabled for Tool + */ + IsEnabledLineBreaks = 'enableLineBreaks', + /** + * Tool Toolbox config + */ + Toolbox = 'toolbox', + /** + * Tool conversion config + */ + ConversionConfig = 'conversionConfig', + /** + * Is readonly mode supported for Tool + */ + IsReadOnlySupported = 'isReadOnlySupported', + /** + * Tool paste config + */ + PasteConfig = 'pasteConfig' +} + +/** + * Enum of Tool options provided by Inline Tool + */ +export enum InternalInlineToolSettings { + /** + * Flag specifies Tool is inline + */ + IsInline = 'isInline', + /** + * Inline Tool title for toolbar + */ + Title = 'title' // for Inline Tools. Block Tools can pass title along with icon through the 'toolbox' static prop. +} + +/** + * Enum of Tool options provided by Block Tune + */ +export enum InternalTuneSettings { + /** + * Flag specifies Tool is Block Tune + */ + IsTune = 'isTune' +} + +export type ToolOptions = Omit; + +interface ConstructorOptions { + name: string; + constructable: ToolConstructable; + config: ToolOptions; + api: ApiMethods; + isDefault: boolean; + isInternal: boolean; + defaultPlaceholder?: string | false; +} + +/** + * Base abstract class for Tools + */ +export abstract class BaseToolBuilder { + /** + * Tool type: Block, Inline or Tune + */ + public abstract type: Type; + + /** + * 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: ApiMethods; + + /** + * Current tool user configuration + */ + protected config: ToolOptions; + + /** + * Tool's constructable blueprint + */ + protected readonly constructable: ToolConstructable; + + /** + * Default placeholder specified in EditorJS user configuration + */ + protected defaultPlaceholder?: string | false; + + /** + * @param options - Constructor options + */ + constructor({ + name, + constructable, + config, + api, + isDefault, + isInternal = false, + defaultPlaceholder, + }: ConstructorOptions) { + this.api = api; + this.name = name; + this.constructable = constructable; + this.config = config; + this.isDefault = isDefault; + this.isInternal = isInternal; + this.defaultPlaceholder = defaultPlaceholder; + } + + /** + * Returns Tool user configuration + */ + public get settings(): ToolOptions { + const config = this.config[UserSettings.Config] || {}; + + if (this.isDefault && !('placeholder' in config) && this.defaultPlaceholder) { + config.placeholder = this.defaultPlaceholder; + } + + return config; + } + + /** + * Calls Tool's reset method + */ + public reset(): void | Promise { + if (isFunction(this.constructable.reset)) { + return this.constructable.reset(); + } + } + + /** + * Calls Tool's prepare method + */ + public prepare(): void | Promise { + if (isFunction(this.constructable.prepare)) { + return this.constructable.prepare({ + toolName: this.name, + config: this.settings, + }); + } + } + + /** + * Returns shortcut for Tool (internal or specified by user) + */ + public get shortcut(): string | undefined { + /** + * @todo check if we support user shortcuts as static property as it is not specified in types + */ + // const toolShortcut = this.constructable[CommonInternalSettings.Shortcut]; + const userShortcut = this.config[UserSettings.Shortcut]; + + return userShortcut; // || toolShortcut; + } + + /** + * Returns Tool's sanitizer configuration + */ + public get sanitizeConfig(): SanitizerConfig { + return this.constructable[CommonInternalSettings.SanitizeConfig] || {}; + } + + /** + * Returns true if Tools is inline + */ + public isInline(): this is InlineToolBuilder { + return this.type === ToolType.Inline; + } + + /** + * Returns true if Tools is block + */ + public isBlock(): this is BlockToolBuilder { + return this.type === ToolType.Block; + } + + /** + * Returns true if Tools is tune + */ + public isTune(): this is BlockTuneBuilder { + return this.type === ToolType.Tune; + } + + /** + * Constructs new Tool instance from constructable blueprint + * @param args + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public abstract create(...args: any[]): ToolClass; +} diff --git a/packages/core/src/tools/builders/BlockToolBuilder.ts b/packages/core/src/tools/builders/BlockToolBuilder.ts new file mode 100644 index 00000000..2f335ed5 --- /dev/null +++ b/packages/core/src/tools/builders/BlockToolBuilder.ts @@ -0,0 +1,217 @@ +import type { BlockToolAdapter } from "@editorjs/dom-adapters"; +import { BaseToolBuilder, InternalBlockToolSettings, UserSettings } from './BaseToolBuilder.js'; +import type { + BlockAPI, + BlockToolData, + ConversionConfig, + PasteConfig, SanitizerConfig, ToolboxConfig, + ToolboxConfigEntry +} from '@editorjs/editorjs'; +import { isEmpty, cacheable, isObject } from '@editorjs/helpers'; +import { type InlineToolBuilder } from './InlineToolBuilder.js'; +import { ToolType } from './ToolType.js'; +import { type BlockTuneBuilder } from './BlockTuneBuilder.js'; +import { ToolsCollection } from './ToolsCollection.js'; +import { BlockToolConstructor as BlockToolConstructable, BlockTool as IBlockTool } from '../../entities/BlockTool.js'; + +/** + * Class to work with Block tools constructables + */ +export class BlockToolBuilder extends BaseToolBuilder { + /** + * Tool type — Block + */ + public type: ToolType.Block = ToolType.Block; + + /** + * InlineTool collection for current Block Tool + */ + public inlineTools: ToolsCollection = new ToolsCollection(); + + /** + * BlockTune collection for current Block Tool + */ + public tunes: ToolsCollection = new ToolsCollection(); + + /** + * Tool's constructable blueprint + */ + protected declare constructable: BlockToolConstructable; + + /** + * Creates new Tool instance + * @param options + * @param options.data - Tool data + * @param options.block - BlockAPI for current Block + * @param options.readOnly - True if Editor is in read-only mode + */ + public create({ data, block, readOnly, adapter }: { data: BlockToolData, block: BlockAPI, readOnly: boolean, adapter: BlockToolAdapter }): IBlockTool { + return new this.constructable({ + adapter, + data, + block, + readOnly, + api: this.api, + config: this.settings, + }); + } + + /** + * 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 + * @todo check if we still need this as BlockToolConstructable doesn't have line breaks option + */ + // public get isLineBreaksEnabled(): boolean { + // return this.constructable[InternalBlockToolSettings.IsEnabledLineBreaks]; + // } + + /** + * Returns Tool toolbox configuration (internal or user-specified). + * + * Merges internal and user-defined toolbox configs based on the following rules: + * + * - If both internal and user-defined toolbox configs are arrays their items are merged. + * Length of the second one is kept. + * + * - If both are objects their properties are merged. + * + * - If one is an object and another is an array than internal config is replaced with user-defined + * config. This is made to allow user to override default tool's toolbox representation (single/multiple entries) + */ + public get toolbox(): ToolboxConfigEntry[] | undefined { + const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig; + const userToolboxSettings = this.config[UserSettings.Toolbox]; + + if (isEmpty(toolToolboxSettings)) { + return; + } + if (userToolboxSettings === false) { + return; + } + /** + * Return tool's toolbox settings if user settings are not defined + */ + if (!userToolboxSettings) { + return Array.isArray(toolToolboxSettings) ? toolToolboxSettings : [toolToolboxSettings]; + } + + /** + * Otherwise merge user settings with tool's settings + */ + if (Array.isArray(toolToolboxSettings)) { + if (Array.isArray(userToolboxSettings)) { + return userToolboxSettings.map((item, i) => { + const toolToolboxEntry = toolToolboxSettings[i]; + + if (toolToolboxEntry) { + return { + ...toolToolboxEntry, + ...item, + }; + } + + return item; + }); + } + + return [userToolboxSettings]; + } else { + if (Array.isArray(userToolboxSettings)) { + return userToolboxSettings; + } + + return [ + { + ...toolToolboxSettings, + ...userToolboxSettings, + }, + ]; + } + } + + /** + * Returns Tool conversion configuration + */ + public get conversionConfig(): ConversionConfig | undefined { + return this.constructable[InternalBlockToolSettings.ConversionConfig]; + } + + /** + * Returns enabled inline tools for Tool + */ + public get enabledInlineTools(): boolean | string[] { + return this.config[UserSettings.EnabledInlineTools] || false; + } + + /** + * Returns enabled tunes for Tool + */ + public get enabledBlockTunes(): boolean | string[] { + return this.config[UserSettings.EnabledBlockTunes] ?? false; + } + + /** + * Returns Tool paste configuration + */ + public get pasteConfig(): PasteConfig { + return this.constructable[InternalBlockToolSettings.PasteConfig] ?? {}; + } + + /** + * Returns sanitize configuration for Block Tool including configs from related Inline Tools and Block Tunes + */ + @cacheable + public get sanitizeConfig(): SanitizerConfig { + const toolRules = super.sanitizeConfig; + const baseConfig = this.baseSanitizeConfig; + + if (isEmpty(toolRules)) { + return baseConfig; + } + + const toolConfig = {} as SanitizerConfig; + + for (const fieldName in toolRules) { + if (Object.prototype.hasOwnProperty.call(toolRules, fieldName)) { + const rule = toolRules[fieldName]; + + /** + * If rule is object, merge it with Inline Tools configuration + * + * Otherwise pass as it is + */ + if (isObject(rule)) { + toolConfig[fieldName] = Object.assign({}, baseConfig, rule); + } else { + toolConfig[fieldName] = rule; + } + } + } + + return toolConfig; + } + + /** + * Returns sanitizer configuration composed from sanitize config of Inline Tools enabled for Tool + */ + @cacheable + public get baseSanitizeConfig(): SanitizerConfig { + const baseConfig = {}; + + Array + .from(this.inlineTools.values()) + .forEach(tool => Object.assign(baseConfig, tool.sanitizeConfig)); + + Array + .from(this.tunes.values()) + .forEach(tune => Object.assign(baseConfig, tune.sanitizeConfig)); + + return baseConfig; + } +} diff --git a/packages/core/src/tools/builders/BlockTuneBuilder.ts b/packages/core/src/tools/builders/BlockTuneBuilder.ts new file mode 100644 index 00000000..4902bb4f --- /dev/null +++ b/packages/core/src/tools/builders/BlockTuneBuilder.ts @@ -0,0 +1,34 @@ +import { BaseToolBuilder } from './BaseToolBuilder.js'; +import type { BlockAPI, BlockTune as IBlockTune, BlockTuneConstructable } from '@editorjs/editorjs'; +import { ToolType } from './ToolType.js'; +// import type { BlockTuneData } from '@editorjs/editorjs'; + +/** + * Stub class for BlockTunes + * @todo Implement + */ +export class BlockTuneBuilder extends BaseToolBuilder { + /** + * Tool type — Tune + */ + public type: ToolType.Tune = ToolType.Tune; + + /** + * Tool's constructable blueprint + */ + protected declare constructable: BlockTuneConstructable; + + /** + * Constructs new BlockTune instance from constructable + * @param data - Tune data + * @param block - Block API object + */ + public create(data: any, block: BlockAPI): IBlockTune { + return new this.constructable({ + api: this.api, + config: this.settings, + block, + data, + }); + } +} diff --git a/packages/core/src/tools/builders/InlineToolBuilder.ts b/packages/core/src/tools/builders/InlineToolBuilder.ts new file mode 100644 index 00000000..3cb3b646 --- /dev/null +++ b/packages/core/src/tools/builders/InlineToolBuilder.ts @@ -0,0 +1,35 @@ +import { BaseToolBuilder, InternalInlineToolSettings } from './BaseToolBuilder.js'; +import type { InlineTool as IInlineTool, InlineToolConstructable } from '@editorjs/editorjs'; +import { ToolType } from './ToolType.js'; + +/** + * InlineTool object to work with Inline Tools constructables + */ +export class InlineToolBuilder extends BaseToolBuilder { + /** + * Tool type — Inline + */ + public type: ToolType.Inline = ToolType.Inline; + + /** + * Tool's constructable blueprint + */ + protected declare constructable: InlineToolConstructable; + + /** + * Returns title for Inline Tool if specified by user + */ + public get title(): string | undefined { + return this.constructable[InternalInlineToolSettings.Title]; + } + + /** + * Constructs new InlineTool instance from constructable + */ + public create(): IInlineTool { + return new this.constructable({ + api: this.api, + config: this.settings, + }) as IInlineTool; + } +} diff --git a/packages/core/src/tools/builders/ToolType.ts b/packages/core/src/tools/builders/ToolType.ts new file mode 100644 index 00000000..78689195 --- /dev/null +++ b/packages/core/src/tools/builders/ToolType.ts @@ -0,0 +1,18 @@ +/** + * What kind of plugins developers can create + */ +export enum ToolType { + /** + * Block tool + */ + Block, + /** + * Inline tool + */ + Inline, + + /** + * Block tune + */ + Tune +} diff --git a/packages/core/src/tools/builders/ToolsCollection.ts b/packages/core/src/tools/builders/ToolsCollection.ts new file mode 100644 index 00000000..ef7849e5 --- /dev/null +++ b/packages/core/src/tools/builders/ToolsCollection.ts @@ -0,0 +1,65 @@ +import { type BlockToolBuilder } from './BlockToolBuilder.js'; +import { type InlineToolBuilder } from './InlineToolBuilder.js'; +import { type BlockTuneBuilder } from './BlockTuneBuilder.js'; + +export type ToolClass = BlockToolBuilder | InlineToolBuilder | BlockTuneBuilder; + +/** + * Class to store Editor Tools + */ +export class ToolsCollection extends Map { + /** + * Returns Block Tools collection + */ + public get blockTools(): ToolsCollection { + const tools = Array + .from(this.entries()) + .filter(([, tool]) => tool.isBlock()) as [string, BlockToolBuilder][]; + + return new ToolsCollection(tools); + } + + /** + * Returns Inline Tools collection + */ + public get inlineTools(): ToolsCollection { + const tools = Array + .from(this.entries()) + .filter(([, tool]) => tool.isInline()) as [string, InlineToolBuilder][]; + + return new ToolsCollection(tools); + } + + /** + * Returns Block Tunes collection + */ + public get blockTunes(): ToolsCollection { + const tools = Array + .from(this.entries()) + .filter(([, tool]) => tool.isTune()) as [string, BlockTuneBuilder][]; + + return new ToolsCollection(tools); + } + + /** + * Returns internal Tools collection + */ + public get internalTools(): ToolsCollection { + const tools = Array + .from(this.entries()) + .filter(([, tool]) => tool.isInternal); + + return new ToolsCollection(tools); + } + + /** + * Returns Tools collection provided by user + */ + public get externalTools(): ToolsCollection { + const tools = Array + .from(this.entries()) + .filter(([, tool]) => !tool.isInternal); + + return new ToolsCollection(tools); + } +} diff --git a/packages/core/src/tools/builders/ToolsFactory.ts b/packages/core/src/tools/builders/ToolsFactory.ts new file mode 100644 index 00000000..6534d52a --- /dev/null +++ b/packages/core/src/tools/builders/ToolsFactory.ts @@ -0,0 +1,94 @@ +import { InternalInlineToolSettings, InternalTuneSettings } from './BaseToolBuilder.js'; +import { InlineToolBuilder } from './InlineToolBuilder.js'; +import { BlockTuneBuilder } from './BlockTuneBuilder.js'; +import { BlockToolBuilder } from './BlockToolBuilder.js'; +// import type ApiModule from '../modules/api'; +import type { + ToolConstructable, + ToolSettings, + EditorConfig, + InlineToolConstructable, + BlockTuneConstructable +} from '@editorjs/editorjs'; + +type ToolConstructor = typeof InlineToolBuilder | typeof BlockToolBuilder | typeof BlockTuneBuilder; + +export type UnifiedToolConfig = Record & { + class: ToolConstructable; + isInternal?: boolean; +}>; + +/** + * Factory to construct classes to work with tools + */ +export class ToolsFactory { + /** + * Tools configuration specified by user + */ + private config: UnifiedToolConfig; + + /** + * EditorJS API Module + */ + private api: any; + + /** + * EditorJS configuration + */ + private editorConfig: EditorConfig; + + /** + * @param config - tools config + * @param editorConfig - EditorJS config + * @param api - EditorJS API module + */ + constructor( + config: UnifiedToolConfig, + editorConfig: EditorConfig, + api: any + ) { + 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): InlineToolBuilder | BlockToolBuilder | BlockTuneBuilder { + const { class: constructable, isInternal = false, ...config } = this.config[name]; + + const Constructor = this.getConstructor(constructable); + // const isTune = constructable[InternalTuneSettings.IsTune]; + + return new Constructor({ + name, + constructable, + config, + api: {}, + // api: this.api.getMethodsForTool(name, isTune), + isDefault: name === this.editorConfig.defaultBlock, + defaultPlaceholder: this.editorConfig.placeholder, + isInternal, + /** + * @todo implement api.getMethodsForTool + */ + } as any); + } + + /** + * Find appropriate Tool object constructor for Tool constructable + * @param constructable - Tools constructable + */ + private getConstructor(constructable: ToolConstructable): ToolConstructor { + switch (true) { + case (constructable as InlineToolConstructable)[InternalInlineToolSettings.IsInline]: + return InlineToolBuilder; + case (constructable as BlockTuneConstructable)[InternalTuneSettings.IsTune]: + return BlockTuneBuilder; + default: + return BlockToolBuilder; + } + } +} diff --git a/packages/core/src/tools/builders/index.ts b/packages/core/src/tools/builders/index.ts new file mode 100644 index 00000000..88e13118 --- /dev/null +++ b/packages/core/src/tools/builders/index.ts @@ -0,0 +1,6 @@ +export * from './BlockToolBuilder.js' +export * from './BlockTuneBuilder.js' +export * from './InlineToolBuilder.js' +export * from './ToolsFactory.js' +export * from './ToolsCollection.js' +export * from './ToolType.js' diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 14b9a083..dfbd8664 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -9,6 +9,7 @@ "strict": true, "skipLibCheck": true, "experimentalDecorators": true, + "emitDecoratorMetadata": true, "types": ["jest"], "rootDir": "src", "outDir": "dist" diff --git a/yarn.lock b/yarn.lock index 97e43eaa..3cb2bbbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -633,9 +633,12 @@ __metadata: dependencies: "@editorjs/dom-adapters": "workspace:^" "@editorjs/editorjs": "npm:^2.30.5" + "@editorjs/helpers": "npm:^1.0.0" "@editorjs/model": "workspace:^" eslint: "npm:^9.9.1" eslint-config-codex: "npm:^2.0.2" + reflect-metadata: "npm:^0.2.2" + typedi: "npm:^0.10.0" typescript: "npm:^5.5.4" languageName: unknown linkType: soft @@ -6873,6 +6876,13 @@ __metadata: languageName: node linkType: hard +"reflect-metadata@npm:^0.2.2": + version: 0.2.2 + resolution: "reflect-metadata@npm:0.2.2" + checksum: 1c93f9ac790fea1c852fde80c91b2760420069f4862f28e6fae0c00c6937a56508716b0ed2419ab02869dd488d123c4ab92d062ae84e8739ea7417fae10c4745 + languageName: node + linkType: hard + "regenerator-runtime@npm:^0.14.0": version: 0.14.1 resolution: "regenerator-runtime@npm:0.14.1" @@ -7915,6 +7925,13 @@ __metadata: languageName: node linkType: hard +"typedi@npm:^0.10.0": + version: 0.10.0 + resolution: "typedi@npm:0.10.0" + checksum: 3d519795b4d1a9edea2e5fdb9fa61bc2eca00546aeaf2dbbc1597cfed021659ca117454913022828c77f4971dd03f38a3f9a646eda374ccfb565d3ddcb661224 + languageName: node + linkType: hard + "typescript-eslint@npm:^8.3.0": version: 8.3.0 resolution: "typescript-eslint@npm:8.3.0" From 151f471054bea4ebfe7e428f8208aad4cd0d6b63 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Thu, 29 Aug 2024 23:24:18 +0300 Subject: [PATCH 2/8] Fixes after review --- packages/core/src/index.ts | 4 +-- packages/core/src/tools/builders/index.ts | 6 ----- .../BaseToolFacade.ts} | 14 +++++----- .../BlockToolFacade.ts} | 12 ++++----- .../BlockTuneFacade.ts} | 4 +-- .../InlineToolFacade.ts} | 4 +-- .../tools/{builders => facades}/ToolType.ts | 0 .../{builders => facades}/ToolsCollection.ts | 26 +++++++++---------- .../{builders => facades}/ToolsFactory.ts | 18 ++++++------- packages/core/src/tools/facades/index.ts | 6 +++++ 10 files changed, 46 insertions(+), 48 deletions(-) delete mode 100644 packages/core/src/tools/builders/index.ts rename packages/core/src/tools/{builders/BaseToolBuilder.ts => facades/BaseToolFacade.ts} (92%) rename packages/core/src/tools/{builders/BlockToolBuilder.ts => facades/BlockToolFacade.ts} (92%) rename packages/core/src/tools/{builders/BlockTuneBuilder.ts => facades/BlockTuneFacade.ts} (84%) rename packages/core/src/tools/{builders/InlineToolBuilder.ts => facades/InlineToolFacade.ts} (82%) rename packages/core/src/tools/{builders => facades}/ToolType.ts (100%) rename packages/core/src/tools/{builders => facades}/ToolsCollection.ts (62%) rename packages/core/src/tools/{builders => facades}/ToolsFactory.ts (81%) create mode 100644 packages/core/src/tools/facades/index.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 751b3e84..540bdb1c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,8 +23,6 @@ const DEFAULT_HOLDER_ID = 'editorjs'; * - adds Blocks accodring to model updates */ export default class Core { - #id = Math.floor(Math.random() * 1e10).toString(); - /** * Editor's Document Model */ @@ -51,7 +49,7 @@ export default class Core { * @param config - Editor configuration */ constructor(config: CoreConfig) { - this.#iocContainer = Container.of(this.#id); + this.#iocContainer = Container.of(Math.floor(Math.random() * 1e10).toString()); this.validateConfig(config); this.#config = config as CoreConfigValidated; diff --git a/packages/core/src/tools/builders/index.ts b/packages/core/src/tools/builders/index.ts deleted file mode 100644 index 88e13118..00000000 --- a/packages/core/src/tools/builders/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './BlockToolBuilder.js' -export * from './BlockTuneBuilder.js' -export * from './InlineToolBuilder.js' -export * from './ToolsFactory.js' -export * from './ToolsCollection.js' -export * from './ToolType.js' diff --git a/packages/core/src/tools/builders/BaseToolBuilder.ts b/packages/core/src/tools/facades/BaseToolFacade.ts similarity index 92% rename from packages/core/src/tools/builders/BaseToolBuilder.ts rename to packages/core/src/tools/facades/BaseToolFacade.ts index 9781df09..d130e11e 100644 --- a/packages/core/src/tools/builders/BaseToolBuilder.ts +++ b/packages/core/src/tools/facades/BaseToolFacade.ts @@ -6,10 +6,10 @@ import type { ToolSettings, } from '@editorjs/editorjs'; import { isFunction } from '@editorjs/helpers'; -import { type BlockToolBuilder } from './BlockToolBuilder.js'; -import { type InlineToolBuilder } from './InlineToolBuilder.js'; +import { type BlockToolFacade } from './BlockToolFacade.js'; +import { type InlineToolFacade } from './InlineToolFacade.js'; import { ToolType } from './ToolType.js'; -import { type BlockTuneBuilder } from './BlockTuneBuilder.js'; +import { type BlockTuneFacade } from './BlockTuneFacade.js'; import { BlockToolConstructor as BlockToolConstructable } from "../../entities/BlockTool.js"; export type ToolConstructable = ToolConstructableV2 | BlockToolConstructable; @@ -120,7 +120,7 @@ interface ConstructorOptions { /** * Base abstract class for Tools */ -export abstract class BaseToolBuilder { +export abstract class BaseToolFacade { /** * Tool type: Block, Inline or Tune */ @@ -239,21 +239,21 @@ export abstract class BaseToolBuilder { +export class BlockToolFacade extends BaseToolFacade { /** * Tool type — Block */ @@ -26,12 +26,12 @@ export class BlockToolBuilder extends BaseToolBuilder = new ToolsCollection(); + public inlineTools: ToolsCollection = new ToolsCollection(); /** * BlockTune collection for current Block Tool */ - public tunes: ToolsCollection = new ToolsCollection(); + public tunes: ToolsCollection = new ToolsCollection(); /** * Tool's constructable blueprint diff --git a/packages/core/src/tools/builders/BlockTuneBuilder.ts b/packages/core/src/tools/facades/BlockTuneFacade.ts similarity index 84% rename from packages/core/src/tools/builders/BlockTuneBuilder.ts rename to packages/core/src/tools/facades/BlockTuneFacade.ts index 4902bb4f..622c9ebb 100644 --- a/packages/core/src/tools/builders/BlockTuneBuilder.ts +++ b/packages/core/src/tools/facades/BlockTuneFacade.ts @@ -1,4 +1,4 @@ -import { BaseToolBuilder } from './BaseToolBuilder.js'; +import { BaseToolFacade } from './BaseToolFacade.js'; import type { BlockAPI, BlockTune as IBlockTune, BlockTuneConstructable } from '@editorjs/editorjs'; import { ToolType } from './ToolType.js'; // import type { BlockTuneData } from '@editorjs/editorjs'; @@ -7,7 +7,7 @@ import { ToolType } from './ToolType.js'; * Stub class for BlockTunes * @todo Implement */ -export class BlockTuneBuilder extends BaseToolBuilder { +export class BlockTuneFacade extends BaseToolFacade { /** * Tool type — Tune */ diff --git a/packages/core/src/tools/builders/InlineToolBuilder.ts b/packages/core/src/tools/facades/InlineToolFacade.ts similarity index 82% rename from packages/core/src/tools/builders/InlineToolBuilder.ts rename to packages/core/src/tools/facades/InlineToolFacade.ts index 3cb3b646..e1cd7922 100644 --- a/packages/core/src/tools/builders/InlineToolBuilder.ts +++ b/packages/core/src/tools/facades/InlineToolFacade.ts @@ -1,11 +1,11 @@ -import { BaseToolBuilder, InternalInlineToolSettings } from './BaseToolBuilder.js'; +import { BaseToolFacade, InternalInlineToolSettings } from './BaseToolFacade.js'; import type { InlineTool as IInlineTool, InlineToolConstructable } from '@editorjs/editorjs'; import { ToolType } from './ToolType.js'; /** * InlineTool object to work with Inline Tools constructables */ -export class InlineToolBuilder extends BaseToolBuilder { +export class InlineToolFacade extends BaseToolFacade { /** * Tool type — Inline */ diff --git a/packages/core/src/tools/builders/ToolType.ts b/packages/core/src/tools/facades/ToolType.ts similarity index 100% rename from packages/core/src/tools/builders/ToolType.ts rename to packages/core/src/tools/facades/ToolType.ts diff --git a/packages/core/src/tools/builders/ToolsCollection.ts b/packages/core/src/tools/facades/ToolsCollection.ts similarity index 62% rename from packages/core/src/tools/builders/ToolsCollection.ts rename to packages/core/src/tools/facades/ToolsCollection.ts index ef7849e5..d4a5e0e7 100644 --- a/packages/core/src/tools/builders/ToolsCollection.ts +++ b/packages/core/src/tools/facades/ToolsCollection.ts @@ -1,8 +1,8 @@ -import { type BlockToolBuilder } from './BlockToolBuilder.js'; -import { type InlineToolBuilder } from './InlineToolBuilder.js'; -import { type BlockTuneBuilder } from './BlockTuneBuilder.js'; +import { type BlockToolFacade } from './BlockToolFacade.js'; +import { type InlineToolFacade } from './InlineToolFacade.js'; +import { type BlockTuneFacade } from './BlockTuneFacade.js'; -export type ToolClass = BlockToolBuilder | InlineToolBuilder | BlockTuneBuilder; +export type ToolClass = BlockToolFacade | InlineToolFacade | BlockTuneFacade; /** * Class to store Editor Tools @@ -11,34 +11,34 @@ export class ToolsCollection extends Map { + public get blockTools(): ToolsCollection { const tools = Array .from(this.entries()) - .filter(([, tool]) => tool.isBlock()) as [string, BlockToolBuilder][]; + .filter(([, tool]) => tool.isBlock()) as [string, BlockToolFacade][]; - return new ToolsCollection(tools); + return new ToolsCollection(tools); } /** * Returns Inline Tools collection */ - public get inlineTools(): ToolsCollection { + public get inlineTools(): ToolsCollection { const tools = Array .from(this.entries()) - .filter(([, tool]) => tool.isInline()) as [string, InlineToolBuilder][]; + .filter(([, tool]) => tool.isInline()) as [string, InlineToolFacade][]; - return new ToolsCollection(tools); + return new ToolsCollection(tools); } /** * Returns Block Tunes collection */ - public get blockTunes(): ToolsCollection { + public get blockTunes(): ToolsCollection { const tools = Array .from(this.entries()) - .filter(([, tool]) => tool.isTune()) as [string, BlockTuneBuilder][]; + .filter(([, tool]) => tool.isTune()) as [string, BlockTuneFacade][]; - return new ToolsCollection(tools); + return new ToolsCollection(tools); } /** diff --git a/packages/core/src/tools/builders/ToolsFactory.ts b/packages/core/src/tools/facades/ToolsFactory.ts similarity index 81% rename from packages/core/src/tools/builders/ToolsFactory.ts rename to packages/core/src/tools/facades/ToolsFactory.ts index 6534d52a..a3be6e19 100644 --- a/packages/core/src/tools/builders/ToolsFactory.ts +++ b/packages/core/src/tools/facades/ToolsFactory.ts @@ -1,7 +1,7 @@ -import { InternalInlineToolSettings, InternalTuneSettings } from './BaseToolBuilder.js'; -import { InlineToolBuilder } from './InlineToolBuilder.js'; -import { BlockTuneBuilder } from './BlockTuneBuilder.js'; -import { BlockToolBuilder } from './BlockToolBuilder.js'; +import { InternalInlineToolSettings, InternalTuneSettings } from './BaseToolFacade.js'; +import { InlineToolFacade } from './InlineToolFacade.js'; +import { BlockTuneFacade } from './BlockTuneFacade.js'; +import { BlockToolFacade } from './BlockToolFacade.js'; // import type ApiModule from '../modules/api'; import type { ToolConstructable, @@ -11,7 +11,7 @@ import type { BlockTuneConstructable } from '@editorjs/editorjs'; -type ToolConstructor = typeof InlineToolBuilder | typeof BlockToolBuilder | typeof BlockTuneBuilder; +type ToolConstructor = typeof InlineToolFacade | typeof BlockToolFacade | typeof BlockTuneFacade; export type UnifiedToolConfig = Record & { class: ToolConstructable; @@ -56,7 +56,7 @@ export class ToolsFactory { * Returns Tool object based on it's type * @param name - tool name */ - public get(name: string): InlineToolBuilder | BlockToolBuilder | BlockTuneBuilder { + public get(name: string): InlineToolFacade | BlockToolFacade | BlockTuneFacade { const { class: constructable, isInternal = false, ...config } = this.config[name]; const Constructor = this.getConstructor(constructable); @@ -84,11 +84,11 @@ export class ToolsFactory { private getConstructor(constructable: ToolConstructable): ToolConstructor { switch (true) { case (constructable as InlineToolConstructable)[InternalInlineToolSettings.IsInline]: - return InlineToolBuilder; + return InlineToolFacade; case (constructable as BlockTuneConstructable)[InternalTuneSettings.IsTune]: - return BlockTuneBuilder; + return BlockTuneFacade; default: - return BlockToolBuilder; + return BlockToolFacade; } } } diff --git a/packages/core/src/tools/facades/index.ts b/packages/core/src/tools/facades/index.ts new file mode 100644 index 00000000..51f5f54e --- /dev/null +++ b/packages/core/src/tools/facades/index.ts @@ -0,0 +1,6 @@ +export * from './BlockToolFacade.js' +export * from './BlockTuneFacade.js' +export * from './InlineToolFacade.js' +export * from './ToolsFactory.js' +export * from './ToolsCollection.js' +export * from './ToolType.js' From cbeb0cd397385d96dddef321103591c8324e820e Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 00:14:27 +0300 Subject: [PATCH 3/8] 'Fix' lint --- packages/core/src/tools/ToolsManager.ts | 33 ++++++------ .../core/src/tools/facades/BaseToolFacade.ts | 54 +++++++++++++++---- .../core/src/tools/facades/BlockToolFacade.ts | 4 +- .../src/tools/facades/InlineToolFacade.ts | 2 +- .../core/src/tools/facades/ToolsFactory.ts | 22 ++++++-- packages/core/src/tools/facades/index.ts | 12 ++--- packages/core/src/ui/InlineToolbar/index.ts | 5 +- 7 files changed, 90 insertions(+), 42 deletions(-) diff --git a/packages/core/src/tools/ToolsManager.ts b/packages/core/src/tools/ToolsManager.ts index 683b3f7c..4b505ad3 100644 --- a/packages/core/src/tools/ToolsManager.ts +++ b/packages/core/src/tools/ToolsManager.ts @@ -7,17 +7,16 @@ import { ToolsCollection, ToolsFactory, UnifiedToolConfig -} from "./facades/index.js"; +} from './facades/index.js'; import { Paragraph } from './internal/block-tools/paragraph/index.js'; import type { EditorConfig, - ToolConfig, ToolConstructable, ToolSettings } from '@editorjs/editorjs'; -import BoldInlineTool from "./internal/inline-tools/bold/index.js"; -import ItalicInlineTool from "./internal/inline-tools/italic/index.js"; -import { InlineTool } from "@editorjs/sdk"; +import BoldInlineTool from './internal/inline-tools/bold/index.js'; +import ItalicInlineTool from './internal/inline-tools/italic/index.js'; +import { BlockToolConstructor, InlineTool, InlineToolConstructor } from '@editorjs/sdk'; /** * Works with tools @@ -85,15 +84,19 @@ export default class ToolsManager { this.#factory = new ToolsFactory(this.#config, editorConfig, {}); - void this.#prepareTools(editorConfig.tools); + void this.#prepareTools(); } - #prepareTools(config: ToolConfig): Promise { + /** + * Calls tools prepare method if it exists and adds tools to relevant collection (available or unavailable tools) + * @returns Promise + */ + #prepareTools(): Promise { const promiseQueue = new PromiseQueue(); Object.entries(this.#config).forEach(([toolName, config]) => { if (isFunction(config.class.prepare)) { - promiseQueue.add(async () => { + void promiseQueue.add(async () => { try { await config.class.prepare!({ toolName: toolName, @@ -107,7 +110,7 @@ export default class ToolsManager { * Some Tools validation */ const inlineToolRequiredMethods = ['render']; - const notImplementedMethods = inlineToolRequiredMethods.filter(method => !tool.create()[method as keyof InlineTool]); + const notImplementedMethods = inlineToolRequiredMethods.filter(method => tool.create()[method as keyof InlineTool] !== undefined); if (notImplementedMethods.length) { /** @@ -144,7 +147,7 @@ export default class ToolsManager { /** * Unify tools config - * @param config + * @param config - user's tools config */ #prepareConfig(config: EditorConfig['tools']): UnifiedToolConfig { const unifiedConfig: UnifiedToolConfig = {} as UnifiedToolConfig; @@ -197,24 +200,24 @@ export default class ToolsManager { * Returns internal tools * Includes Bold, Italic, Link and Paragraph */ - get #internalTools(): UnifiedToolConfig { + get #internalTools(): UnifiedToolConfig { return { paragraph: { /** * @todo solve problems with types */ - class: Paragraph as any, + class: Paragraph as unknown as BlockToolConstructor, inlineToolbar: true, isInternal: true, }, bold: { - class: BoldInlineTool as any, + class: BoldInlineTool as unknown as InlineToolConstructor, isInternal: true, }, italic: { - class: ItalicInlineTool as any, + class: ItalicInlineTool as unknown as InlineToolConstructor, isInternal: true, - } + }, }; } } diff --git a/packages/core/src/tools/facades/BaseToolFacade.ts b/packages/core/src/tools/facades/BaseToolFacade.ts index 64400210..1def6ef4 100644 --- a/packages/core/src/tools/facades/BaseToolFacade.ts +++ b/packages/core/src/tools/facades/BaseToolFacade.ts @@ -3,14 +3,14 @@ import type { API as ApiMethods, Tool, ToolConstructable as ToolConstructableV2, - ToolSettings, + ToolSettings } from '@editorjs/editorjs'; import { isFunction } from '@editorjs/helpers'; import { type BlockToolFacade } from './BlockToolFacade.js'; import { type InlineToolFacade } from './InlineToolFacade.js'; import { ToolType } from './ToolType.js'; import { type BlockTuneFacade } from './BlockTuneFacade.js'; -import { BlockTool, BlockToolConstructor, InlineTool, InlineToolConstructor } from "@editorjs/sdk"; +import type { BlockTool, BlockToolConstructor, InlineTool, InlineToolConstructor } from '@editorjs/sdk'; export type ToolConstructable = ToolConstructableV2 | BlockToolConstructor | InlineToolConstructor; @@ -107,25 +107,51 @@ export enum InternalTuneSettings { export type ToolOptions = Omit; +/** + * BlockToolFacade constructor options inteface + */ interface ConstructorOptions { + /** + * Tool name + */ name: string; + + /** + * Tool constructor function/class + */ constructable: ToolConstructable; + + /** + * Tool options + */ config: ToolOptions; + + /** + * Api methods for the Tool + */ api: ApiMethods; + + /** + * Is tool default + */ isDefault: boolean; + + /** + * Is tool internal + */ isInternal: boolean; + + /** + * Defualt placaholder for the Tol + */ defaultPlaceholder?: string | false; } /** * Base abstract class for Tools */ -export abstract class BaseToolFacade { - /** - * Tool type: Block, Inline or Tune - */ - public abstract type: Type; - +// eslint-disable-next-line @stylistic/type-generic-spacing +export abstract class BaseToolFacade { /** * Tool name specified in EditorJS config */ @@ -162,7 +188,13 @@ export abstract class BaseToolFacade * @param options.block - BlockAPI for current Block * @param options.readOnly - True if Editor is in read-only mode */ - public create({ data, block, readOnly, adapter }: { data: BlockToolData, block: BlockAPI, readOnly: boolean, adapter: BlockToolAdapter }): IBlockTool { + public create({ data, block, readOnly, adapter }: { data: BlockToolData; block: BlockAPI; readOnly: boolean; adapter: BlockToolAdapter }): IBlockTool { return new this.constructable({ adapter, data, diff --git a/packages/core/src/tools/facades/InlineToolFacade.ts b/packages/core/src/tools/facades/InlineToolFacade.ts index 8ff16822..d21c932d 100644 --- a/packages/core/src/tools/facades/InlineToolFacade.ts +++ b/packages/core/src/tools/facades/InlineToolFacade.ts @@ -7,7 +7,7 @@ import { ToolType } from './ToolType.js'; */ export class InlineToolFacade extends BaseToolFacade { /** - * Tool type — Inline + * Tool type for InlineToolFacade tools — Inline */ public type: ToolType.Inline = ToolType.Inline; diff --git a/packages/core/src/tools/facades/ToolsFactory.ts b/packages/core/src/tools/facades/ToolsFactory.ts index b6c303e6..9a6a8f73 100644 --- a/packages/core/src/tools/facades/ToolsFactory.ts +++ b/packages/core/src/tools/facades/ToolsFactory.ts @@ -1,4 +1,5 @@ -import type { BlockToolConstructor, InlineToolConstructor } from "@editorjs/sdk"; +/* eslint-disable jsdoc/informative-docs */ +import type { BlockToolConstructor, InlineToolConstructor } from '@editorjs/sdk'; import { InternalInlineToolSettings, InternalTuneSettings } from './BaseToolFacade.js'; import { InlineToolFacade } from './InlineToolFacade.js'; import { BlockTuneFacade } from './BlockTuneFacade.js'; @@ -15,7 +16,14 @@ import type { type ToolConstructor = typeof InlineToolFacade | typeof BlockToolFacade | typeof BlockTuneFacade; export type UnifiedToolConfig = Record & { + /** + * Tool constructor + */ class: ToolConstructable | BlockToolConstructor | InlineToolConstructor; + + /** + * Specifies if tool is internal + */ isInternal?: boolean; }>; @@ -31,6 +39,7 @@ export class ToolsFactory { /** * EditorJS API Module */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any private api: any; /** @@ -39,15 +48,18 @@ export class ToolsFactory { private editorConfig: EditorConfig; /** - * @param config - tools config - * @param editorConfig - EditorJS config - * @param api - EditorJS API module + * ToolsFactory + * @param config - unified tools config for user`s and internal tools + * @param editorConfig - full Editor.js configuration + * @param api - EditorJS module with all Editor methods */ constructor( config: UnifiedToolConfig, editorConfig: EditorConfig, + // eslint-disable-next-line @typescript-eslint/no-explicit-any api: any ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.api = api; this.config = config; this.editorConfig = editorConfig; @@ -63,6 +75,7 @@ export class ToolsFactory { const Constructor = this.getConstructor(constructable); // const isTune = constructable[InternalTuneSettings.IsTune]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return new Constructor({ name, constructable, @@ -75,6 +88,7 @@ export class ToolsFactory { /** * @todo implement api.getMethodsForTool */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); } diff --git a/packages/core/src/tools/facades/index.ts b/packages/core/src/tools/facades/index.ts index 51f5f54e..cd655e06 100644 --- a/packages/core/src/tools/facades/index.ts +++ b/packages/core/src/tools/facades/index.ts @@ -1,6 +1,6 @@ -export * from './BlockToolFacade.js' -export * from './BlockTuneFacade.js' -export * from './InlineToolFacade.js' -export * from './ToolsFactory.js' -export * from './ToolsCollection.js' -export * from './ToolType.js' +export * from './BlockToolFacade.js'; +export * from './BlockTuneFacade.js'; +export * from './InlineToolFacade.js'; +export * from './ToolsFactory.js'; +export * from './ToolsCollection.js'; +export * from './ToolType.js'; diff --git a/packages/core/src/ui/InlineToolbar/index.ts b/packages/core/src/ui/InlineToolbar/index.ts index 69453718..7e85fd1f 100644 --- a/packages/core/src/ui/InlineToolbar/index.ts +++ b/packages/core/src/ui/InlineToolbar/index.ts @@ -1,10 +1,9 @@ import type { InlineToolsAdapter } from '@editorjs/dom-adapters'; -import type { InlineTool, InlineToolsConfig } from '@editorjs/sdk'; import type { InlineToolName } from '@editorjs/model'; -import { type EditorJSModel, type TextRange, createInlineToolData, createInlineToolName, Index } from '@editorjs/model'; +import { type EditorJSModel, type TextRange, createInlineToolData, Index } from '@editorjs/model'; import { EventType } from '@editorjs/model'; import { make } from '@editorjs/dom'; -import { InlineToolFacade, ToolsCollection } from "../../tools/facades/index.js"; +import type { InlineToolFacade, ToolsCollection } from '../../tools/facades/index.js'; /** * Class determines, when inline toolbar should be rendered From 6b24e7be42510b3d1df2889c9d5d66b3112527d3 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 00:15:50 +0300 Subject: [PATCH 4/8] Add unsaved files --- packages/core/src/index.ts | 1 + packages/core/src/tools/facades/BaseToolFacade.ts | 5 ++++- packages/core/src/tools/facades/BlockToolFacade.ts | 12 +++++++----- packages/core/src/tools/facades/BlockTuneFacade.ts | 4 +++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c07e0913..c3bb674d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -65,6 +65,7 @@ export default class Core { * @param config - Editor configuration */ constructor(config: CoreConfig) { + // eslint-disable-next-line @typescript-eslint/no-magic-numbers this.#iocContainer = Container.of(Math.floor(Math.random() * 1e10).toString()); this.validateConfig(config); diff --git a/packages/core/src/tools/facades/BaseToolFacade.ts b/packages/core/src/tools/facades/BaseToolFacade.ts index 1def6ef4..dfd4a7d0 100644 --- a/packages/core/src/tools/facades/BaseToolFacade.ts +++ b/packages/core/src/tools/facades/BaseToolFacade.ts @@ -218,12 +218,15 @@ export abstract class BaseToolFacade { /** - * Tool type — Block + * Tool type for BlockToolFacade tools — Block */ public type: ToolType.Block = ToolType.Block; @@ -40,14 +40,16 @@ export class BlockToolFacade extends BaseToolFacade /** * Creates new Tool instance - * @param options - * @param options.data - Tool data + * @param options - Tool constructor options + * @param options.data - Tools data * @param options.block - BlockAPI for current Block * @param options.readOnly - True if Editor is in read-only mode */ + // eslint-disable-next-line jsdoc/require-jsdoc public create({ data, block, readOnly, adapter }: { data: BlockToolData; block: BlockAPI; readOnly: boolean; adapter: BlockToolAdapter }): IBlockTool { return new this.constructable({ adapter, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment data, block, readOnly, @@ -109,7 +111,7 @@ export class BlockToolFacade extends BaseToolFacade return userToolboxSettings.map((item, i) => { const toolToolboxEntry = toolToolboxSettings[i]; - if (toolToolboxEntry) { + if (toolToolboxEntry !== undefined) { return { ...toolToolboxEntry, ...item, @@ -146,7 +148,7 @@ export class BlockToolFacade extends BaseToolFacade * Returns enabled inline tools for Tool */ public get enabledInlineTools(): boolean | string[] { - return this.config[UserSettings.EnabledInlineTools] || false; + return this.config[UserSettings.EnabledInlineTools] ?? false; } /** diff --git a/packages/core/src/tools/facades/BlockTuneFacade.ts b/packages/core/src/tools/facades/BlockTuneFacade.ts index 622c9ebb..c29d8d4e 100644 --- a/packages/core/src/tools/facades/BlockTuneFacade.ts +++ b/packages/core/src/tools/facades/BlockTuneFacade.ts @@ -20,14 +20,16 @@ export class BlockTuneFacade extends BaseToolFacade { /** * Constructs new BlockTune instance from constructable - * @param data - Tune data + * @param data - Tunes data * @param block - Block API object */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any public create(data: any, block: BlockAPI): IBlockTune { return new this.constructable({ api: this.api, config: this.settings, block, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment data, }); } From 153d09e2f6aa963f3dd568f517a68961f84d3740 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 00:23:00 +0300 Subject: [PATCH 5/8] Fix comments --- packages/core/src/entities/UnifiedToolConfig.ts | 14 ++++++++++++++ packages/core/src/entities/index.ts | 1 + packages/core/src/tools/ToolsManager.ts | 4 ++-- packages/core/src/tools/facades/BlockToolFacade.ts | 12 +++++------- packages/core/src/tools/facades/ToolsFactory.ts | 14 +------------- 5 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 packages/core/src/entities/UnifiedToolConfig.ts diff --git a/packages/core/src/entities/UnifiedToolConfig.ts b/packages/core/src/entities/UnifiedToolConfig.ts new file mode 100644 index 00000000..7c999d2f --- /dev/null +++ b/packages/core/src/entities/UnifiedToolConfig.ts @@ -0,0 +1,14 @@ +import type { ToolSettings, ToolConstructable } from '@editorjs/editorjs'; +import type { BlockToolConstructor, InlineToolConstructor } from '@editorjs/sdk'; + +export type UnifiedToolConfig = Record & { + /** + * Tool constructor + */ + class: ToolConstructable | BlockToolConstructor | InlineToolConstructor; + + /** + * Specifies if tool is internal + */ + isInternal?: boolean; +}>; diff --git a/packages/core/src/entities/index.ts b/packages/core/src/entities/index.ts index 72f6900e..5c995404 100644 --- a/packages/core/src/entities/index.ts +++ b/packages/core/src/entities/index.ts @@ -1 +1,2 @@ export * from './Config.js'; +export * from './UnifiedToolConfig.js'; diff --git a/packages/core/src/tools/ToolsManager.ts b/packages/core/src/tools/ToolsManager.ts index 4b505ad3..6de1dea9 100644 --- a/packages/core/src/tools/ToolsManager.ts +++ b/packages/core/src/tools/ToolsManager.ts @@ -5,8 +5,7 @@ import { BlockToolFacade, BlockTuneFacade, InlineToolFacade, ToolsCollection, - ToolsFactory, - UnifiedToolConfig + ToolsFactory } from './facades/index.js'; import { Paragraph } from './internal/block-tools/paragraph/index.js'; import type { @@ -17,6 +16,7 @@ import type { import BoldInlineTool from './internal/inline-tools/bold/index.js'; import ItalicInlineTool from './internal/inline-tools/italic/index.js'; import { BlockToolConstructor, InlineTool, InlineToolConstructor } from '@editorjs/sdk'; +import { UnifiedToolConfig } from '../entities/index.js'; /** * Works with tools diff --git a/packages/core/src/tools/facades/BlockToolFacade.ts b/packages/core/src/tools/facades/BlockToolFacade.ts index 325d4e0f..1cd195f6 100644 --- a/packages/core/src/tools/facades/BlockToolFacade.ts +++ b/packages/core/src/tools/facades/BlockToolFacade.ts @@ -1,10 +1,9 @@ -import type { BlockToolAdapter } from '@editorjs/dom-adapters'; import { BaseToolFacade, InternalBlockToolSettings, UserSettings } from './BaseToolFacade.js'; import type { - BlockAPI, - BlockToolData, ConversionConfig, - PasteConfig, SanitizerConfig, ToolboxConfig, + PasteConfig, + SanitizerConfig, + ToolboxConfig, ToolboxConfigEntry } from '@editorjs/editorjs'; import { isEmpty, cacheable, isObject } from '@editorjs/helpers'; @@ -12,7 +11,7 @@ import { type InlineToolFacade } from './InlineToolFacade.js'; import { ToolType } from './ToolType.js'; import { type BlockTuneFacade } from './BlockTuneFacade.js'; import { ToolsCollection } from './ToolsCollection.js'; -import { BlockToolConstructor as BlockToolConstructable, BlockTool as IBlockTool } from '@editorjs/sdk'; +import { BlockToolConstructor as BlockToolConstructable, BlockToolConstructorOptions, BlockTool as IBlockTool } from '@editorjs/sdk'; /** * Class to work with Block tools constructables @@ -45,8 +44,7 @@ export class BlockToolFacade extends BaseToolFacade * @param options.block - BlockAPI for current Block * @param options.readOnly - True if Editor is in read-only mode */ - // eslint-disable-next-line jsdoc/require-jsdoc - public create({ data, block, readOnly, adapter }: { data: BlockToolData; block: BlockAPI; readOnly: boolean; adapter: BlockToolAdapter }): IBlockTool { + public create({ data, block, readOnly, adapter }: Pick): IBlockTool { return new this.constructable({ adapter, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment diff --git a/packages/core/src/tools/facades/ToolsFactory.ts b/packages/core/src/tools/facades/ToolsFactory.ts index 9a6a8f73..8dd38a1f 100644 --- a/packages/core/src/tools/facades/ToolsFactory.ts +++ b/packages/core/src/tools/facades/ToolsFactory.ts @@ -7,26 +7,14 @@ import { BlockToolFacade } from './BlockToolFacade.js'; // import type ApiModule from '../modules/api'; import type { ToolConstructable, - ToolSettings, EditorConfig, InlineToolConstructable, BlockTuneConstructable } from '@editorjs/editorjs'; +import type { UnifiedToolConfig } from '../../entities/UnifiedToolConfig.js'; type ToolConstructor = typeof InlineToolFacade | typeof BlockToolFacade | typeof BlockTuneFacade; -export type UnifiedToolConfig = Record & { - /** - * Tool constructor - */ - class: ToolConstructable | BlockToolConstructor | InlineToolConstructor; - - /** - * Specifies if tool is internal - */ - isInternal?: boolean; -}>; - /** * Factory to construct classes to work with tools */ From ff2fb1beda36da87148fbb1a62354bbf6de688a6 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 00:27:28 +0300 Subject: [PATCH 6/8] Remove log & add docs --- packages/core/src/index.ts | 3 +++ packages/core/src/tools/ToolsManager.ts | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c3bb674d..c0f5a6d7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -44,6 +44,9 @@ export default class Core { */ #caretAdapter: CaretAdapter; + /** + * Inversion of Control container for dependency injections + */ #iocContainer: ContainerInstance; /** diff --git a/packages/core/src/tools/ToolsManager.ts b/packages/core/src/tools/ToolsManager.ts index 6de1dea9..58f13793 100644 --- a/packages/core/src/tools/ToolsManager.ts +++ b/packages/core/src/tools/ToolsManager.ts @@ -129,8 +129,6 @@ export default class ToolsManager { } this.#availableTools.set(toolName, tool); - - console.log(this.#availableTools.entries()); } catch (e) { console.error(`Tool ${toolName} failed to prepare`, e); From 33a04bc72225a4896c9bbbd602fd15e372de5e52 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 00:43:34 +0300 Subject: [PATCH 7/8] Update packages/core/src/entities/UnifiedToolConfig.ts Co-authored-by: Peter --- packages/core/src/entities/UnifiedToolConfig.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/core/src/entities/UnifiedToolConfig.ts b/packages/core/src/entities/UnifiedToolConfig.ts index 7c999d2f..fd064753 100644 --- a/packages/core/src/entities/UnifiedToolConfig.ts +++ b/packages/core/src/entities/UnifiedToolConfig.ts @@ -1,6 +1,17 @@ import type { ToolSettings, ToolConstructable } from '@editorjs/editorjs'; import type { BlockToolConstructor, InlineToolConstructor } from '@editorjs/sdk'; +/** + * Users can pass tool's config in two ways: + * toolName: ToolClass + * or + * toolName: { + * class: ToolClass, + * // .. other options + * } + * + * This interface unifies these variants to a single format + */ export type UnifiedToolConfig = Record & { /** * Tool constructor From 1ab1a6a186d1b5edee4381be35d5738ce4135ce2 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 00:47:06 +0300 Subject: [PATCH 8/8] Fixes after review --- packages/core/src/entities/BlockTool.ts | 0 .../core/src/entities/UnifiedToolConfig.ts | 8 ++++--- packages/core/src/tools/ToolsManager.ts | 24 ++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) delete mode 100644 packages/core/src/entities/BlockTool.ts diff --git a/packages/core/src/entities/BlockTool.ts b/packages/core/src/entities/BlockTool.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/core/src/entities/UnifiedToolConfig.ts b/packages/core/src/entities/UnifiedToolConfig.ts index fd064753..f2c38247 100644 --- a/packages/core/src/entities/UnifiedToolConfig.ts +++ b/packages/core/src/entities/UnifiedToolConfig.ts @@ -5,10 +5,10 @@ import type { BlockToolConstructor, InlineToolConstructor } from '@editorjs/sdk' * Users can pass tool's config in two ways: * toolName: ToolClass * or - * toolName: { + * toolName: { * class: ToolClass, - * // .. other options - * } + * // .. other options + * } * * This interface unifies these variants to a single format */ @@ -20,6 +20,8 @@ export type UnifiedToolConfig = Record & { /** * Specifies if tool is internal + * + * Internal tools set it to true, external tools omit it */ isInternal?: boolean; }>; diff --git a/packages/core/src/tools/ToolsManager.ts b/packages/core/src/tools/ToolsManager.ts index 58f13793..73e2d835 100644 --- a/packages/core/src/tools/ToolsManager.ts +++ b/packages/core/src/tools/ToolsManager.ts @@ -25,9 +25,24 @@ import { UnifiedToolConfig } from '../entities/index.js'; */ @Service() export default class ToolsManager { + /** + * ToolsFactory instance + */ #factory: ToolsFactory; + + /** + * Unified config with internal and internal tools + */ #config: UnifiedToolConfig; + + /** + * Tools available for use + */ #availableTools = new ToolsCollection(); + + /** + * Tools loaded but unavailable for use + */ #unavailableTools = new ToolsCollection(); /** @@ -84,20 +99,23 @@ export default class ToolsManager { this.#factory = new ToolsFactory(this.#config, editorConfig, {}); - void this.#prepareTools(); + void this.prepareTools(); } /** * Calls tools prepare method if it exists and adds tools to relevant collection (available or unavailable tools) * @returns Promise */ - #prepareTools(): Promise { + public async prepareTools(): Promise { const promiseQueue = new PromiseQueue(); Object.entries(this.#config).forEach(([toolName, config]) => { if (isFunction(config.class.prepare)) { void promiseQueue.add(async () => { try { + /** + * TypeScript doesn't get type guard here, so non-null assertion is used + */ await config.class.prepare!({ toolName: toolName, config: config, @@ -140,7 +158,7 @@ export default class ToolsManager { } }); - return promiseQueue.completed; + await promiseQueue.completed; } /**