From 1c8dcf276d31915d986a0b4d2a7b26d0d2515ef2 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 30 Aug 2024 02:25:12 +0300 Subject: [PATCH] Add BlockManager and EditorUI classes (#85) * Add BlockManager and EditorUI classes * Add JSDocs * Add try catch for block.render() call --- packages/core/src/BlockManager.ts | 128 +++++++++++++++++++++++++++ packages/core/src/index.ts | 85 ++---------------- packages/core/src/ui/Editor/index.ts | 74 ++++++++++++++++ 3 files changed, 208 insertions(+), 79 deletions(-) create mode 100644 packages/core/src/BlockManager.ts create mode 100644 packages/core/src/ui/Editor/index.ts diff --git a/packages/core/src/BlockManager.ts b/packages/core/src/BlockManager.ts new file mode 100644 index 0000000..46044a4 --- /dev/null +++ b/packages/core/src/BlockManager.ts @@ -0,0 +1,128 @@ +import { BlockAddedEvent, BlockRemovedEvent, EditorJSModel, EventType, ModelEvents } from '@editorjs/model'; +import 'reflect-metadata'; +import { Service } from 'typedi'; +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 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, + 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)); + } + + /** + * Handles model update events + * Filters only BlockAddedEvent and BlockRemovedEvent + * @param event - Model update event + */ + #handleModelUpdate(event: ModelEvents): void { + switch (true) { + case event instanceof BlockAddedEvent: + void this.#handleBlockAddedEvent(event); + break; + case event instanceof BlockRemovedEvent: + this.#handleBlockRemovedEvent(event); + break; + default: + } + } + + /** + * 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('[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); + + const tool = this.#toolsManager.blockTools.get(event.detail.data.name); + + if (!tool) { + throw new Error(`[BlockManager] Block Tool ${event.detail.data.name} not found`); + } + + const block = tool.create({ + adapter: blockToolAdapter, + data: data, + block: {} as BlockAPI, + readOnly: false, + }); + + 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); + } + } + + /** + * Handles BlockRemovedEvent + * - callse UI module to remove the block + * @param event - BlockRemovedEvent + */ + #handleBlockRemovedEvent(event: BlockRemovedEvent): void { + 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 c0f5a6d..1031f20 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,14 +1,13 @@ -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'; /** * If no holder is provided via config, the editor will be appended to the element with this id @@ -82,8 +81,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 +92,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(BlocksManager); + this.#model.initializeDocument({ blocks }); } @@ -123,78 +122,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/Editor/index.ts b/packages/core/src/ui/Editor/index.ts new file mode 100644 index 0000000..e020a18 --- /dev/null +++ b/packages/core/src/ui/Editor/index.ts @@ -0,0 +1,74 @@ +import 'reflect-metadata'; +import { Inject, Service } from 'typedi'; +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); + + 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); + } + } + + /** + * Removes block from the page + * @param index - index where to remove block at + */ + public removeBlock(index: number): void { + this.#validateIndex(index); + + this.#blocks[index].remove(); + 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'); + } + } +}