Skip to content

Add BlockManager and EditorUI classes #85

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
}
}
}
Loading