Skip to content

Commit

Permalink
Add BlockManager and EditorUI classes (#85)
Browse files Browse the repository at this point in the history
* Add BlockManager and EditorUI classes

* Add JSDocs

* Add try catch for block.render() call
  • Loading branch information
gohabereg authored Aug 29, 2024
1 parent 46615ca commit 1c8dcf2
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 79 deletions.
128 changes: 128 additions & 0 deletions packages/core/src/BlockManager.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
}
}
85 changes: 6 additions & 79 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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 });
}

Expand Down Expand Up @@ -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<void> {
/**
* @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<Record<string, unknown>>;
}, 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';
74 changes: 74 additions & 0 deletions packages/core/src/ui/Editor/index.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
}

2 comments on commit 1c8dcf2

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for ./packages/model

St.
Category Percentage Covered / Total
🟢 Statements 100% 753/753
🟢 Branches 99.5% 201/202
🟢 Functions 98.37% 181/184
🟢 Lines 100% 725/725

Test suite run success

389 tests passing in 24 suites.

Report generated by 🧪jest coverage report action from 1c8dcf2

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for ./packages/collaboration-manager

St.
Category Percentage Covered / Total
🟢 Statements 86.11% 62/72
🟡 Branches 62.5% 15/24
🟢 Functions 100% 10/10
🟢 Lines 86.11% 62/72

Test suite run success

6 tests passing in 1 suite.

Report generated by 🧪jest coverage report action from 1c8dcf2

Please sign in to comment.