diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index af8ad28..e503697 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,8 +1,17 @@ import { CollaborationManager } from '@editorjs/collaboration-manager'; +import type { ToolSettings } from '@editorjs/editorjs/types/tools/index'; import { type DocumentId, EditorJSModel, EventType } from '@editorjs/model'; import type { ContainerInstance } from 'typedi'; import { Container } from 'typedi'; -import { CoreEventType, EventBus, UiComponentType } from '@editorjs/sdk'; +import { + type BlockToolConstructor, + CoreEventType, + EventBus, + type InlineToolConstructor, + UiComponentType +} from '@editorjs/sdk'; +import { Paragraph } from './tools/internal/block-tools/paragraph/index'; +import type { ExtendedToolSettings } from './tools/ToolsFactory'; import { composeDataFromVersion2 } from './utils/composeDataFromVersion2.js'; import ToolsManager from './tools/ToolsManager.js'; import { CaretAdapter, FormattingAdapter } from '@editorjs/dom-adapters'; @@ -69,7 +78,7 @@ export default class Core { // eslint-disable-next-line @typescript-eslint/no-magic-numbers this.#iocContainer = Container.of(Math.floor(Math.random() * 1e10).toString()); - this.validateConfig(config); + this.#validateConfig(config); this.#config = config as CoreConfigValidated; @@ -119,16 +128,44 @@ export default class Core { eventBus.addEventListener(`core:${CoreEventType.Redo}`, () => { this.#collaborationManager.redo(); }); + + // @ts-expect-error - weird TS error, will resolve later + this.use(Paragraph); } /** - * Initialize and injects Plugin into the container + * Injects Tool constructor and it's config into the container + * @param tool + * @param config + */ + public use(tool: BlockToolConstructor | InlineToolConstructor, config?: Omit): Core; + /** + * Injects Plugin into the container to initialize on Editor's init * @param plugin - allows to pass any implementation of editor plugins */ - public use(plugin: EditorjsPluginConstructor): Core { - const pluginType = plugin.type; - - this.#iocContainer.set(pluginType, plugin); + public use(plugin: EditorjsPluginConstructor): Core; + /** + * Overloaded method to register Editor.js Plugins/Tools/etc + * @param pluginOrTool - entity to register + * @param toolConfig - entity configuration + */ + public use( + pluginOrTool: BlockToolConstructor | InlineToolConstructor | EditorjsPluginConstructor, + toolConfig?: Omit + ): Core { + const pluginType = pluginOrTool.type; + + switch (pluginType) { + case 'tool': + this.#iocContainer.set({ + id: pluginType, + multiple: true, + value: [pluginOrTool, toolConfig], + }); + break; + default: + this.#iocContainer.set(pluginType, pluginOrTool); + } return this; } @@ -136,27 +173,34 @@ export default class Core { /** * Initializes the core */ - public initialize(): void { - const { blocks } = composeDataFromVersion2(this.#config.data ?? { blocks: [] }); + public async initialize(): Promise { + try { + const { blocks } = composeDataFromVersion2(this.#config.data ?? { blocks: [] }); - this.initializePlugins(); + this.#initializePlugins(); - this.#toolsManager.prepareTools() - .then(() => { - this.#model.initializeDocument({ blocks }); - }) - .then(() => { - this.#collaborationManager.connect(); - }) - .catch((error) => { - console.error('Editor.js initialization failed', error); - }); + await this.#initializeTools(); + + this.#model.initializeDocument({ blocks }); + this.#collaborationManager.connect(); + } catch (error) { + console.error('Editor.js initialization failed', error); + } + } + + /** + * Initalizes loaded tools + */ + async #initializeTools(): Promise { + const tools = this.#iocContainer.getMany<[ BlockToolConstructor | InlineToolConstructor, ExtendedToolSettings]>('tool'); + + return this.#toolsManager.prepareTools(tools); } /** * Initialize all registered UI plugins */ - private initializePlugins(): void { + #initializePlugins(): void { /** * Get all registered plugin types from the container */ @@ -166,7 +210,7 @@ export default class Core { const plugin = this.#iocContainer.get(pluginType); if (plugin !== undefined && typeof plugin === 'function') { - this.initializePlugin(plugin); + this.#initializePlugin(plugin); } } } @@ -175,7 +219,7 @@ export default class Core { * Create instance of plugin * @param plugin - Plugin constructor to initialize */ - private initializePlugin(plugin: EditorjsPluginConstructor): void { + #initializePlugin(plugin: EditorjsPluginConstructor): void { const eventBus = this.#iocContainer.get(EventBus); const api = this.#iocContainer.get(EditorAPI); @@ -190,7 +234,7 @@ export default class Core { * Validate configuration * @param config - Editor configuration */ - private validateConfig(config: CoreConfig): void { + #validateConfig(config: CoreConfig): void { if (config.holder === undefined) { const holder = document.getElementById(DEFAULT_HOLDER_ID); diff --git a/packages/core/src/tools/ToolsFactory.ts b/packages/core/src/tools/ToolsFactory.ts index e6db40f..eee2b7e 100644 --- a/packages/core/src/tools/ToolsFactory.ts +++ b/packages/core/src/tools/ToolsFactory.ts @@ -11,12 +11,19 @@ import type { ToolConstructable, EditorConfig, InlineToolConstructable, - BlockTuneConstructable + BlockTuneConstructable, ToolSettings } from '@editorjs/editorjs'; -// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents type ToolConstructor = typeof InlineToolFacade | typeof BlockToolFacade | typeof BlockTuneFacade; +export type ExtendedToolSettings = ToolSettings & { + /** + * Flag shows if a Tool is an internal tool + * @todo do we need this distinction any more? + */ + isInternal: boolean; +}; + /** * Factory to construct classes to work with tools */ @@ -24,18 +31,23 @@ export class ToolsFactory { /** * Tools configuration specified by user */ - private config: UnifiedToolConfig; + #config: UnifiedToolConfig; /** * EditorJS API Module */ - private api: EditorAPI; + #api: EditorAPI; /** * EditorJS configuration */ - private editorConfig: EditorConfig; + #editorConfig: EditorConfig; + + /** + * Map of tool settings + */ + #toolsSettings = new Map(); /** * ToolsFactory @@ -49,20 +61,38 @@ export class ToolsFactory { // eslint-disable-next-line @typescript-eslint/no-explicit-any api: any ) { - this.api = api; - this.config = config; - this.editorConfig = editorConfig; + this.#api = api; + this.#config = config; + this.#editorConfig = editorConfig; + } + + /** + * Register tools in the factory + * @param tools - tools to register in the factory + */ + public setTools(tools: [InlineToolConstructor | BlockToolConstructor, ExtendedToolSettings][]): void { + tools.forEach(([tool, settings]) => { + this.#toolsSettings.set(tool.name, { + ...settings, + class: tool, + }); + }); } /** * Returns Tool object based on it's type * @param name - tool name */ - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents public get(name: string): InlineToolFacade | BlockToolFacade | BlockTuneFacade { - const { class: constructable, isInternal = false, ...config } = this.config[name]; + const toolSettings = this.#toolsSettings.get(name); + + if (!toolSettings) { + throw new Error(`Tool ${name} is not registered`); + } + + const { class: constructable, isInternal = false, ...config } = toolSettings; - const Constructor = this.getConstructor(constructable); + const Constructor = this.#getConstructor(constructable!); // const isTune = constructable[InternalTuneSettings.IsTune]; return new Constructor({ @@ -71,8 +101,8 @@ export class ToolsFactory { config, api: {}, // api: this.api.getMethodsForTool(name, isTune), - isDefault: name === this.editorConfig.defaultBlock, - defaultPlaceholder: this.editorConfig.placeholder, + isDefault: name === this.#editorConfig.defaultBlock, + defaultPlaceholder: this.#editorConfig.placeholder, isInternal, /** * @todo implement api.getMethodsForTool @@ -85,8 +115,7 @@ export class ToolsFactory { * Find appropriate Tool object constructor for Tool constructable * @param constructable - Tools constructable */ - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - private getConstructor(constructable: ToolConstructable | BlockToolConstructor | InlineToolConstructor): ToolConstructor { + #getConstructor(constructable: ToolConstructable | BlockToolConstructor | InlineToolConstructor): ToolConstructor { switch (true) { case (constructable as InlineToolConstructable)[InternalInlineToolSettings.IsInline]: return InlineToolFacade; diff --git a/packages/core/src/tools/ToolsManager.ts b/packages/core/src/tools/ToolsManager.ts index bc4b5a0..da740e7 100644 --- a/packages/core/src/tools/ToolsManager.ts +++ b/packages/core/src/tools/ToolsManager.ts @@ -5,7 +5,7 @@ import type { import 'reflect-metadata'; import { deepMerge, isFunction, isObject, PromiseQueue } from '@editorjs/helpers'; import { Inject, Service } from 'typedi'; -import { ToolsFactory } from './ToolsFactory.js'; +import { type ExtendedToolSettings, ToolsFactory } from './ToolsFactory.js'; import { Paragraph } from './internal/block-tools/paragraph/index.js'; import type { EditorConfig, @@ -33,8 +33,6 @@ import LinkInlineTool from './internal/inline-tools/link/index.js'; */ @Service() export default class ToolsManager { - #tools: EditorConfig['tools']; - /** * ToolsFactory instance */ @@ -122,9 +120,9 @@ export default class ToolsManager { /** * Calls tools prepare method if it exists and adds tools to relevant collection (available or unavailable tools) - * @returns Promise + * @param tools - tools to prepare and their settings */ - public async prepareTools(): Promise { + public async prepareTools(tools: [InlineToolConstructor | BlockToolConstructor, ExtendedToolSettings][]): Promise { const promiseQueue = new PromiseQueue(); const setToAvailableToolsCollection = (toolName: string, tool: ToolFacadeClass): void => { @@ -135,15 +133,20 @@ export default class ToolsManager { })); }; - Object.entries(this.#config).forEach(([toolName, config]) => { - if (isFunction(config.class.prepare)) { + this.#factory.setTools(tools); + + tools.forEach(([toolConstructor, config]) => { + const toolName = toolConstructor.name; + + // eslint-disable-next-line @typescript-eslint/unbound-method + if (isFunction(toolConstructor.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, + await toolConstructor.prepare!({ + toolName, config: config, }); diff --git a/packages/core/src/tools/internal/block-tools/paragraph/index.ts b/packages/core/src/tools/internal/block-tools/paragraph/index.ts index 4c0bea7..67303e8 100644 --- a/packages/core/src/tools/internal/block-tools/paragraph/index.ts +++ b/packages/core/src/tools/internal/block-tools/paragraph/index.ts @@ -26,6 +26,10 @@ export type ParagraphConfig = ToolConfig<{ * Base text block tool */ export class Paragraph implements BlockTool { + public static type = 'tool'; + + public static name = 'paragraph'; + /** * Adapter for linking block data with the DOM */ diff --git a/packages/sdk/src/entities/BlockTool.ts b/packages/sdk/src/entities/BlockTool.ts index 7fb8499..32f268d 100644 --- a/packages/sdk/src/entities/BlockTool.ts +++ b/packages/sdk/src/entities/BlockTool.ts @@ -1,4 +1,8 @@ -import type { BlockTool as BlockToolVersion2, BlockToolConstructable as BlockToolConstructableV2, ToolConfig } from '@editorjs/editorjs'; +import type { + BlockTool as BlockToolVersion2, + BlockToolConstructable as BlockToolConstructableV2, + ToolConfig +} from '@editorjs/editorjs'; import type { BlockToolConstructorOptions as BlockToolConstructorOptionsVersion2 } from '@editorjs/editorjs'; import type { ValueSerialized } from '@editorjs/model'; import type { BlockToolAdapter } from './BlockToolAdapter.js'; @@ -11,7 +15,6 @@ export interface BlockToolConstructorOptions< * Data structure describing the tool's input/output data */ Data extends BlockToolData = BlockToolData, - /** * User-end configuration for the tool */ @@ -46,7 +49,6 @@ export type BlockTool< */ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any Data extends BlockToolData = any, - /** * User-end configuration for the tool * @@ -59,7 +61,21 @@ export type BlockTool< /** * Block Tool constructor class */ -export type BlockToolConstructor = BlockToolConstructableV2 & (new (options: BlockToolConstructorOptions) => BlockTool); +export type BlockToolConstructor< + /** + * Data structure describing the tool's input/output data + */ + Data extends BlockToolData = BlockToolData, + /** + * User-end configuration for the tool + */ + Config extends ToolConfig = ToolConfig +> = BlockToolConstructableV2 & (new (options: BlockToolConstructorOptions) => BlockTool) & { + /** + * Property specifies that the class is a Tool + */ + type: 'tool'; +}; /** * Data structure describing the tool's input/output data diff --git a/packages/sdk/src/entities/EventBus/events/core/SelectionChangedCoreEvent.ts b/packages/sdk/src/entities/EventBus/events/core/SelectionChangedCoreEvent.ts index e2595b0..8f2059e 100644 --- a/packages/sdk/src/entities/EventBus/events/core/SelectionChangedCoreEvent.ts +++ b/packages/sdk/src/entities/EventBus/events/core/SelectionChangedCoreEvent.ts @@ -11,7 +11,6 @@ export interface SelectionChangedCoreEventPayload { /** * Updated caret index */ - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents readonly index: Index | null; /** diff --git a/packages/sdk/src/entities/InlineTool.ts b/packages/sdk/src/entities/InlineTool.ts index 6e8dd3f..7a46c70 100644 --- a/packages/sdk/src/entities/InlineTool.ts +++ b/packages/sdk/src/entities/InlineTool.ts @@ -47,7 +47,7 @@ export interface ActionsElementWithOptions { element: HTMLElement; /** - * Oprions of custom toolbar behaviour + * Options of custom toolbar behaviour */ toolbarOptions?: ToolbarOptions; } @@ -68,14 +68,14 @@ export interface InlineTool extends Omit * @todo support options: InlineToolConstructableOptions * Inline Tool constructor class */ -export type InlineToolConstructor = InlineToolConstructableV2 & (new () => InlineTool); +export type InlineToolConstructor = InlineToolConstructableV2 & (new () => InlineTool) & { + /** + * Property specifies the class is a Tool + */ + type: 'tool'; +};