From 1a1308d9f8ccb8f5bd1fdca36cf716ce88d0b747 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 02:01:43 +0300 Subject: [PATCH 1/3] Add BlockManager and EditorUI classes --- packages/core/src/BlockManager.ts | 76 ++++++++++++++++++ packages/core/src/index.ts | 77 +------------------ .../core/src/ui/InlineToolbar/Editor/index.ts | 42 ++++++++++ 3 files changed, 121 insertions(+), 74 deletions(-) create mode 100644 packages/core/src/BlockManager.ts create mode 100644 packages/core/src/ui/InlineToolbar/Editor/index.ts diff --git a/packages/core/src/BlockManager.ts b/packages/core/src/BlockManager.ts new file mode 100644 index 00000000..12fea355 --- /dev/null +++ b/packages/core/src/BlockManager.ts @@ -0,0 +1,76 @@ +import { BlockAddedEvent, BlockRemovedEvent, EditorJSModel, EventType, ModelEvents } from '@editorjs/model'; +import 'reflect-metadata'; +import { Service } from 'typedi'; +import { EditorUI } from './ui/InlineToolbar/Editor/index.js'; +import { BlockToolAdapter, CaretAdapter } from '@editorjs/dom-adapters'; +import ToolsManager from './tools/ToolsManager.js'; +import { BlockAPI } from '@editorjs/editorjs'; + +@Service() +export class BlockManager { + #model: EditorJSModel; + #editorUI: EditorUI; + #caretAdapter: CaretAdapter; + #toolsManager: ToolsManager; + + constructor( + model: EditorJSModel, + editorUI: EditorUI, + caretAdapter: CaretAdapter, + toolsManager: ToolsManager + ) { + this.#model = model; + this.#editorUI = editorUI; + this.#caretAdapter = caretAdapter; + this.#toolsManager = toolsManager; + + this.#model.addEventListener(EventType.Changed, event => this.#handleModelUpdate(event)); + } + + #handleModelUpdate(event: ModelEvents): void { + switch (true) { + case event instanceof BlockAddedEvent: + this.#handleBlockAddedEvent(event); + break; + case event instanceof BlockRemovedEvent: + this.#handleBlockRemovedEvent(event); + break; + default: + } + } + + async #handleBlockAddedEvent(event: BlockAddedEvent) { + const { index, data } = event.detail; + + if (index.blockIndex === undefined) { + throw new Error('Block index should be defined. Probably something wrong with the Editor Model. Please, report this issue'); + } + + const blockToolAdapter = new BlockToolAdapter(this.#model, this.#caretAdapter, index.blockIndex); + + const tool = this.#toolsManager.blockTools.get(event.detail.data.name); + + if (!tool) { + throw new Error(`Block Tool ${event.detail.data.name} not found`); + } + + const block = tool.create({ + adapter: blockToolAdapter, + data: data, + block: {} as BlockAPI, + readOnly: false, + }); + + this.#editorUI.addBlock(await block.render(), index.blockIndex); + } + + #handleBlockRemovedEvent(event: BlockRemovedEvent) { + const { index } = event.detail; + + if (index.blockIndex === undefined) { + throw new Error('Block index should be defined. Probably something wrong with the Editor Model. Please, report this issue'); + } + + this.#editorUI.removeBlock(index.blockIndex); + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c0f5a6d7..df95e5e3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,6 +9,7 @@ import type { BlockAPI, BlockToolData } from '@editorjs/editorjs'; import { InlineToolbar } from './ui/InlineToolbar/index.js'; import type { CoreConfigValidated } from './entities/Config.js'; import type { BlockTool, CoreConfig } from '@editorjs/sdk'; +import { BlockManager } from './BlockManager.js'; /** * If no holder is provided via config, the editor will be appended to the element with this id @@ -82,8 +83,6 @@ export default class Core { this.#iocContainer.set(EditorJSModel, this.#model); - this.#model.addEventListener(EventType.Changed, (event: ModelEvents) => this.handleModelUpdate(event)); - this.#toolsManager = this.#iocContainer.get(ToolsManager); this.#caretAdapter = new CaretAdapter(this.#config.holder, this.#model); @@ -95,6 +94,8 @@ export default class Core { this.#inlineToolbar = new InlineToolbar(this.#model, this.#inlineToolsAdapter, this.#toolsManager.inlineTools, this.#config.holder); this.#iocContainer.set(InlineToolbar, this.#inlineToolbar); + this.#iocContainer.get(BlockManager); + this.#model.initializeDocument({ blocks }); } @@ -123,78 +124,6 @@ export default class Core { } } } - - /** - * When model emits block-added event, add an actual block to the editor - * @param event - Any model event - */ - private handleModelUpdate(event: ModelEvents): void { - if (event instanceof BlockAddedEvent === false) { - return; - } - - void this.handleBlockAdded(event); - } - - /** - * Insert block added to the model to the DOM - * @param event - Event containing information about the added block - */ - private async handleBlockAdded(event: BlockAddedEvent): Promise { - /** - * @todo add batch rendering to improve performance on large documents - */ - const index = event.detail.index; - - if (index.blockIndex === undefined) { - throw new Error('Block index should be defined. Probably something wrong with the Editor Model. Please, report this issue'); - } - - const blockToolAdapter = new BlockToolAdapter(this.#model, this.#caretAdapter, index.blockIndex); - - const block = this.createBlock({ - name: event.detail.data.name, - data: event.detail.data.data, - }, blockToolAdapter); - - const blockEl = await block.render(); - - /** - * @todo add block to the correct position - */ - this.#config.holder.appendChild(blockEl); - } - - /** - * Create Block Tools instance - * @param blockOptions - options to pass to the tool - * @param blockToolAdapter - adapter for linking block and model - */ - private createBlock({ name, data }: { - /** - * Tool name - */ - name: string; - /** - * Saved block data - */ - data: BlockToolData>; - }, blockToolAdapter: BlockToolAdapter): BlockTool { - 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, - block: {} as BlockAPI, - readOnly: false, - }); - - return block; - } } export * from './entities/index.js'; diff --git a/packages/core/src/ui/InlineToolbar/Editor/index.ts b/packages/core/src/ui/InlineToolbar/Editor/index.ts new file mode 100644 index 00000000..5ff86ca3 --- /dev/null +++ b/packages/core/src/ui/InlineToolbar/Editor/index.ts @@ -0,0 +1,42 @@ +import 'reflect-metadata'; +import { Inject, Service } from 'typedi'; +import { CoreConfigValidated } from '../../../entities'; + +@Service() +export class EditorUI { + #holder: HTMLElement; + #blocks: HTMLElement[] = []; + + constructor(@Inject('EditorConfig') config: CoreConfigValidated) { + this.#holder = config.holder; + } + + public render(): void { + // will add UI to holder element + } + + public addBlock(blockElement: HTMLElement, index: number): void { + this.#validateIndex(index); + + if (index < this.#blocks.length) { + this.#blocks[index].insertAdjacentElement('beforebegin', blockElement); + this.#blocks.splice(index, 0, blockElement); + } else { + this.#holder.appendChild(blockElement); + this.#blocks.push(blockElement); + } + } + + public removeBlock(index: number): void { + this.#validateIndex(index); + + this.#blocks[index].remove(); + this.#blocks.splice(index, 1); + } + + #validateIndex(index: number): void { + if (index < 0 || index > this.#blocks.length) { + throw new Error('Index out of bounds'); + } + } +} From 973d28d8440a46c4ea6aced07f94da7b9b8a154e Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 02:19:32 +0300 Subject: [PATCH 2/3] Add JSDocs --- packages/core/src/BlockManager.ts | 60 ++++++++++++++++--- packages/core/src/index.ts | 4 +- .../ui/{InlineToolbar => }/Editor/index.ts | 34 ++++++++++- 3 files changed, 88 insertions(+), 10 deletions(-) rename packages/core/src/ui/{InlineToolbar => }/Editor/index.ts (54%) diff --git a/packages/core/src/BlockManager.ts b/packages/core/src/BlockManager.ts index 12fea355..2d40f343 100644 --- a/packages/core/src/BlockManager.ts +++ b/packages/core/src/BlockManager.ts @@ -1,18 +1,47 @@ import { BlockAddedEvent, BlockRemovedEvent, EditorJSModel, EventType, ModelEvents } from '@editorjs/model'; import 'reflect-metadata'; import { Service } from 'typedi'; -import { EditorUI } from './ui/InlineToolbar/Editor/index.js'; +import { EditorUI } from './ui/Editor/index.js'; import { BlockToolAdapter, CaretAdapter } from '@editorjs/dom-adapters'; import ToolsManager from './tools/ToolsManager.js'; import { BlockAPI } from '@editorjs/editorjs'; +/** + * BlocksManager is responsible for + * - handling block adding and removing events + * - updating the Model blocks data on user actions + */ @Service() -export class BlockManager { +export class BlocksManager { + /** + * Editor's Document Model instance to get and update blocks data + */ #model: EditorJSModel; + + /** + * Editor's UI class instance to add and remove blocks to the UI + */ #editorUI: EditorUI; + + /** + * Caret Adapter instance + * Required here to create BlockToolAdapter + */ #caretAdapter: CaretAdapter; + + /** + * Tools manager instance to get block tools + */ #toolsManager: ToolsManager; + /** + * BlocksManager constructor + * All parameters are injected thorugh the IoC container + * @param model - Editor's Document Model instance + * @param editorUI - Editor's UI class instance + * @param caretAdapter - Caret Adapter instance + * @param toolsManager - Tools manager instance + */ constructor( model: EditorJSModel, editorUI: EditorUI, @@ -27,10 +56,15 @@ export class BlockManager { this.#model.addEventListener(EventType.Changed, event => this.#handleModelUpdate(event)); } + /** + * Handles model update events + * Filters only BlockAddedEvent and BlockRemovedEvent + * @param event - Model update event + */ #handleModelUpdate(event: ModelEvents): void { switch (true) { case event instanceof BlockAddedEvent: - this.#handleBlockAddedEvent(event); + void this.#handleBlockAddedEvent(event); break; case event instanceof BlockRemovedEvent: this.#handleBlockRemovedEvent(event); @@ -39,11 +73,18 @@ export class BlockManager { } } - async #handleBlockAddedEvent(event: BlockAddedEvent) { + /** + * Handles BlockAddedEvent + * - creates BlockTool instance + * - renders its content + * - calls UI module to render the block + * @param event - BlockAddedEvent + */ + async #handleBlockAddedEvent(event: BlockAddedEvent): Promise { const { index, data } = event.detail; if (index.blockIndex === undefined) { - throw new Error('Block index should be defined. Probably something wrong with the Editor Model. Please, report this issue'); + throw new Error('[BlockManager] Block index should be defined. Probably something wrong with the Editor Model. Please, report this issue'); } const blockToolAdapter = new BlockToolAdapter(this.#model, this.#caretAdapter, index.blockIndex); @@ -51,7 +92,7 @@ export class BlockManager { const tool = this.#toolsManager.blockTools.get(event.detail.data.name); if (!tool) { - throw new Error(`Block Tool ${event.detail.data.name} not found`); + throw new Error(`[BlockManager] Block Tool ${event.detail.data.name} not found`); } const block = tool.create({ @@ -64,7 +105,12 @@ export class BlockManager { this.#editorUI.addBlock(await block.render(), index.blockIndex); } - #handleBlockRemovedEvent(event: BlockRemovedEvent) { + /** + * Handles BlockRemovedEvent + * - callse UI module to remove the block + * @param event - BlockRemovedEvent + */ + #handleBlockRemovedEvent(event: BlockRemovedEvent): void { const { index } = event.detail; if (index.blockIndex === undefined) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index df95e5e3..6f1b805d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,7 +9,7 @@ import type { BlockAPI, BlockToolData } from '@editorjs/editorjs'; import { InlineToolbar } from './ui/InlineToolbar/index.js'; import type { CoreConfigValidated } from './entities/Config.js'; import type { BlockTool, CoreConfig } from '@editorjs/sdk'; -import { BlockManager } from './BlockManager.js'; +import { BlocksManager } from './BlockManager.js'; /** * If no holder is provided via config, the editor will be appended to the element with this id @@ -94,7 +94,7 @@ export default class Core { this.#inlineToolbar = new InlineToolbar(this.#model, this.#inlineToolsAdapter, this.#toolsManager.inlineTools, this.#config.holder); this.#iocContainer.set(InlineToolbar, this.#inlineToolbar); - this.#iocContainer.get(BlockManager); + this.#iocContainer.get(BlocksManager); this.#model.initializeDocument({ blocks }); } diff --git a/packages/core/src/ui/InlineToolbar/Editor/index.ts b/packages/core/src/ui/Editor/index.ts similarity index 54% rename from packages/core/src/ui/InlineToolbar/Editor/index.ts rename to packages/core/src/ui/Editor/index.ts index 5ff86ca3..e020a184 100644 --- a/packages/core/src/ui/InlineToolbar/Editor/index.ts +++ b/packages/core/src/ui/Editor/index.ts @@ -1,20 +1,44 @@ import 'reflect-metadata'; import { Inject, Service } from 'typedi'; -import { CoreConfigValidated } from '../../../entities'; +import { CoreConfigValidated } from '../../entities/index.js'; +/** + * Editor's main UI renderer for HTML environment + * - renders the editor UI + * - adds and removes blocks on the page + * - handles user UI interactions + */ @Service() export class EditorUI { + /** + * Editor holder element + */ #holder: HTMLElement; + /** + * Elements of the blocks added to the editor + */ #blocks: HTMLElement[] = []; + /** + * EditorUI constructor method + * @param config - EditorJS validated configuration + */ constructor(@Inject('EditorConfig') config: CoreConfigValidated) { this.#holder = config.holder; } + /** + * Renders the editor UI + */ public render(): void { // will add UI to holder element } + /** + * Renders block's content on the page + * @param blockElement - block HTML element to add to the page + * @param index - index where to add a block at + */ public addBlock(blockElement: HTMLElement, index: number): void { this.#validateIndex(index); @@ -27,6 +51,10 @@ export class EditorUI { } } + /** + * Removes block from the page + * @param index - index where to remove block at + */ public removeBlock(index: number): void { this.#validateIndex(index); @@ -34,6 +62,10 @@ export class EditorUI { this.#blocks.splice(index, 1); } + /** + * Validates index to be in bounds of the blocks array + * @param index - index to validate + */ #validateIndex(index: number): void { if (index < 0 || index > this.#blocks.length) { throw new Error('Index out of bounds'); From 657da65475bef0c1b56aa19491deb502938963da Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 02:22:38 +0300 Subject: [PATCH 3/3] Add try catch for block.render() call --- packages/core/src/BlockManager.ts | 8 +++++++- packages/core/src/index.ts | 8 +++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/core/src/BlockManager.ts b/packages/core/src/BlockManager.ts index 2d40f343..46044a4b 100644 --- a/packages/core/src/BlockManager.ts +++ b/packages/core/src/BlockManager.ts @@ -102,7 +102,13 @@ export class BlocksManager { readOnly: false, }); - this.#editorUI.addBlock(await block.render(), index.blockIndex); + try { + const blockElement = await block.render(); + + this.#editorUI.addBlock(blockElement, index.blockIndex); + } catch (error) { + console.error(`[BlockManager] Block Tool ${event.detail.data.name} failed to render`, error); + } } /** diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6f1b805d..1031f20f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,14 +1,12 @@ -import type { ModelEvents } from '@editorjs/model'; -import { BlockAddedEvent, EditorJSModel, EventType } from '@editorjs/model'; +import { EditorJSModel } from '@editorjs/model'; import type { ContainerInstance } from 'typedi'; import { Container } from 'typedi'; import { composeDataFromVersion2 } from './utils/composeDataFromVersion2.js'; import ToolsManager from './tools/ToolsManager.js'; -import { BlockToolAdapter, CaretAdapter, InlineToolsAdapter } from '@editorjs/dom-adapters'; -import type { BlockAPI, BlockToolData } from '@editorjs/editorjs'; +import { CaretAdapter, InlineToolsAdapter } from '@editorjs/dom-adapters'; import { InlineToolbar } from './ui/InlineToolbar/index.js'; import type { CoreConfigValidated } from './entities/Config.js'; -import type { BlockTool, CoreConfig } from '@editorjs/sdk'; +import type { CoreConfig } from '@editorjs/sdk'; import { BlocksManager } from './BlockManager.js'; /**