From de7d7c3c5b8b29a78498fb29ebbd1aa6e6a0a239 Mon Sep 17 00:00:00 2001 From: gohabereg Date: Thu, 14 May 2026 00:20:04 +0100 Subject: [PATCH 1/5] Add API methods to update the model contents --- .../src/api/BlocksAPI.integration.spec.ts | 6 +- packages/core/src/api/BlocksAPI.spec.ts | 85 +++++++-- packages/core/src/api/BlocksAPI.ts | 85 ++++++++- .../src/api/DocumentAPI/DocumentAPI.spec.ts | 3 + .../core/src/api/DocumentAPI/DocumentAPI.ts | 14 +- packages/core/src/api/SelectionAPI.spec.ts | 15 +- packages/core/src/api/SelectionAPI.ts | 49 ++++- packages/core/src/api/TextAPI.ts | 176 ++++++++++++++++++ packages/core/src/api/index.ts | 7 + packages/core/src/index.ts | 1 - .../src/BlockToolAdapter/index.ts | 92 +++++---- .../dom-adapters/src/CaretAdapter/index.ts | 26 ++- .../src/FormattingAdapter/index.ts | 20 +- .../dom-adapters/src/InputsRegistry/index.ts | 12 +- packages/dom-adapters/src/index.ts | 12 +- packages/dom-adapters/src/tokens.ts | 5 + packages/model/src/EditorJSModel.ts | 6 +- .../model/src/EventBus/types/EventType.ts | 4 +- .../model/src/entities/BlockNode/index.ts | 2 +- .../src/entities/EditorDocument/index.ts | 4 +- packages/sdk/src/api/BlocksAPI.ts | 54 +++++- packages/sdk/src/api/DocumentAPI.ts | 8 +- packages/sdk/src/api/EditorAPI.ts | 6 + packages/sdk/src/api/SelectionAPI.ts | 20 +- packages/sdk/src/api/TextAPI.ts | 96 ++++++++++ packages/sdk/src/api/index.ts | 1 + packages/sdk/src/entities/BlockToolAdapter.ts | 66 +++---- .../sdk/src/entities/EditorjsAdapterPlugin.ts | 17 +- packages/sdk/src/entities/EditorjsPlugin.ts | 6 +- 29 files changed, 712 insertions(+), 186 deletions(-) create mode 100644 packages/core/src/api/TextAPI.ts create mode 100644 packages/sdk/src/api/TextAPI.ts diff --git a/packages/core/src/api/BlocksAPI.integration.spec.ts b/packages/core/src/api/BlocksAPI.integration.spec.ts index 82619fb6..fd23bbab 100644 --- a/packages/core/src/api/BlocksAPI.integration.spec.ts +++ b/packages/core/src/api/BlocksAPI.integration.spec.ts @@ -82,7 +82,11 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { config ); - blocksAPI = new BlocksAPI(blocksManager, config); + blocksAPI = new BlocksAPI( + blocksManager, + config, + new EditorJSModel('userId', { identifier: 'documentId' }) + ); }); afterEach(() => { diff --git a/packages/core/src/api/BlocksAPI.spec.ts b/packages/core/src/api/BlocksAPI.spec.ts index cc502c1d..a0acb4b2 100644 --- a/packages/core/src/api/BlocksAPI.spec.ts +++ b/packages/core/src/api/BlocksAPI.spec.ts @@ -14,7 +14,14 @@ jest.unstable_mockModule('../components/BlockManager', () => ({ })), })); +jest.unstable_mockModule('@editorjs/model', () => ({ + EditorJSModel: jest.fn(), + createBlockId: jest.fn(id => id), + createDataKey: jest.fn(key => key), +})); + const { BlocksManager } = await import('../components/BlockManager'); +const { EditorJSModel } = await import('@editorjs/model'); const { BlocksAPI } = await import('./BlocksAPI.js'); import type { CoreConfigValidated } from '@editorjs/sdk'; @@ -26,7 +33,11 @@ describe('BlocksAPI', () => { describe('.clear()', () => { it('should call blocksManager.clear', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); api.clear(); @@ -36,10 +47,16 @@ describe('BlocksAPI', () => { describe('.render()', () => { it('should call blocksManager.render with provided document', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); - const doc = { identifier: 'doc', + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); + const doc = { + identifier: 'doc', blocks: [], - properties: {} }; + properties: {}, + }; api.render(doc); @@ -49,7 +66,11 @@ describe('BlocksAPI', () => { describe('.delete()', () => { it('should pass explicit index to blocksManager.deleteBlock', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); api.delete(2); @@ -57,7 +78,11 @@ describe('BlocksAPI', () => { }); it('should pass undefined when index is omitted', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); api.delete(); @@ -67,7 +92,11 @@ describe('BlocksAPI', () => { describe('.move()', () => { it('should call blocksManager.move with toIndex and fromIndex', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); api.move(3, 1); @@ -77,7 +106,11 @@ describe('BlocksAPI', () => { describe('.getBlocksCount()', () => { it('should return blocksManager.blocksCount', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); // @ts-expect-error - need to assign a value to check the method blocksManager.blocksCount = 5; @@ -88,10 +121,16 @@ describe('BlocksAPI', () => { describe('.insertMany()', () => { it('should pass blocks and index to blocksManager.insertMany', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); - const blocks = [{ name: 'a', - data: {} }]; + const blocks = [{ + name: 'a', + data: {}, + }]; api.insertMany(blocks as never, 4); @@ -99,10 +138,16 @@ describe('BlocksAPI', () => { }); it('should pass undefined index to blocksManager.insertMany when omitted', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); - const blocks = [{ name: 'a', - data: {} }]; + const blocks = [{ + name: 'a', + data: {}, + }]; api.insertMany(blocks as never); @@ -112,7 +157,11 @@ describe('BlocksAPI', () => { describe('.insert()', () => { it('should use defaults and pass payload to blocksManager.insert', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); api.insert(); @@ -125,7 +174,11 @@ describe('BlocksAPI', () => { }); it('should pass provided params to blocksManager.insert and ignore compatibility args', () => { - const api = new BlocksAPI(blocksManager, { defaultBlock } as CoreConfigValidated); + const api = new BlocksAPI( + blocksManager, + { defaultBlock } as CoreConfigValidated, + new EditorJSModel('userId', { identifier: 'docId' }) + ); api.insert( 'header', diff --git a/packages/core/src/api/BlocksAPI.ts b/packages/core/src/api/BlocksAPI.ts index a29055e1..328c0c3d 100644 --- a/packages/core/src/api/BlocksAPI.ts +++ b/packages/core/src/api/BlocksAPI.ts @@ -5,7 +5,14 @@ import { BlocksManager } from '../components/BlockManager.js'; import { BlockToolData } from '@editorjs/editorjs'; import { CoreConfigValidated } from '@editorjs/sdk'; import { BlocksAPI as BlocksApiInterface } from '@editorjs/sdk'; -import { type BlockNodeInit, type EditorDocumentSerialized } from '@editorjs/model'; +import { + BlockId, BlockIndexOrId, + type BlockNodeInit, + createBlockId, createDataKey, + type EditorDocumentSerialized, + EditorJSModel, + TextNodeSerialized, ValueSerialized +} from '@editorjs/model'; /** * Blocks API @@ -23,17 +30,25 @@ export class BlocksAPI implements BlocksApiInterface { */ #config: CoreConfigValidated; + /** + * Model instance + */ + #model: EditorJSModel; + /** * BlocksAPI class constructor * @param blocksManager - BlocksManager instance to work with blocks * @param config - EditorJS configuration + * @param model - EditorJS model instance */ constructor( blocksManager: BlocksManager, - @inject(TOKENS.EditorConfig) config: CoreConfigValidated + @inject(TOKENS.EditorConfig) config: CoreConfigValidated, + model: EditorJSModel ) { this.#blocksManager = blocksManager; this.#config = config; + this.#model = model; } /** @@ -113,4 +128,70 @@ export class BlocksAPI implements BlocksApiInterface { focus, }); }; + + /** + * Returns block's index by its id + * @param id - block id to get index for + */ + public getIndexById(id: string): number { + return this.#model.getBlockIndexById(createBlockId(id)); + } + + /** + * Returns block id by its index + * @param index - block index to get id for + */ + public getIdByIndex(index: number): BlockId | undefined { + return this.#model.getBlockId(index); + } + + /** + * Returns serialized data for provided data key + * @param blockIndexOrId - index or identifier of the block + * @param dataKey - data key to get serialized data for + */ + public getData(blockIndexOrId: number | string, dataKey: string): TextNodeSerialized | ValueSerialized | undefined { + /** + * Need an explicit cast here because TS doesn't pass generic for some reason + */ + return this.#model.getDataNode(blockIndexOrId as BlockIndexOrId, dataKey) as TextNodeSerialized | ValueSerialized | undefined; + } + + /** + * Creates data node with the given key + * @param indexOrId - index or id of the block + * @param dataKey - data key of the new data node + * @param [initialData] - optional initial data + */ + public createData( + indexOrId: number | string, + dataKey: string, + initialData?: TextNodeSerialized | ValueSerialized + ): void { + this.#model.createDataNode( + this.#config.userId, + indexOrId as BlockIndexOrId, + dataKey, + initialData + ); + } + + /** + * Removes data by the data key + * @param blockIndexOrId - index or identifier of the block + * @param dataKey - data key of the node to remove + */ + public removeData(blockIndexOrId: string | number, dataKey: string): void { + this.#model.removeDataNode(this.#config.userId, blockIndexOrId as BlockIndexOrId, dataKey); + } + + /** + * Updates value by the given key + * @param blockIndexOrId - index or identifier of the block + * @param dataKey - key of the data node to update + * @param value - new value + */ + public updateValue(blockIndexOrId: string | number, dataKey: string, value: V): void { + this.#model.updateValue(this.#config.userId, blockIndexOrId as BlockIndexOrId, createDataKey(dataKey), value); + } } diff --git a/packages/core/src/api/DocumentAPI/DocumentAPI.spec.ts b/packages/core/src/api/DocumentAPI/DocumentAPI.spec.ts index fa3b340a..943dea0f 100644 --- a/packages/core/src/api/DocumentAPI/DocumentAPI.spec.ts +++ b/packages/core/src/api/DocumentAPI/DocumentAPI.spec.ts @@ -8,6 +8,9 @@ jest.unstable_mockModule('@editorjs/model', () => { return { EditorJSModel, + EventType: { + Changed: 'update', + }, }; }); diff --git a/packages/core/src/api/DocumentAPI/DocumentAPI.ts b/packages/core/src/api/DocumentAPI/DocumentAPI.ts index 4cefd28a..a3151501 100644 --- a/packages/core/src/api/DocumentAPI/DocumentAPI.ts +++ b/packages/core/src/api/DocumentAPI/DocumentAPI.ts @@ -1,6 +1,6 @@ import 'reflect-metadata'; -import { type EditorDocumentSerialized, EditorJSModel } from '@editorjs/model'; +import { type EditorDocumentSerialized, EditorJSModel, EventType, ModelEvents } from '@editorjs/model'; import { DocumentAPI as DocumentApiInterface } from '@editorjs/sdk'; import { injectable } from 'inversify'; @@ -30,4 +30,16 @@ export class DocumentAPI implements DocumentApiInterface { public get data(): EditorDocumentSerialized { return this.#model.serialized; } + + /** + * Registers model's update callback. Returns a cleanup function + * @param callback - callback called on model update + */ + public onUpdate(callback: (event: ModelEvents) => void): () => void { + this.#model.addEventListener(EventType.Changed, callback); + + return () => { + this.#model.removeEventListener(EventType.Changed, callback); + }; + } } diff --git a/packages/core/src/api/SelectionAPI.spec.ts b/packages/core/src/api/SelectionAPI.spec.ts index 04d4be4d..88ee3fe4 100644 --- a/packages/core/src/api/SelectionAPI.spec.ts +++ b/packages/core/src/api/SelectionAPI.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { jest } from '@jest/globals'; +import type { CoreConfigValidated } from '@editorjs/sdk'; // Mock dependencies before importing the module under test jest.unstable_mockModule('../components/SelectionManager', () => ({ @@ -9,12 +10,16 @@ jest.unstable_mockModule('../components/SelectionManager', () => ({ })); jest.unstable_mockModule('@editorjs/model', () => ({ + EditorJSModel: jest.fn(), createInlineToolName: jest.fn((name: string) => `inline:${name}`), + EventType: { + CaretManagerUpdated: 'update', + }, })); const { SelectionAPI } = await import('./SelectionAPI.js'); const { SelectionManager } = await import('../components/SelectionManager'); -const { createInlineToolName } = await import('@editorjs/model'); +const { EditorJSModel, createInlineToolName } = await import('@editorjs/model'); describe('SelectionAPI', () => { // @ts-expect-error - mock object @@ -22,9 +27,13 @@ describe('SelectionAPI', () => { describe('.applyInlineToolForCurrentSelection()', () => { it('should convert toolName and delegate to SelectionManager', () => { - const api = new SelectionAPI(selectionManager as never as InstanceType); + const api = new SelectionAPI( + selectionManager as unknown as InstanceType, + new EditorJSModel('userId', { identifier: 'docId' }), + {} as unknown as CoreConfigValidated + ); - api.applyInlineToolForCurrentSelection('bold', { level: 1 } as never); + api.applyInlineToolForCurrentSelection('bold', { level: 1 }); expect(createInlineToolName).toHaveBeenCalledWith('bold'); expect(selectionManager.applyInlineToolForCurrentSelection).toHaveBeenCalledWith('inline:bold', { level: 1 }); diff --git a/packages/core/src/api/SelectionAPI.ts b/packages/core/src/api/SelectionAPI.ts index 4e139b26..06b88022 100644 --- a/packages/core/src/api/SelectionAPI.ts +++ b/packages/core/src/api/SelectionAPI.ts @@ -1,10 +1,11 @@ import 'reflect-metadata'; -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; import { SelectionManager } from '../components/SelectionManager.js'; -import { createInlineToolName } from '@editorjs/model'; -import { InlineToolFormatData } from '@editorjs/sdk'; +import { Caret, CaretManagerEvents, createInlineToolName, EditorJSModel, EventType } from '@editorjs/model'; +import { CoreConfigValidated, InlineToolFormatData } from '@editorjs/sdk'; import { SelectionAPI as SelectionApiInterface } from '@editorjs/sdk'; +import { TOKENS } from '../tokens'; /** * Selection API class @@ -13,16 +14,24 @@ import { SelectionAPI as SelectionApiInterface } from '@editorjs/sdk'; @injectable() export class SelectionAPI implements SelectionApiInterface { #selectionManager: SelectionManager; + #model: EditorJSModel; + #config: CoreConfigValidated; /** * SelectionAPI class constructor - * @param selectionManager - SelectionManager instance to work with selection and inline fotmatting + * @param selectionManager - SelectionManager instance to work with selection and inline formatting + * @param model - EditorJS model instance + * @param config - EditorJS validated config */ constructor( - selectionManager: SelectionManager + selectionManager: SelectionManager, + model: EditorJSModel, + @inject(TOKENS.EditorConfig) config: CoreConfigValidated ) { this.#selectionManager = selectionManager; - }; + this.#model = model; + this.#config = config; + } /** * Applies passed inline tool to the current selection @@ -32,4 +41,32 @@ export class SelectionAPI implements SelectionApiInterface { public applyInlineToolForCurrentSelection(toolName: string, data?: InlineToolFormatData): void { this.#selectionManager.applyInlineToolForCurrentSelection(createInlineToolName(toolName), data); } + + /** + * Registers a callback for CaretManager updates. Returns a cleanup function + * @param callback - callback for CaretManager updates + */ + public onCaretUpdate(callback: (event: CaretManagerEvents) => void): () => void { + this.#model.addEventListener(EventType.CaretManagerUpdated, callback); + + return () => { + this.#model.removeEventListener(EventType.CaretManagerUpdated, callback); + }; + } + + /** + * Creates a new caret for a user + * @param userId - user id. If not provided, creates for current user + */ + public createCaret(userId = this.#config.userId): Caret { + return this.#model.createCaret(userId); + } + + /** + * Returns user caret + * @param userId - user id. If not provided, returns for current user + */ + public getCaret(userId = this.#config.userId): Caret | undefined { + return this.#model.getCaret(userId); + } } diff --git a/packages/core/src/api/TextAPI.ts b/packages/core/src/api/TextAPI.ts new file mode 100644 index 00000000..ae3bd15a --- /dev/null +++ b/packages/core/src/api/TextAPI.ts @@ -0,0 +1,176 @@ +import { + BlockIndexOrId, + createDataKey, + createInlineToolData, + createInlineToolName, + EditorJSModel, InlineFragment +} from '@editorjs/model'; +import type { CoreConfigValidated } from '@editorjs/sdk'; +import { inject, injectable } from 'inversify'; +import { TOKENS } from '../tokens'; +import { TextAPI as TextAPIInterface } from '@editorjs/sdk'; + +/** + * Text API to work with the text content of the document + */ +@injectable() +export class TextAPI implements TextAPIInterface { + /** + * EditorJS Model instance + */ + #model: EditorJSModel; + + /** + * Validated Editor's Config + */ + #config: CoreConfigValidated; + + /** + * Class constructor function + * @param config - Editor's validated config + * @param model - EditorJS model instance + */ + constructor( + @inject(TOKENS.EditorConfig) config: CoreConfigValidated, + model: EditorJSModel + ) { + this.#model = model; + this.#config = config; + } + + /** + * Inserts text at a given position + * @param text - new text to insert + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - start offset + */ + public insert( + text: string, + blockIndexOrId: number | string, + dataKey: string, + start?: number + ): void { + this.#model.insertText( + this.#config.userId, + blockIndexOrId as BlockIndexOrId, + createDataKey(dataKey), + text, + start + ); + } + + /** + * Removes text from a given range. Returns removed text + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - range start + * @param end - range end + */ + public remove( + blockIndexOrId: number | string, + dataKey: string, + start?: number, + end?: number + ): string { + return this.#model.removeText( + this.#config.userId, + blockIndexOrId as BlockIndexOrId, + createDataKey(dataKey), + start, + end + ); + } + + /** + * Formats the given range + * @param tool - tool to apply + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - range start + * @param end - range end + * @param data - optional tool's data + */ + public format( + tool: string, + blockIndexOrId: number | string, + dataKey: string, + start: number, + end: number, + data?: Record + ): void { + this.#model.format( + this.#config.userId, + blockIndexOrId as BlockIndexOrId, + createDataKey(dataKey), + createInlineToolName(tool), + start, + end, + data !== undefined ? createInlineToolData(data) : undefined + ); + } + + /** + * Unformats the given range + * @param tool - tool to remove + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - range start + * @param end - range end + */ + public unformat( + tool: string, + blockIndexOrId: number | string, + dataKey: string, + start: number, + end: number + ): void { + this.#model.unformat( + this.#config.userId, + blockIndexOrId as BlockIndexOrId, + createDataKey(dataKey), + createInlineToolName(tool), + start, + end + ); + } + + /** + * Returns applied inline fragments for a given range + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - range start + * @param end - range end + * @param tool - optional filter tool. If provided, will return only fragments of the given tool + */ + public getFragments( + blockIndexOrId: number | string, + dataKey: string, + start?: number, + end?: number, + tool?: string + ): InlineFragment[] { + return this.#model.getFragments( + blockIndexOrId as BlockIndexOrId, + createDataKey(dataKey), + start, + end, + tool !== undefined ? createInlineToolName(tool) : undefined + ); + } + + /** + * Returns text content of the text node + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + */ + public get( + blockIndexOrId: number | string, + dataKey: string + ): string { + return this.#model.getText( + blockIndexOrId as BlockIndexOrId, + createDataKey(dataKey) + ); + } +} diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index b1b5c28a..3909cc60 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -4,6 +4,7 @@ import { EditorAPI as EditorApiInterface } from '@editorjs/sdk'; import { BlocksAPI } from './BlocksAPI.js'; import { SelectionAPI } from './SelectionAPI.js'; import { DocumentAPI } from './DocumentAPI/index.js'; +import { TextAPI } from './TextAPI.js'; /** * Class gathers all Editor's APIs @@ -27,4 +28,10 @@ export class EditorAPI implements EditorApiInterface { */ @inject(DocumentAPI) public document!: DocumentAPI; + + /** + * Text API instance to work with the text content of the document + */ + @inject(TextAPI) + public text!: TextAPI; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 002d8555..a28bb6c4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -240,7 +240,6 @@ export default class Core { const api = ctx.get(EditorAPI); return new Adapter({ - model: this.#model, config: this.#config, api, eventBus, diff --git a/packages/dom-adapters/src/BlockToolAdapter/index.ts b/packages/dom-adapters/src/BlockToolAdapter/index.ts index 070b0814..9958bbaa 100644 --- a/packages/dom-adapters/src/BlockToolAdapter/index.ts +++ b/packages/dom-adapters/src/BlockToolAdapter/index.ts @@ -1,7 +1,5 @@ import { createDataKey, - type DataKey, - EditorJSModel, EventAction, IndexBuilder, type ModelEvents, @@ -11,7 +9,7 @@ import { import type { BeforeInputUIEvent, BeforeInputUIEventPayload, - CoreConfig + CoreConfig, EditorAPI } from '@editorjs/sdk'; import { BeforeInputUIEventName, BlockToolAdapter, EventBus } from '@editorjs/sdk'; @@ -52,28 +50,31 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { */ #beforeInputListener: EventListener; + #api: EditorAPI; + /** * BlockToolAdapter constructor * @param config - Editor's config - * @param model - EditorJSModel instance * @param eventBus - Editor EventBus instance * @param caretAdapter - CaretAdapter instance * @param formattingAdapter - needed to render formatted text * @param registry - shared inputs registry + * @param api - EditorJS API */ constructor( @inject(TOKENS.EditorConfig) config: Required, - model: EditorJSModel, eventBus: EventBus, caretAdapter: CaretAdapter, formattingAdapter: FormattingAdapter, - registry: InputsRegistry + registry: InputsRegistry, + @inject(TOKENS.EditorAPI) api: EditorAPI ) { - super(config, model, eventBus); + super(config, api, eventBus); this.#caretAdapter = caretAdapter; this.#formattingAdapter = formattingAdapter; this.#inputsRegistry = registry; + this.#api = api; /** * @param event - BeforeInputEvent @@ -109,12 +110,10 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { /** * Attaches or re-attaches input to the model using key. * Each input registered in the InputRegistry — the shared registry of all inputs in the editor - * @param keyRaw - tools data key to attach input to + * @param key - tools data key to attach input to * @param input - input element */ - public setInput(keyRaw: string, input: HTMLElement | undefined): void { - const key = createDataKey(keyRaw); - + public setInput(key: string, input: HTMLElement | undefined): void { if (input === undefined) { this.#inputsRegistry.unregister(this.blockId, key); @@ -135,8 +134,8 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { return; } - const value = this.model.getText(this.blockId, key); - const fragments = this.model.getFragments(this.blockId, key); + const value = this.#api.text.get(this.blockId, key); + const fragments = this.#api.text.getFragments(this.blockId, key); this.#inputsRegistry.register(this.blockId, key, input); @@ -150,8 +149,8 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { /** * Returns the (dataKey → element) map for this block from the shared registry. */ - get #attachedInputs(): Map { - return this.#inputsRegistry.getBlockInputs(this.blockId) ?? new Map(); + get #attachedInputs(): Map { + return this.#inputsRegistry.getBlockInputs(this.blockId) ?? new Map(); } /** @@ -159,7 +158,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { * Public getter for all attached inputs. * Can be used to loop through all inputs to find a particular input(s) */ - public getAttachedInputs(): Map { + public getAttachedInputs(): Map { return this.#attachedInputs; } @@ -168,7 +167,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { * Allows access to a particular input by key * @param key - data key of the input */ - public getInput(key: DataKey): HTMLElement | undefined { + public getInput(key: string): HTMLElement | undefined { return this.#attachedInputs.get(key); } @@ -177,7 +176,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { * @param targetRanges - ranges to find inputs for * @returns array of tuples containing data key and input element */ - #findInputsByRanges(targetRanges: StaticRange[]): [DataKey, HTMLElement][] { + #findInputsByRanges(targetRanges: StaticRange[]): [string, HTMLElement][] { return Array.from(this.#attachedInputs.entries()).filter(([_, input]) => { return targetRanges.some((range) => { const startContainer = range.startContainer; @@ -274,7 +273,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { */ #handleDeleteInContentEditable( input: HTMLElement, - key: DataKey, + key: string, range: StaticRange, isRestoreCaretToTheEnd: boolean = false ): void { @@ -282,7 +281,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { * Middle block in a cross-input selection: remove the whole block, not the same as removeText(0, length). */ if (isInputInBetweenSelection(input, range)) { - this.model.removeBlock(this.config.userId, this.blockId); + this.#api.blocks.delete(this.blockId); return; } @@ -310,7 +309,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { } const [start, end] = clipped; - const removedText = this.model.removeText(this.config.userId, this.blockId, key, start, end); + const removedText = this.#api.text.remove(this.blockId, key, start, end); let newCaretIndex: number | null = null; @@ -335,8 +334,8 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { if (newCaretIndex !== null) { this.#caretAdapter.updateIndex( new IndexBuilder() - .addBlockIndex(this.model.getBlockIndexById(this.blockId)) - .addDataKey(key) + .addBlockIndex(this.#api.blocks.getIndexById(this.blockId)) + .addDataKey(createDataKey(key)) .addTextRange([newCaretIndex, newCaretIndex]) .build() ); @@ -351,7 +350,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { * @param input - input element * @param key - data key input is attached to */ - #handleBeforeInputEvent(payload: BeforeInputUIEventPayload, input: HTMLElement, key: DataKey): void { + #handleBeforeInputEvent(payload: BeforeInputUIEventPayload, input: HTMLElement, key: string): void { const { data, inputType, targetRanges } = payload; const range = targetRanges[0]; const isFormattingInputType = inputType.startsWith('format'); @@ -376,7 +375,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { if (data !== undefined && input.contains(range.startContainer)) { start = getAbsoluteRangeOffset(input, range.startContainer, range.startOffset); - this.model.insertText(this.config.userId, this.blockId, key, data, start); + this.#api.text.insert(data, this.blockId, key, start); } break; } @@ -388,7 +387,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { if (data !== undefined && input.contains(range.startContainer)) { start = getAbsoluteRangeOffset(input, range.startContainer, range.startOffset); - this.model.insertText(this.config.userId, this.blockId, key, data, start); + this.#api.text.insert(data, this.blockId, key, start); } break; } @@ -442,12 +441,12 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { * @param start - start index of the split * @param end - end index of the selected range */ - #handleSplit(key: DataKey, start: number, end: number): void { - const currentBlockIndex = this.model.getBlockIndexById(this.blockId); - const currentValue = this.model.getText(this.blockId, key); + #handleSplit(key: string, start: number, end: number): void { + const currentBlockIndex = this.#api.blocks.getIndexById(this.blockId); + const currentValue = this.#api.text.get(this.blockId, key); const newValueAfter = currentValue.slice(end); - const relatedFragments = this.model.getFragments(this.blockId, key, end, currentValue.length); + const relatedFragments = this.#api.text.getFragments(this.blockId, key, end, currentValue.length); /** * Fragment ranges bounds should be decreased by end index, because end is the index of the first character of the new block @@ -457,20 +456,17 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { fragment.range[1] -= end; }); - this.model.removeText(this.config.userId, this.blockId, key, start, currentValue.length); - this.model.addBlock( - this.config.userId, + this.#api.text.remove(this.blockId, key, start, currentValue.length); + this.#api.blocks.insert( + /** + * @todo when implementing split/merge, think of how to not use toolname here + */ + this.#toolName, { - /** - * @todo when implementing split/merge, think of how to not use toolname here - */ - name: this.#toolName, - data: { - [key]: { - $t: 't', - value: newValueAfter, - fragments: relatedFragments, - }, + [key]: { + $t: 't', + value: newValueAfter, + fragments: relatedFragments, }, }, currentBlockIndex + 1 @@ -483,7 +479,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { this.#caretAdapter.updateIndex( new IndexBuilder() .addBlockIndex(currentBlockIndex + 1) - .addDataKey(key) + .addDataKey(createDataKey(key)) .addTextRange([0, 0]) .build() ); @@ -496,7 +492,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { * @param input - input element * @param key - data key input is attached to */ - #handleModelUpdateForContentEditableElement(event: ModelEvents, input: HTMLElement, key: DataKey): void { + #handleModelUpdateForContentEditableElement(event: ModelEvents, input: HTMLElement, key: string): void { const { userId, index, action } = event.detail; const { textRange, blockIndex: eventBlockIndex } = index; @@ -510,7 +506,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { const builder = new IndexBuilder(); - builder.addDataKey(key).addBlockIndex(eventBlockIndex); + builder.addDataKey(createDataKey(key)).addBlockIndex(eventBlockIndex); let newCaretIndex: number | null = null; @@ -552,12 +548,12 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { const { textRange, dataKey } = event.detail.index; - const input = this.#attachedInputs.get(dataKey!); + const input = this.#attachedInputs.get(dataKey as string); if (!input || textRange === undefined) { return; } - this.#handleModelUpdateForContentEditableElement(event, input, dataKey!); + this.#handleModelUpdateForContentEditableElement(event, input, dataKey as string); }; } diff --git a/packages/dom-adapters/src/CaretAdapter/index.ts b/packages/dom-adapters/src/CaretAdapter/index.ts index ce1c0c5d..49a24cc7 100644 --- a/packages/dom-adapters/src/CaretAdapter/index.ts +++ b/packages/dom-adapters/src/CaretAdapter/index.ts @@ -2,14 +2,12 @@ import { isNativeInput } from '@editorjs/dom'; import { type Caret, type CaretManagerEvents, - EditorJSModel, - EventType, Index, IndexBuilder, createDataKey, createBlockId } from '@editorjs/model'; -import type { CoreConfig } from '@editorjs/sdk'; +import type { CoreConfig, EditorAPI } from '@editorjs/sdk'; import { getAbsoluteRangeOffset, getBoundaryPointByAbsoluteOffset, @@ -33,9 +31,9 @@ export class CaretAdapter { #container: HTMLElement; /** - * Editor.js model + * Editor's API */ - #model: EditorJSModel; + #api: EditorAPI; /** * Shared inputs registry — single source of truth for (blockIndex, dataKey) → HTMLElement. @@ -55,19 +53,19 @@ export class CaretAdapter { /** * @param config - Editor's config - * @param model - Editor.js model + * @param api - Editor's API * @param registry - shared inputs registry */ constructor( @inject(TOKENS.EditorConfig) config: Required, - model: EditorJSModel, + @inject(TOKENS.EditorAPI) api: EditorAPI, registry: InputsRegistry ) { this.#config = config; - this.#model = model; + this.#api = api; this.#inputsRegistry = registry; this.#container = config.holder; - this.#currentUserCaret = this.#model.createCaret(this.#config.userId); + this.#currentUserCaret = this.#api.selection.createCaret(this.#config.userId); const { on } = useSelectionChange(); @@ -76,7 +74,7 @@ export class CaretAdapter { */ on(this.#container, selection => this.#onSelectionChange(selection), this); - this.#model.addEventListener(EventType.CaretManagerUpdated, event => this.#onModelCaretUpdate(event)); + this.#api.selection.onCaretUpdate(event => this.#onModelCaretUpdate(event)); } /** @@ -98,7 +96,7 @@ export class CaretAdapter { return; } - const caretToUpdate = this.#model.getCaret(userId); + const caretToUpdate = this.#api.selection.getCaret(userId); if (caretToUpdate === undefined) { return; @@ -115,13 +113,13 @@ export class CaretAdapter { * @returns input element or undefined if not found */ public findInput(blockIndex: number, dataKeyRaw: string): HTMLElement | undefined { - const blockId = this.#model.getBlockId(blockIndex); + const blockId = this.#api.blocks.getIdByIndex(blockIndex); if (blockId === undefined) { return undefined; } - return this.#inputsRegistry.getInput(blockId, createDataKey(dataKeyRaw)); + return this.#inputsRegistry.getInput(blockId, dataKeyRaw); } /** @@ -213,7 +211,7 @@ export class CaretAdapter { continue; } - const blockIndex = this.#model.getBlockIndexById(createBlockId(blockId)); + const blockIndex = this.#api.blocks.getIndexById(createBlockId(blockId)); if (blockIndex === -1) { continue; diff --git a/packages/dom-adapters/src/FormattingAdapter/index.ts b/packages/dom-adapters/src/FormattingAdapter/index.ts index 25221543..5c70eb81 100644 --- a/packages/dom-adapters/src/FormattingAdapter/index.ts +++ b/packages/dom-adapters/src/FormattingAdapter/index.ts @@ -4,16 +4,14 @@ import type { ModelEvents } from '@editorjs/model'; import { - createInlineToolName, - EditorJSModel + createInlineToolName } from '@editorjs/model'; import { - EventType, TextFormattedEvent, TextUnformattedEvent } from '@editorjs/model'; import { CaretAdapter } from '../CaretAdapter/index.js'; -import type { CoreConfig, InlineTool, ToolLoadedCoreEvent } from '@editorjs/sdk'; +import type { CoreConfig, EditorAPI, InlineTool, ToolLoadedCoreEvent } from '@editorjs/sdk'; import { CoreEventType, EventBus } from '@editorjs/sdk'; import { surround } from '../utils/surround.js'; import { getBoundaryPointByAbsoluteOffset } from '../utils/getRelativeIndex.js'; @@ -28,9 +26,9 @@ import { TOKENS } from '../tokens.js'; @injectable() export class FormattingAdapter { /** - * Editor model instance + * Editor's API */ - #model: EditorJSModel; + #api: EditorAPI; /** * Tools, attached to the inline tool adapter @@ -49,18 +47,18 @@ export class FormattingAdapter { /** * @param config - Editor's config - * @param model - editor model instance + * @param api - Editor's API * @param caretAdapter - caret adapter instance * @param eventBus - Editor's EventBus instance */ constructor( @inject(TOKENS.EditorConfig) config: Required, - model: EditorJSModel, + @inject(TOKENS.EditorAPI) api: EditorAPI, caretAdapter: CaretAdapter, eventBus: EventBus ) { this.#config = config; - this.#model = model; + this.#api = api; this.#caretAdapter = caretAdapter; /** @@ -82,7 +80,7 @@ export class FormattingAdapter { /** * Add event listener for model changes */ - this.#model.addEventListener(EventType.Changed, (event: ModelEvents) => this.#handleModelUpdates(event)); + this.#api.document.onUpdate((event: ModelEvents) => this.#handleModelUpdates(event)); } /** @@ -157,7 +155,7 @@ export class FormattingAdapter { const rangeStart = Math.max(0, textRange[0] - 1); const rangeEnd = inputContent !== null ? Math.min(inputContent.length, textRange[1] + 1) : 0; - const affectedFragments = this.#model.getFragments(blockIndex, dataKey, rangeStart, rangeEnd); + const affectedFragments = this.#api.text.getFragments(blockIndex, dataKey as string, rangeStart, rangeEnd); const leftBoundary = affectedFragments[0]?.range[0] ?? textRange[0]; let rightBoundary = textRange[1]; diff --git a/packages/dom-adapters/src/InputsRegistry/index.ts b/packages/dom-adapters/src/InputsRegistry/index.ts index 037ecb43..2fe90d89 100644 --- a/packages/dom-adapters/src/InputsRegistry/index.ts +++ b/packages/dom-adapters/src/InputsRegistry/index.ts @@ -12,7 +12,7 @@ export class InputsRegistry { /** * Key = block id. Each entry is a (dataKey → element) map for that block. */ - #inputs: Map> = new Map(); + #inputs: Map> = new Map(); /** * Registers (or replaces) an input element for a given block + data key. @@ -20,7 +20,7 @@ export class InputsRegistry { * @param dataKey - data key of the input within the block * @param element - the DOM element to register */ - public register(blockId: BlockId, dataKey: DataKey, element: HTMLElement): void { + public register(blockId: BlockId, dataKey: string, element: HTMLElement): void { let blockMap = this.#inputs.get(blockId); if (blockMap === undefined) { @@ -37,7 +37,7 @@ export class InputsRegistry { * @param blockId - unique id of the block * @param dataKey - optional specific data key to unregister */ - public unregister(blockId: BlockId, dataKey?: DataKey): void { + public unregister(blockId: BlockId, dataKey?: string): void { if (dataKey === undefined) { this.#inputs.delete(blockId); @@ -52,7 +52,7 @@ export class InputsRegistry { * @param blockId - unique id of the block * @param dataKey - data key of the input */ - public getInput(blockId: BlockId, dataKey: DataKey): HTMLElement | undefined { + public getInput(blockId: BlockId, dataKey: string): HTMLElement | undefined { return this.#inputs.get(blockId)?.get(dataKey); } @@ -60,7 +60,7 @@ export class InputsRegistry { * Returns all inputs for a block as a (dataKey → element) map. * @param blockId - unique id of the block */ - public getBlockInputs(blockId: BlockId): Map | undefined { + public getBlockInputs(blockId: BlockId): Map | undefined { return this.#inputs.get(blockId); } @@ -69,7 +69,7 @@ export class InputsRegistry { * Useful for CaretAdapter to iterate all inputs during selection mapping. * @yields */ - public *entries(): Iterable<[BlockId, DataKey, HTMLElement]> { + public *entries(): Iterable<[BlockId, string, HTMLElement]> { for (const [blockId, keyMap] of this.#inputs) { for (const [dataKey, element] of keyMap) { yield [blockId, dataKey, element]; diff --git a/packages/dom-adapters/src/index.ts b/packages/dom-adapters/src/index.ts index 5dbc2b43..10255223 100644 --- a/packages/dom-adapters/src/index.ts +++ b/packages/dom-adapters/src/index.ts @@ -1,14 +1,14 @@ import 'reflect-metadata'; import type { BlockToolAdapter, - EditorJSAdapterPlugin, EditorjsAdapterPluginConstructor, - EditorjsAdapterPluginParams + EditorJSAdapterPlugin, + EditorjsAdapterPluginConstructor, + EditorjsPluginParams } from '@editorjs/sdk'; import { EventBus } from '@editorjs/sdk'; import { PluginType } from '@editorjs/sdk'; import { DOMBlockToolAdapter } from './BlockToolAdapter/index.js'; import { InputsRegistry } from './InputsRegistry/index.js'; -import { EditorJSModel } from '@editorjs/model'; import type { BlockId } from '@editorjs/model'; import { Container } from 'inversify'; import { TOKENS } from './tokens.js'; @@ -38,13 +38,13 @@ export class DOMAdapters implements EditorJSAdapterPlugin { /** * @param params - Plugin parameters * @param params.config - Editor's config - * @param params.model - Model instance + * @param params.api - Editor's API * @param params.eventBus - EventBus instance */ - constructor({ config, model, eventBus }: EditorjsAdapterPluginParams) { + constructor({ config, api, eventBus }: EditorjsPluginParams) { this.#iocContainer.bind>(TOKENS.EditorConfig).toConstantValue(config as Required); - this.#iocContainer.bind(EditorJSModel).toConstantValue(model); this.#iocContainer.bind(EventBus).toConstantValue(eventBus); + this.#iocContainer.bind(TOKENS.EditorAPI).toConstantValue(api); this.#iocContainer .bind(DOMBlockToolAdapter) .toSelf() diff --git a/packages/dom-adapters/src/tokens.ts b/packages/dom-adapters/src/tokens.ts index c547ca63..4a4d27a3 100644 --- a/packages/dom-adapters/src/tokens.ts +++ b/packages/dom-adapters/src/tokens.ts @@ -8,4 +8,9 @@ export const TOKENS = { */ // eslint-disable-next-line @typescript-eslint/naming-convention EditorConfig: Symbol.for('EditorConfig'), + /** + * Editor API token + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + EditorAPI: Symbol.for('EditorAPI'), } as const; diff --git a/packages/model/src/EditorJSModel.ts b/packages/model/src/EditorJSModel.ts index a9a7f97b..b33af370 100644 --- a/packages/model/src/EditorJSModel.ts +++ b/packages/model/src/EditorJSModel.ts @@ -302,14 +302,12 @@ export class EditorJSModel extends EventBus { /** * Returns a data node by the block index and key - * @param _userId - user identifier which is being set to the context * @param parameters - getDataNode method parameters * @param parameters.blockIndex - index of the BlockNode where data node is stored * @param parameters.dataKey - key of the node to get */ - @WithContext - public getDataNode(_userId: string | number, ...parameters: Parameters): ReturnType { - return this.#document.getDataNode(...parameters); + public getDataNode(...parameters: Parameters): ReturnType { + return this.#document.getDataNode(...parameters); } /** diff --git a/packages/model/src/EventBus/types/EventType.ts b/packages/model/src/EventBus/types/EventType.ts index 1c306fbf..6124d905 100644 --- a/packages/model/src/EventBus/types/EventType.ts +++ b/packages/model/src/EventBus/types/EventType.ts @@ -5,10 +5,10 @@ export enum EventType { /** * The document model has been changed */ - Changed = 'changed', + Changed = 'model:changed', /** * The position of caret has been updated */ - CaretManagerUpdated = 'caret-updated' + CaretManagerUpdated = 'model:caret-updated' } diff --git a/packages/model/src/entities/BlockNode/index.ts b/packages/model/src/entities/BlockNode/index.ts index c0b11e11..d8ca4a39 100644 --- a/packages/model/src/entities/BlockNode/index.ts +++ b/packages/model/src/entities/BlockNode/index.ts @@ -170,7 +170,7 @@ export class BlockNode extends EventBus { * Returns data node by the key * @param dataKey - key of the node to get */ - public getDataNode(dataKey: DataKey): ValueSerialized | TextNodeSerialized | undefined { + public getDataNode(dataKey: DataKey): ValueSerialized | TextNodeSerialized | undefined { const node = get(this.data, dataKey as string); if (node === undefined) { diff --git a/packages/model/src/entities/EditorDocument/index.ts b/packages/model/src/entities/EditorDocument/index.ts index ec184c02..f02eb90a 100644 --- a/packages/model/src/entities/EditorDocument/index.ts +++ b/packages/model/src/entities/EditorDocument/index.ts @@ -260,12 +260,12 @@ export class EditorDocument extends EventBus { * @param indexOrId - block index or block id where data node is stored * @param key - data key of the data node */ - public getDataNode(indexOrId: BlockIndexOrId, key: DataKey | string): ValueSerialized | TextNodeSerialized | undefined { + public getDataNode(indexOrId: BlockIndexOrId, key: DataKey | string): ValueSerialized | TextNodeSerialized | undefined { const resolvedIndex = this.#resolveBlockIndex(indexOrId); this.#checkIndexOutOfBounds(resolvedIndex, this.length - 1); - return this.#children[resolvedIndex].getDataNode(createDataKey(key)); + return this.#children[resolvedIndex].getDataNode(createDataKey(key)); } /** diff --git a/packages/sdk/src/api/BlocksAPI.ts b/packages/sdk/src/api/BlocksAPI.ts index a98f9340..4b14b1b1 100644 --- a/packages/sdk/src/api/BlocksAPI.ts +++ b/packages/sdk/src/api/BlocksAPI.ts @@ -1,5 +1,11 @@ import type { BlockToolData } from '@editorjs/editorjs'; -import type { BlockNodeInit, EditorDocumentSerialized } from '@editorjs/model'; +import type { + BlockId, + BlockNodeInit, + EditorDocumentSerialized, + TextNodeSerialized, + ValueSerialized +} from '@editorjs/model'; /** * Blocks API interface @@ -45,9 +51,9 @@ export interface BlocksAPI { /** * Removes Block by index, or current block if index is not passed - * @param index - index of a block to delete + * @param indexOrId - index or id of a block to delete */ - delete(index?: number): void; + delete(indexOrId?: number | string): void; /** * Moves a block to a new index @@ -100,6 +106,48 @@ export interface BlocksAPI { index?: number, ): void; // BlockAPI[]; + /** + * Returns block's index by its id + * @param id - block id to get index for + */ + getIndexById(id: string): number; + + /** + * Returns block id by its index + * @param index - block index to get id for + */ + getIdByIndex(index: number): BlockId | undefined; + + /** + * Returns serialized data for provided data key + * @param indexOrId - index or id of the block + * @param dataKey - data key to get + */ + getData(indexOrId: number | string, dataKey: string): TextNodeSerialized | ValueSerialized | undefined; + + /** + * Removes data by the data key + * @param indexOrId - index or id of the block + * @param dataKey - data key to remove + */ + removeData(indexOrId: number | string, dataKey: string): void; + + /** + * Creates data node with the given key + * @param indexOrId - index or id of the block + * @param dataKey - data key to create + * @param [initialData] - optional initial data + */ + createData(indexOrId: number | string, dataKey: string, initialData?: TextNodeSerialized | ValueSerialized): void; + + /** + * Updates value by the given key + * @param indexOrId - index or id of the block + * @param dataKey - data key to update + * @param value - new value + */ + updateValue(indexOrId: number | string, dataKey: string, value: V): void; + /** * Creates data of an empty block with a passed type. * @param toolName - block tool name diff --git a/packages/sdk/src/api/DocumentAPI.ts b/packages/sdk/src/api/DocumentAPI.ts index 603d05cf..6c26bbf2 100644 --- a/packages/sdk/src/api/DocumentAPI.ts +++ b/packages/sdk/src/api/DocumentAPI.ts @@ -1,4 +1,4 @@ -import type { EditorDocumentSerialized } from '@editorjs/model'; +import type { EditorDocumentSerialized, ModelEvents } from '@editorjs/model'; /** * Document API interface @@ -9,4 +9,10 @@ export interface DocumentAPI { * Returns serialized document object */ get data(): EditorDocumentSerialized; + + /** + * Registers model's update callback. Returns a cleanup function + * @param callback - callback called on model update + */ + onUpdate(callback: (event: ModelEvents) => void): () => void; } diff --git a/packages/sdk/src/api/EditorAPI.ts b/packages/sdk/src/api/EditorAPI.ts index 27dd36b1..3b09ad39 100644 --- a/packages/sdk/src/api/EditorAPI.ts +++ b/packages/sdk/src/api/EditorAPI.ts @@ -1,6 +1,7 @@ import type { BlocksAPI } from './BlocksAPI.js'; import type { SelectionAPI } from './SelectionAPI.js'; import type { DocumentAPI } from './DocumentAPI.js'; +import type { TextAPI } from './TextAPI.js'; /** * Editor API interface @@ -21,4 +22,9 @@ export interface EditorAPI { * Document API instance to work with Editor's document */ document: DocumentAPI; + + /** + * Text API to work with the text content of the document + */ + text: TextAPI; } diff --git a/packages/sdk/src/api/SelectionAPI.ts b/packages/sdk/src/api/SelectionAPI.ts index 6e12990e..c7cfe305 100644 --- a/packages/sdk/src/api/SelectionAPI.ts +++ b/packages/sdk/src/api/SelectionAPI.ts @@ -1,4 +1,4 @@ -import type { InlineToolName } from '@editorjs/model'; +import type { Caret, CaretManagerEvents, InlineToolName } from '@editorjs/model'; /** * Selection API interface @@ -11,4 +11,22 @@ export interface SelectionAPI { * @param data - optional data for the inline tool */ applyInlineToolForCurrentSelection(tool: InlineToolName, data?: Record): void; + + /** + * Registers a callback for CaretManager updates. Returns a cleanup function + * @param callback - callback for CaretManager updates + */ + onCaretUpdate(callback: (event: CaretManagerEvents) => void): () => void; + + /** + * Creates a new caret for a user + * @param userId - user id. If not provided, creates for current user + */ + createCaret(userId?: string | number): Caret; + + /** + * Returns user caret + * @param userId - user id. If not provided, returns for current user + */ + getCaret(userId?: string | number): Caret | undefined; } diff --git a/packages/sdk/src/api/TextAPI.ts b/packages/sdk/src/api/TextAPI.ts new file mode 100644 index 00000000..2d95f9ab --- /dev/null +++ b/packages/sdk/src/api/TextAPI.ts @@ -0,0 +1,96 @@ +import type { + InlineFragment +} from '@editorjs/model'; + +/** + * Editor's TextAPI to work with text content of the document + */ +export interface TextAPI { + /** + * Inserts text at a given position + * @param text - new text to insert + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - start offset + */ + insert( + text: string, + blockIndexOrId: number | string, + dataKey: string, + start?: number + ): void; + + /** + * Removes text from a given range + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - range start + * @param end - range end + */ + remove( + blockIndexOrId: number | string, + dataKey: string, + start?: number, + end?: number + ): string; + + /** + * Formats the given range + * @param tool - tool to apply + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - range start + * @param end - range end + * @param data - optional tool's data + */ + format( + tool: string, + blockIndexOrId: number | string, + dataKey: string, + start: number, + end: number, + data?: Record, + ): void; + + /** + * Unformats the given range + * @param tool - tool to remove + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - range start + * @param end - range end + */ + unformat( + tool: string, + blockIndexOrId: number | string, + dataKey: string, + start: number, + end: number + ): void; + + /** + * Returns applied inline fragments for a given range + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + * @param start - range start + * @param end - range end + * @param tool - optional filter tool. If provided, will return only fragments of the given tool + */ + getFragments( + blockIndexOrId: number | string, + dataKey: string, + start?: number, + end?: number, + tool?: string + ): InlineFragment[]; + + /** + * Returns text content of the text node + * @param blockIndexOrId - block index or identifier + * @param dataKey - data key of the text node + */ + get( + blockIndexOrId: number | string, + dataKey: string + ): string; +} diff --git a/packages/sdk/src/api/index.ts b/packages/sdk/src/api/index.ts index 3e77540f..23872b60 100644 --- a/packages/sdk/src/api/index.ts +++ b/packages/sdk/src/api/index.ts @@ -2,3 +2,4 @@ export type * from './EditorAPI.js'; export type * from './BlocksAPI.js'; export type * from './SelectionAPI.js'; export type * from './DocumentAPI.js'; +export type * from './TextAPI.js'; diff --git a/packages/sdk/src/entities/BlockToolAdapter.ts b/packages/sdk/src/entities/BlockToolAdapter.ts index e31cb47e..10059cb4 100644 --- a/packages/sdk/src/entities/BlockToolAdapter.ts +++ b/packages/sdk/src/entities/BlockToolAdapter.ts @@ -1,24 +1,23 @@ -import type { BlockId, DataKey, EditorJSModel, EventBus, ModelEvents, TextNodeSerialized, ValueSerialized } from '@editorjs/model'; +import type { BlockId, EventBus, ModelEvents, TextNodeSerialized, ValueSerialized } from '@editorjs/model'; import { - createDataKey, DataNodeAddedEvent, DataNodeRemovedEvent, - EventType, NODE_TYPE_HIDDEN_PROP, ValueModifiedEvent, BlockChildType } from '@editorjs/model'; import type { CoreConfig } from '@/entities/Config'; import { KeyAddedEvent, KeyRemovedEvent, ValueNodeChangedEvent } from './EventBus/events/adapter/index.js'; +import type { EditorAPI } from '../api/index.js'; /** * Abstract BlockToolAdapter class implementing core functionality of the block adapter */ export abstract class BlockToolAdapter extends EventTarget { /** - * Model instance + * Editor's API */ - protected model: EditorJSModel; + #api: EditorAPI; /** * Unique identifier of the block that this adapter is connected to @@ -38,22 +37,23 @@ export abstract class BlockToolAdapter extends EventTarget { /** * Stored reference to the model change listener so it can be removed on destroy. */ - #modelChangeListener: EventListener; + #modelChangeListenerCleanup: () => void; /** * @param config - editor's configuration - * @param model - model instance + * @param api - Editor's API * @param eventBus - global event bus instance */ - constructor(config: Required, model: EditorJSModel, eventBus: EventBus) { + constructor(config: Required, api: EditorAPI, eventBus: EventBus) { super(); - this.model = model; + this.#api = api; this.config = config; this.eventBus = eventBus; - this.#modelChangeListener = ((event: ModelEvents) => this.#handleModelUpdate(event)) as EventListener; - this.model.addEventListener(EventType.Changed, this.#modelChangeListener); + this.#modelChangeListenerCleanup = this.#api.document.onUpdate( + ((event: ModelEvents) => this.#handleModelUpdate(event)) as EventListener + ); } /** @@ -63,7 +63,7 @@ export abstract class BlockToolAdapter extends EventTarget { * call `super.destroy()`, and then remove their own listeners. */ public destroy(): void { - this.model.removeEventListener(EventType.Changed, this.#modelChangeListener); + this.#modelChangeListenerCleanup(); } /** @@ -81,62 +81,52 @@ export abstract class BlockToolAdapter extends EventTarget { return this.blockId; } - /** - * @deprecated Use {@link setBlockId} + {@link getBlockId} instead. - * Kept temporarily for backward compatibility while callers are migrated. - * Updates the internal block index (derived on demand from the model). - * @param index - new block index value - */ - public setBlockIndex(index: number): void { - void index; // no-op – adapters are now addressed by blockId - } - /** * @deprecated Use {@link getBlockId} instead. * Returns the current block index by asking the model. */ public getBlockIndex(): number { - return this.model.getBlockIndexById(this.blockId); + return this.#api.blocks.getIndexById(this.blockId); } /** * Creates data node for the text input key - * @param keyRaw - input key within the block + * @param key - input key within the block * @param initialData - optional initial data for the block */ - public registerTextInputKey(keyRaw: string, initialData?: Pick & Partial): void { + public registerTextInputKey(key: string, initialData?: Pick & Partial): void { const data: TextNodeSerialized = { value: initialData?.value ?? '', fragments: initialData?.fragments ?? [], [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, }; - this.#createDataNode(createDataKey(keyRaw), data); + this.#createDataNode(key, data); } /** * Creates data node for the value key. Returns an update function which could be called to update value in the model - * @param keyRaw - value key within the block + * @param key - value key within the block * @param initialData - optional initial data for the value */ - public registerValueKey(keyRaw: string, initialData?: ValueSerialized): (newValue: V) => void { - this.#createDataNode(createDataKey(keyRaw), initialData); + public registerValueKey(key: string, initialData?: ValueSerialized): (newValue: V) => void { + this.#createDataNode(key, initialData); return (newValue: V) => { - this.model.updateValue(this.config.userId, this.blockId, createDataKey(keyRaw), newValue); + this.#api.blocks.updateValue(this.blockId, key, newValue); }; } /** * Remove data node by the key - * @param keyRaw - key of the node to remove + * @param key - key of the node to remove */ - public removeKey(keyRaw: string): void { - if (this.model.getDataNode(this.config.userId, this.blockId, keyRaw) === undefined) { + public removeKey(key: string): void { + if (this.#api.blocks.getData(this.blockId, key) === undefined) { return; } - this.model.removeDataNode(this.config.userId, this.blockId, createDataKey(keyRaw)); + this.#api.blocks.removeData(this.blockId, key); } /** @@ -150,12 +140,12 @@ export abstract class BlockToolAdapter extends EventTarget { * // Register a value key in an array (e.g. for items[0].content) * this.#createDataNode(createDataKey('items[0].content'), { $t: 'v', value: 'Item text' }); */ - #createDataNode(key: DataKey, initialData?: TextNodeSerialized | ValueSerialized): void { - if (this.model.getDataNode(this.config.userId, this.blockId, key) !== undefined) { + #createDataNode(key: string, initialData?: TextNodeSerialized | ValueSerialized): void { + if (this.#api.blocks.getData(this.blockId, key) !== undefined) { return; } - this.model.createDataNode(this.config.userId, this.blockId, key, initialData); + this.#api.blocks.createData(this.blockId, key, initialData); } /** @@ -169,7 +159,7 @@ export abstract class BlockToolAdapter extends EventTarget { return; } - const eventBlockId = this.model.getBlockId(blockIndex); + const eventBlockId = this.#api.blocks.getIdByIndex(blockIndex); if (eventBlockId !== this.blockId) { return; diff --git a/packages/sdk/src/entities/EditorjsAdapterPlugin.ts b/packages/sdk/src/entities/EditorjsAdapterPlugin.ts index a2c7d35f..2e6696aa 100644 --- a/packages/sdk/src/entities/EditorjsAdapterPlugin.ts +++ b/packages/sdk/src/entities/EditorjsAdapterPlugin.ts @@ -1,19 +1,8 @@ -import type { EditorjsPlugin, EditorjsPluginConstructor, EditorjsPluginParams } from '@/entities/EditorjsPlugin'; -import type { BlockId, EditorJSModel } from '@editorjs/model'; +import type { EditorjsPlugin, EditorjsPluginConstructor } from '@/entities/EditorjsPlugin'; +import type { BlockId } from '@editorjs/model'; import type { PluginType } from '@/entities/EntityType'; import type { BlockToolAdapter } from '@/entities/BlockToolAdapter'; -/** - * Parameters for adapter plugin constructor. - * Extends standard plugin params with direct model access for low-level operations. - */ -export interface EditorjsAdapterPluginParams extends EditorjsPluginParams { - /** - * Editor's document model instance - */ - model: EditorJSModel; -} - /** * Base interface for adapter plugins */ @@ -37,7 +26,7 @@ export interface EditorJSAdapterPlugin extends EditorjsPlugin { /** * Constructor type for adapter plugins */ -export interface EditorjsAdapterPluginConstructor extends EditorjsPluginConstructor { +export interface EditorjsAdapterPluginConstructor extends EditorjsPluginConstructor { /** * Marks the plugin as a singleton adapter, replaceable via core.use() */ diff --git a/packages/sdk/src/entities/EditorjsPlugin.ts b/packages/sdk/src/entities/EditorjsPlugin.ts index 84045a57..fe4085af 100644 --- a/packages/sdk/src/entities/EditorjsPlugin.ts +++ b/packages/sdk/src/entities/EditorjsPlugin.ts @@ -37,10 +37,6 @@ export interface EditorjsPlugin { * Constructor type for EditorjsPlugin */ export interface EditorjsPluginConstructor< - /** - * Plugin's params. Has to be a generic param as constructor can not be overloaded - */ - Params extends EditorjsPluginParams = EditorjsPluginParams, /** * Plugin's instance interface. Has to be a generic param as constructor can not be overloaded */ @@ -49,7 +45,7 @@ export interface EditorjsPluginConstructor< /** * Create new EditorjsPlugin instance */ - new (params: Params): Instance; + new (params: EditorjsPluginParams): Instance; /** * Plugin's entity type: UI plugin, Tool, etc. From 3c6f1063030dd4695b364291ad18ca517076f299 Mon Sep 17 00:00:00 2001 From: gohabereg Date: Thu, 14 May 2026 00:25:35 +0100 Subject: [PATCH 2/5] Fix models tests --- packages/model/src/EditorJSModel.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/model/src/EditorJSModel.spec.ts b/packages/model/src/EditorJSModel.spec.ts index 0e65a8c6..240496b1 100644 --- a/packages/model/src/EditorJSModel.spec.ts +++ b/packages/model/src/EditorJSModel.spec.ts @@ -237,7 +237,7 @@ describe('EditorJSModel', () => { // DataNodeAdded events are queued as microtasks, flush before asserting await Promise.resolve(); - const node = model.getDataNode(userId, 0, 'text'); + const node = model.getDataNode(0, 'text'); expect(node).toBeDefined(); }); @@ -245,7 +245,7 @@ describe('EditorJSModel', () => { it('should return undefined for a non-existent key', async () => { await Promise.resolve(); - const node = model.getDataNode(userId, 0, 'nonexistent'); + const node = model.getDataNode(0, 'nonexistent'); expect(node).toBeUndefined(); }); From c2ba5b2d39ab1e78467b5e31118c2b4649474cbc Mon Sep 17 00:00:00 2001 From: gohabereg Date: Thu, 14 May 2026 18:50:23 +0100 Subject: [PATCH 3/5] Update types for delete block method --- packages/core/src/api/BlocksAPI.ts | 6 +++--- packages/core/src/components/BlockManager.ts | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/core/src/api/BlocksAPI.ts b/packages/core/src/api/BlocksAPI.ts index 328c0c3d..dcf5fd75 100644 --- a/packages/core/src/api/BlocksAPI.ts +++ b/packages/core/src/api/BlocksAPI.ts @@ -68,10 +68,10 @@ export class BlocksAPI implements BlocksApiInterface { /** * Removes Block by index, or current block if index is not passed - * @param index - index of a block to delete + * @param indexOrId - index or identifier of a block to delete */ - public delete(index?: number): void { - return this.#blocksManager.deleteBlock(index); + public delete(indexOrId?: number): void { + return this.#blocksManager.deleteBlock(indexOrId); } /** diff --git a/packages/core/src/components/BlockManager.ts b/packages/core/src/components/BlockManager.ts index 7cfd61c2..710a40d8 100644 --- a/packages/core/src/components/BlockManager.ts +++ b/packages/core/src/components/BlockManager.ts @@ -1,4 +1,5 @@ import { + BlockIndexOrId, type BlockNodeInit, type EditorDocumentSerialized, EditorJSModel @@ -171,17 +172,17 @@ export class BlocksManager { /** * Removes Block by index, or current block if index is not passed - * @param index - index of a block to delete + * @param indexOrId - index or identifier of a block to delete */ - public deleteBlock(index: number | undefined = this.#getCurrentBlockIndex()): void { - if (index === undefined) { + public deleteBlock(indexOrId: number | string | undefined = this.#getCurrentBlockIndex()): void { + if (indexOrId === undefined) { /** * @todo see what happens in legacy */ throw new Error('No block selected to delete'); } - this.#model.removeBlock(this.#config.userId, index); + this.#model.removeBlock(this.#config.userId, indexOrId as BlockIndexOrId); } /** From 475dde64a320ffb48dc519caddae0a45edddc0f1 Mon Sep 17 00:00:00 2001 From: gohabereg Date: Fri, 15 May 2026 20:55:40 +0100 Subject: [PATCH 4/5] Change API methods to accept a single params object --- .../src/api/BlocksAPI.integration.spec.ts | 200 ++++++++++++------ packages/core/src/api/BlocksAPI.spec.ts | 28 +-- packages/core/src/api/BlocksAPI.ts | 113 +++++----- packages/core/src/api/SelectionAPI.spec.ts | 7 +- packages/core/src/api/SelectionAPI.ts | 11 +- packages/core/src/api/TextAPI.ts | 126 +++++------ packages/core/src/plugins/ShortcutsPlugin.ts | 3 +- .../src/BlockToolAdapter/index.ts | 61 ++++-- .../src/FormattingAdapter/index.ts | 7 +- packages/sdk/src/api/BlocksAPI.ts | 132 ++++++++---- packages/sdk/src/api/SelectionAPI.ts | 10 +- packages/sdk/src/api/TextAPI.ts | 142 +++++++------ packages/sdk/src/entities/BlockToolAdapter.ts | 27 ++- .../ui/src/InlineToolbar/InlineToolbar.ts | 7 +- packages/ui/src/Toolbox/Toolbox.ts | 12 +- 15 files changed, 523 insertions(+), 363 deletions(-) diff --git a/packages/core/src/api/BlocksAPI.integration.spec.ts b/packages/core/src/api/BlocksAPI.integration.spec.ts index fd23bbab..b6435369 100644 --- a/packages/core/src/api/BlocksAPI.integration.spec.ts +++ b/packages/core/src/api/BlocksAPI.integration.spec.ts @@ -95,7 +95,10 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { describe('insert()', () => { it('should add a block to an empty document and model.length becomes 1', () => { - blocksAPI.insert('paragraph', {}); + blocksAPI.insert({ + type: 'paragraph', + data: {}, + }); expect(model.length).toBe(1); expect(model.serialized.blocks[0]).toEqual( @@ -104,10 +107,14 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { }); it('should insert a block at the specified index', () => { - blocksAPI.insert('paragraph'); - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); + blocksAPI.insert({ type: 'paragraph' }); - blocksAPI.insert('header', { text: 'Title' }, 1); + blocksAPI.insert({ + type: 'header', + data: { text: 'Title' }, + index: 1, + }); expect(model.length).toBe(3); expect(model.serialized.blocks[1]).toEqual( @@ -125,10 +132,15 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { }); it('should replace a block at the given index when replace flag is set', () => { - blocksAPI.insert('paragraph'); - blocksAPI.insert('paragraph'); - - blocksAPI.insert('header', {}, 0, undefined, true); + blocksAPI.insert({ type: 'paragraph' }); + blocksAPI.insert({ type: 'paragraph' }); + + blocksAPI.insert({ + type: 'header', + data: {}, + index: 0, + replace: true, + }); expect(model.length).toBe(2); expect(model.serialized.blocks[0]).toEqual( @@ -139,10 +151,10 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { describe('insertMany()', () => { it('should insert multiple blocks at the specified index', () => { - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); - blocksAPI.insertMany( - [ + blocksAPI.insertMany({ + blocks: [ { name: 'header', data: {}, @@ -152,8 +164,8 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { data: {}, }, ], - 0 - ); + index: 0, + }); expect(model.length).toBe(3); expect(model.serialized.blocks[0]).toEqual(expect.objectContaining({ name: 'header' })); @@ -162,18 +174,20 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { }); it('should append blocks at the end when index is omitted', () => { - blocksAPI.insert('paragraph'); - - blocksAPI.insertMany([ - { - name: 'header', - data: {}, - }, - { - name: 'list', - data: {}, - }, - ]); + blocksAPI.insert({ type: 'paragraph' }); + + blocksAPI.insertMany({ + blocks: [ + { + name: 'header', + data: {}, + }, + { + name: 'list', + data: {}, + }, + ], + }); expect(model.length).toBe(3); expect(model.serialized.blocks[1]).toEqual(expect.objectContaining({ name: 'header' })); @@ -183,11 +197,17 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { describe('delete()', () => { it('should remove a block at the given index', () => { - blocksAPI.insert('paragraph'); - blocksAPI.insert('header', {}, 1); - blocksAPI.insert('list', {}, 2); + blocksAPI.insert({ type: 'paragraph' }); + blocksAPI.insert({ + type: 'header', + index: 1, + }); + blocksAPI.insert({ + type: 'list', + index: 2, + }); - blocksAPI.delete(1); + blocksAPI.delete({ block: 1 }); expect(model.length).toBe(2); expect(model.serialized.blocks[0]).toEqual(expect.objectContaining({ name: 'paragraph' })); @@ -195,17 +215,20 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { }); it('should remove the first block when index is 0', () => { - blocksAPI.insert('paragraph'); - blocksAPI.insert('header', {}, 1); + blocksAPI.insert({ type: 'paragraph' }); + blocksAPI.insert({ + type: 'header', + index: 1, + }); - blocksAPI.delete(0); + blocksAPI.delete({ block: 0 }); expect(model.length).toBe(1); expect(model.serialized.blocks[0]).toEqual(expect.objectContaining({ name: 'header' })); }); it('should throw when no index is provided and no caret is set', () => { - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); expect(() => blocksAPI.delete()).toThrow('No block selected to delete'); }); @@ -213,39 +236,63 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { describe('move()', () => { it('should move a block from fromIndex to toIndex (forward)', () => { - blocksAPI.insert('a'); - blocksAPI.insert('b', {}, 1); - blocksAPI.insert('c', {}, 2); + blocksAPI.insert({ type: 'a' }); + blocksAPI.insert({ + type: 'b', + index: 1, + }); + blocksAPI.insert({ + type: 'c', + index: 2, + }); // Move block 0 ("a") to index 2 - blocksAPI.move(2, 0); + blocksAPI.move({ + toIndex: 2, + fromIndex: 0, + }); expect(model.serialized.blocks.map(b => b.name)).toEqual(['b', 'c', 'a']); }); it('should move a block from fromIndex to toIndex (backward)', () => { - blocksAPI.insert('a'); - blocksAPI.insert('b', {}, 1); - blocksAPI.insert('c', {}, 2); + blocksAPI.insert({ type: 'a' }); + blocksAPI.insert({ + type: 'b', + index: 1, + }); + blocksAPI.insert({ + type: 'c', + index: 2, + }); - blocksAPI.move(0, 2); + blocksAPI.move({ + toIndex: 0, + fromIndex: 2, + }); expect(model.serialized.blocks.map(b => b.name)).toEqual(['c', 'a', 'b']); }); it('should not change anything when fromIndex equals toIndex', () => { - blocksAPI.insert('a'); - blocksAPI.insert('b', {}, 1); + blocksAPI.insert({ type: 'a' }); + blocksAPI.insert({ + type: 'b', + index: 1, + }); - blocksAPI.move(0, 0); + blocksAPI.move({ + toIndex: 0, + fromIndex: 0, + }); expect(model.serialized.blocks.map(b => b.name)).toEqual(['a', 'b']); }); it('should throw when no fromIndex is provided and no caret is set', () => { - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); - expect(() => blocksAPI.move(0)).toThrow('No block selected to move'); + expect(() => blocksAPI.move({ toIndex: 0 })).toThrow('No block selected to move'); }); }); @@ -255,13 +302,13 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { }); it('should return the correct count after insertions and deletions', () => { - blocksAPI.insert('paragraph'); - blocksAPI.insert('paragraph'); - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); + blocksAPI.insert({ type: 'paragraph' }); + blocksAPI.insert({ type: 'paragraph' }); expect(blocksAPI.getBlocksCount()).toBe(3); - blocksAPI.delete(0); + blocksAPI.delete({ block: 0 }); expect(blocksAPI.getBlocksCount()).toBe(2); }); @@ -269,8 +316,8 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { describe('render()', () => { it('should replace document content with the provided serialized data', () => { - blocksAPI.insert('paragraph'); - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); + blocksAPI.insert({ type: 'paragraph' }); blocksAPI.render({ identifier: 'new-doc', @@ -289,7 +336,7 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { }); it('should result in an empty document when empty blocks array is passed', () => { - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); blocksAPI.render({ identifier: 'empty-doc', @@ -303,9 +350,15 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { describe('clear()', () => { it('should remove all blocks from the document', () => { - blocksAPI.insert('paragraph'); - blocksAPI.insert('header', {}, 1); - blocksAPI.insert('list', {}, 2); + blocksAPI.insert({ type: 'paragraph' }); + blocksAPI.insert({ + type: 'header', + index: 1, + }); + blocksAPI.insert({ + type: 'list', + index: 2, + }); blocksAPI.clear(); @@ -326,7 +379,7 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { model.addEventListener(EventType.Changed, handler); - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); expect(handler).toHaveBeenCalled(); @@ -334,13 +387,13 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { }); it('should emit BlockRemovedEvent on model when delete is called', async () => { - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); const handler = jest.fn(); model.addEventListener(EventType.Changed, handler); - blocksAPI.delete(0); + blocksAPI.delete({ block: 0 }); await Promise.resolve(); // flush queueMicrotask used by removeBlock @@ -352,22 +405,33 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { describe('combined operations', () => { it('should handle a sequence of insert, move, delete, and clear', () => { - // Insert 3 blocks: a, b, c - blocksAPI.insert('a'); - blocksAPI.insert('b', {}, 1); - blocksAPI.insert('c', {}, 2); + blocksAPI.insert({ type: 'a' }); + blocksAPI.insert({ + type: 'b', + index: 1, + }); + blocksAPI.insert({ + type: 'c', + index: 2, + }); expect(model.serialized.blocks.map(b => b.name)).toEqual(['a', 'b', 'c']); // Move c to front: c, a, b - blocksAPI.move(0, 2); + blocksAPI.move({ + toIndex: 0, + fromIndex: 2, + }); expect(model.serialized.blocks.map(b => b.name)).toEqual(['c', 'a', 'b']); // Delete middle block (a): c, b - blocksAPI.delete(1); + blocksAPI.delete({ block: 1 }); expect(model.serialized.blocks.map(b => b.name)).toEqual(['c', 'b']); // Insert d at index 1: c, d, b - blocksAPI.insert('d', {}, 1); + blocksAPI.insert({ + type: 'd', + index: 1, + }); expect(model.serialized.blocks.map(b => b.name)).toEqual(['c', 'd', 'b']); // Clear everything @@ -376,7 +440,7 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { }); it('should support render after clear and then further mutations', () => { - blocksAPI.insert('paragraph'); + blocksAPI.insert({ type: 'paragraph' }); blocksAPI.clear(); blocksAPI.render({ @@ -398,7 +462,7 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => { expect(model.length).toBe(2); - blocksAPI.delete(0); + blocksAPI.delete({ block: 0 }); expect(model.length).toBe(1); expect(model.serialized.blocks[0]).toEqual(expect.objectContaining({ name: 'list' })); diff --git a/packages/core/src/api/BlocksAPI.spec.ts b/packages/core/src/api/BlocksAPI.spec.ts index a0acb4b2..e0b56e8a 100644 --- a/packages/core/src/api/BlocksAPI.spec.ts +++ b/packages/core/src/api/BlocksAPI.spec.ts @@ -72,7 +72,7 @@ describe('BlocksAPI', () => { new EditorJSModel('userId', { identifier: 'docId' }) ); - api.delete(2); + api.delete({ block: 2 }); expect(blocksManager.deleteBlock).toHaveBeenCalledWith(2); }); @@ -98,7 +98,8 @@ describe('BlocksAPI', () => { new EditorJSModel('userId', { identifier: 'docId' }) ); - api.move(3, 1); + api.move({ toIndex: 3, + fromIndex: 1 }); expect(blocksManager.move).toHaveBeenCalledWith(3, 1); }); @@ -132,7 +133,10 @@ describe('BlocksAPI', () => { data: {}, }]; - api.insertMany(blocks as never, 4); + api.insertMany({ + blocks: blocks, + index: 4, + }); expect(blocksManager.insertMany).toHaveBeenCalledWith(blocks, 4); }); @@ -149,7 +153,7 @@ describe('BlocksAPI', () => { data: {}, }]; - api.insertMany(blocks as never); + api.insertMany({ blocks }); expect(blocksManager.insertMany).toHaveBeenCalledWith(blocks, undefined); }); @@ -180,14 +184,14 @@ describe('BlocksAPI', () => { new EditorJSModel('userId', { identifier: 'docId' }) ); - api.insert( - 'header', - { text: 'Title' }, - 2, - true, - true, - 'id-1' - ); + api.insert({ + type: 'header', + data: { text: 'Title' }, + index: 2, + focus: true, + replace: true, + id: 'id-1', + }); expect(blocksManager.insert).toHaveBeenCalledWith({ type: 'header', diff --git a/packages/core/src/api/BlocksAPI.ts b/packages/core/src/api/BlocksAPI.ts index dcf5fd75..33511cc2 100644 --- a/packages/core/src/api/BlocksAPI.ts +++ b/packages/core/src/api/BlocksAPI.ts @@ -2,16 +2,17 @@ import 'reflect-metadata'; import { inject, injectable } from 'inversify'; import { TOKENS } from '../tokens.js'; import { BlocksManager } from '../components/BlockManager.js'; -import { BlockToolData } from '@editorjs/editorjs'; import { CoreConfigValidated } from '@editorjs/sdk'; import { BlocksAPI as BlocksApiInterface } from '@editorjs/sdk'; import { - BlockId, BlockIndexOrId, - type BlockNodeInit, - createBlockId, createDataKey, - type EditorDocumentSerialized, + BlockId, + BlockIndexOrId, + createBlockId, + createDataKey, + EditorDocumentSerialized, EditorJSModel, - TextNodeSerialized, ValueSerialized + TextNodeSerialized, + ValueSerialized } from '@editorjs/model'; /** @@ -67,19 +68,21 @@ export class BlocksAPI implements BlocksApiInterface { } /** - * Removes Block by index, or current block if index is not passed - * @param indexOrId - index or identifier of a block to delete + * Removes Block by index or id, or current block if params are not passed + * @param params - delete parameters + * @param params.block - index or id of a block to delete */ - public delete(indexOrId?: number): void { - return this.#blocksManager.deleteBlock(indexOrId); + public delete({ block }: NonNullable[0]> = {}): void { + return this.#blocksManager.deleteBlock(block); } /** * Moves a block to a new index - * @param toIndex - index where the block is moved to - * @param [fromIndex] - block to move. Current block if not passed + * @param params - move parameters + * @param params.toIndex - index where the block is moved to + * @param [params.fromIndex] - block to move. Current block if not passed */ - public move(toIndex: number, fromIndex?: number): void { + public move({ toIndex, fromIndex }: Parameters[0]): void { return this.#blocksManager.move(toIndex, fromIndex); } @@ -92,31 +95,24 @@ export class BlocksAPI implements BlocksApiInterface { /** * Inserts several Blocks to specified index - * @param blocks - array of blocks to insert - * @param [index] - index to insert blocks at. If undefined, inserts at the end + * @param params - insertMany parameters + * @param params.blocks - array of blocks to insert + * @param [params.index] - index to insert blocks at. If undefined, inserts at the end */ - public insertMany(blocks: BlockNodeInit[], index?: number): void { + public insertMany({ blocks, index }: Parameters[0]): void { return this.#blocksManager.insertMany(blocks, index); } /** * Inserts a new block to the editor - * @param type - Block tool name to insert - * @param data - Block's initial data - * @param index - index to insert block at - * @param focus - flag indicates if new block should be focused @todo implement - * @param replace - flag indicates if block at index should be replaced @todo implement - * @param id - id of the inserted block @todo implement - */ - public insert( - type?: string, - data?: BlockToolData, - index?: number, - focus?: boolean, - replace?: boolean, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - id?: string - ): void { + * @param [params] - insert parameters + * @param [params.type] - Block tool name to insert + * @param [params.data] - Block's initial data + * @param [params.index] - index to insert block at + * @param [params.focus] - flag indicates if new block should be focused @todo implement + * @param [params.replace] - flag indicates if block at index should be replaced @todo implement + */ + public insert({ type, data, index, focus, replace }: NonNullable[0]> = {}): void { const blockType = type ?? this.#config.defaultBlock; const blockData = data ?? {}; @@ -147,51 +143,48 @@ export class BlocksAPI implements BlocksApiInterface { /** * Returns serialized data for provided data key - * @param blockIndexOrId - index or identifier of the block - * @param dataKey - data key to get serialized data for - */ - public getData(blockIndexOrId: number | string, dataKey: string): TextNodeSerialized | ValueSerialized | undefined { - /** - * Need an explicit cast here because TS doesn't pass generic for some reason - */ - return this.#model.getDataNode(blockIndexOrId as BlockIndexOrId, dataKey) as TextNodeSerialized | ValueSerialized | undefined; + * @param params - getData parameters + * @param params.block - index or id of the block + * @param params.key - data key to get serialized data for + */ + public getData({ block, key }: Parameters[0]): TextNodeSerialized | ValueSerialized | undefined { + return this.#model.getDataNode(block as BlockIndexOrId, key) as TextNodeSerialized | ValueSerialized | undefined; } /** * Creates data node with the given key - * @param indexOrId - index or id of the block - * @param dataKey - data key of the new data node - * @param [initialData] - optional initial data - */ - public createData( - indexOrId: number | string, - dataKey: string, - initialData?: TextNodeSerialized | ValueSerialized - ): void { + * @param params - createData parameters + * @param params.block - index or id of the block + * @param params.key - data key of the new data node + * @param [params.initialData] - optional initial data + */ + public createData({ block, key, initialData }: Parameters[0]): void { this.#model.createDataNode( this.#config.userId, - indexOrId as BlockIndexOrId, - dataKey, + block as BlockIndexOrId, + key, initialData ); } /** * Removes data by the data key - * @param blockIndexOrId - index or identifier of the block - * @param dataKey - data key of the node to remove + * @param params - removeData parameters + * @param params.block - index or identifier of the block + * @param params.key - data key of the node to remove */ - public removeData(blockIndexOrId: string | number, dataKey: string): void { - this.#model.removeDataNode(this.#config.userId, blockIndexOrId as BlockIndexOrId, dataKey); + public removeData({ block, key }: Parameters[0]): void { + this.#model.removeDataNode(this.#config.userId, block as BlockIndexOrId, key); } /** * Updates value by the given key - * @param blockIndexOrId - index or identifier of the block - * @param dataKey - key of the data node to update - * @param value - new value + * @param params - updateValue parameters + * @param params.block - index or identifier of the block + * @param params.key - key of the data node to update + * @param params.value - new value */ - public updateValue(blockIndexOrId: string | number, dataKey: string, value: V): void { - this.#model.updateValue(this.#config.userId, blockIndexOrId as BlockIndexOrId, createDataKey(dataKey), value); + public updateValue({ block, key, value }: Parameters[0]): void { + this.#model.updateValue(this.#config.userId, block as BlockIndexOrId, createDataKey(key), value); } } diff --git a/packages/core/src/api/SelectionAPI.spec.ts b/packages/core/src/api/SelectionAPI.spec.ts index 88ee3fe4..e01f09ab 100644 --- a/packages/core/src/api/SelectionAPI.spec.ts +++ b/packages/core/src/api/SelectionAPI.spec.ts @@ -25,7 +25,7 @@ describe('SelectionAPI', () => { // @ts-expect-error - mock object const selectionManager = new SelectionManager(); - describe('.applyInlineToolForCurrentSelection()', () => { + describe('.applyInlineTool()', () => { it('should convert toolName and delegate to SelectionManager', () => { const api = new SelectionAPI( selectionManager as unknown as InstanceType, @@ -33,7 +33,10 @@ describe('SelectionAPI', () => { {} as unknown as CoreConfigValidated ); - api.applyInlineToolForCurrentSelection('bold', { level: 1 }); + api.applyInlineTool({ + tool: 'bold', + data: { level: 1 }, + }); expect(createInlineToolName).toHaveBeenCalledWith('bold'); expect(selectionManager.applyInlineToolForCurrentSelection).toHaveBeenCalledWith('inline:bold', { level: 1 }); diff --git a/packages/core/src/api/SelectionAPI.ts b/packages/core/src/api/SelectionAPI.ts index 06b88022..f7bfa9f8 100644 --- a/packages/core/src/api/SelectionAPI.ts +++ b/packages/core/src/api/SelectionAPI.ts @@ -3,7 +3,7 @@ import { inject, injectable } from 'inversify'; import { SelectionManager } from '../components/SelectionManager.js'; import { Caret, CaretManagerEvents, createInlineToolName, EditorJSModel, EventType } from '@editorjs/model'; -import { CoreConfigValidated, InlineToolFormatData } from '@editorjs/sdk'; +import { CoreConfigValidated } from '@editorjs/sdk'; import { SelectionAPI as SelectionApiInterface } from '@editorjs/sdk'; import { TOKENS } from '../tokens'; @@ -35,11 +35,12 @@ export class SelectionAPI implements SelectionApiInterface { /** * Applies passed inline tool to the current selection - * @param toolName - Inline Tool name from the config to apply on the current selection - * @param data - Inline Tool data to apply to the current selection (eg. link data) + * @param params - methods parameters + * @param params.tool - Inline Tool name from the config to apply on the current selection + * @param [params.data] - Inline Tool data to apply to the current selection (e.g. link data) */ - public applyInlineToolForCurrentSelection(toolName: string, data?: InlineToolFormatData): void { - this.#selectionManager.applyInlineToolForCurrentSelection(createInlineToolName(toolName), data); + public applyInlineTool({ tool, data }: Parameters[0]): void { + this.#selectionManager.applyInlineToolForCurrentSelection(createInlineToolName(tool), data); } /** diff --git a/packages/core/src/api/TextAPI.ts b/packages/core/src/api/TextAPI.ts index ae3bd15a..1e51d74e 100644 --- a/packages/core/src/api/TextAPI.ts +++ b/packages/core/src/api/TextAPI.ts @@ -40,21 +40,17 @@ export class TextAPI implements TextAPIInterface { /** * Inserts text at a given position - * @param text - new text to insert - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - start offset + * @param params - insert parameters + * @param params.text - new text to insert + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param params.start - start offset */ - public insert( - text: string, - blockIndexOrId: number | string, - dataKey: string, - start?: number - ): void { + public insert({ text, block, key, start }: Parameters[0]): void { this.#model.insertText( this.#config.userId, - blockIndexOrId as BlockIndexOrId, - createDataKey(dataKey), + block as BlockIndexOrId, + createDataKey(key), text, start ); @@ -62,21 +58,17 @@ export class TextAPI implements TextAPIInterface { /** * Removes text from a given range. Returns removed text - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - range start - * @param end - range end + * @param params - remove parameters + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param params.start - range start + * @param params.end - range end */ - public remove( - blockIndexOrId: number | string, - dataKey: string, - start?: number, - end?: number - ): string { + public remove({ block, key, start, end }: Parameters[0]): string { return this.#model.removeText( this.#config.userId, - blockIndexOrId as BlockIndexOrId, - createDataKey(dataKey), + block as BlockIndexOrId, + createDataKey(key), start, end ); @@ -84,25 +76,19 @@ export class TextAPI implements TextAPIInterface { /** * Formats the given range - * @param tool - tool to apply - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - range start - * @param end - range end - * @param data - optional tool's data + * @param params - format parameters + * @param params.tool - tool to apply + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param params.start - range start + * @param params.end - range end + * @param params.data - optional tool's data */ - public format( - tool: string, - blockIndexOrId: number | string, - dataKey: string, - start: number, - end: number, - data?: Record - ): void { + public format({ tool, block, key, start, end, data }: Parameters[0]): void { this.#model.format( this.#config.userId, - blockIndexOrId as BlockIndexOrId, - createDataKey(dataKey), + block as BlockIndexOrId, + createDataKey(key), createInlineToolName(tool), start, end, @@ -112,23 +98,18 @@ export class TextAPI implements TextAPIInterface { /** * Unformats the given range - * @param tool - tool to remove - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - range start - * @param end - range end + * @param params - unformat parameters + * @param params.tool - tool to remove + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param params.start - range start + * @param params.end - range end */ - public unformat( - tool: string, - blockIndexOrId: number | string, - dataKey: string, - start: number, - end: number - ): void { + public unformat({ tool, block, key, start, end }: Parameters[0]): void { this.#model.unformat( this.#config.userId, - blockIndexOrId as BlockIndexOrId, - createDataKey(dataKey), + block as BlockIndexOrId, + createDataKey(key), createInlineToolName(tool), start, end @@ -137,22 +118,17 @@ export class TextAPI implements TextAPIInterface { /** * Returns applied inline fragments for a given range - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - range start - * @param end - range end - * @param tool - optional filter tool. If provided, will return only fragments of the given tool + * @param params - getFragments parameters + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param params.start - range start + * @param params.end - range end + * @param params.tool - optional filter tool. If provided, will return only fragments of the given tool */ - public getFragments( - blockIndexOrId: number | string, - dataKey: string, - start?: number, - end?: number, - tool?: string - ): InlineFragment[] { + public getFragments({ block, key, start, end, tool }: Parameters[0]): InlineFragment[] { return this.#model.getFragments( - blockIndexOrId as BlockIndexOrId, - createDataKey(dataKey), + block as BlockIndexOrId, + createDataKey(key), start, end, tool !== undefined ? createInlineToolName(tool) : undefined @@ -161,16 +137,14 @@ export class TextAPI implements TextAPIInterface { /** * Returns text content of the text node - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node + * @param params - get parameters + * @param params.block - block index or identifier + * @param params.key - data key of the text node */ - public get( - blockIndexOrId: number | string, - dataKey: string - ): string { + public get({ block, key }: Parameters[0]): string { return this.#model.getText( - blockIndexOrId as BlockIndexOrId, - createDataKey(dataKey) + block as BlockIndexOrId, + createDataKey(key) ); } } diff --git a/packages/core/src/plugins/ShortcutsPlugin.ts b/packages/core/src/plugins/ShortcutsPlugin.ts index 47251b04..df565fcf 100644 --- a/packages/core/src/plugins/ShortcutsPlugin.ts +++ b/packages/core/src/plugins/ShortcutsPlugin.ts @@ -1,4 +1,3 @@ -import type { InlineToolName } from '@editorjs/model'; import type { BlockToolFacade, BlockTuneFacade, @@ -100,7 +99,7 @@ export class ShortcutsPlugin implements EditorjsPlugin { */ #processInlineTool(toolName: string): void { try { - this.#api.selection.applyInlineToolForCurrentSelection(toolName as InlineToolName); + this.#api.selection.applyInlineTool({ tool: toolName }); } catch (error) { if (error instanceof IndexError) { /** diff --git a/packages/dom-adapters/src/BlockToolAdapter/index.ts b/packages/dom-adapters/src/BlockToolAdapter/index.ts index 9958bbaa..2c29efa7 100644 --- a/packages/dom-adapters/src/BlockToolAdapter/index.ts +++ b/packages/dom-adapters/src/BlockToolAdapter/index.ts @@ -50,6 +50,9 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { */ #beforeInputListener: EventListener; + /** + * Editor API instance + */ #api: EditorAPI; /** @@ -134,8 +137,14 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { return; } - const value = this.#api.text.get(this.blockId, key); - const fragments = this.#api.text.getFragments(this.blockId, key); + const value = this.#api.text.get({ + block: this.blockId, + key, + }); + const fragments = this.#api.text.getFragments({ + block: this.blockId, + key, + }); this.#inputsRegistry.register(this.blockId, key, input); @@ -281,7 +290,7 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { * Middle block in a cross-input selection: remove the whole block, not the same as removeText(0, length). */ if (isInputInBetweenSelection(input, range)) { - this.#api.blocks.delete(this.blockId); + this.#api.blocks.delete({ block: this.blockId }); return; } @@ -309,7 +318,12 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { } const [start, end] = clipped; - const removedText = this.#api.text.remove(this.blockId, key, start, end); + const removedText = this.#api.text.remove({ + block: this.blockId, + key, + start, + end, + }); let newCaretIndex: number | null = null; @@ -375,7 +389,12 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { if (data !== undefined && input.contains(range.startContainer)) { start = getAbsoluteRangeOffset(input, range.startContainer, range.startOffset); - this.#api.text.insert(data, this.blockId, key, start); + this.#api.text.insert({ + text: data, + block: this.blockId, + key, + start, + }); } break; } @@ -387,7 +406,12 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { if (data !== undefined && input.contains(range.startContainer)) { start = getAbsoluteRangeOffset(input, range.startContainer, range.startOffset); - this.#api.text.insert(data, this.blockId, key, start); + this.#api.text.insert({ + text: data, + block: this.blockId, + key, + start, + }); } break; } @@ -443,10 +467,14 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { */ #handleSplit(key: string, start: number, end: number): void { const currentBlockIndex = this.#api.blocks.getIndexById(this.blockId); - const currentValue = this.#api.text.get(this.blockId, key); + const currentValue = this.#api.text.get({ block: this.blockId, + key }); const newValueAfter = currentValue.slice(end); - const relatedFragments = this.#api.text.getFragments(this.blockId, key, end, currentValue.length); + const relatedFragments = this.#api.text.getFragments({ block: this.blockId, + key, + start: end, + end: currentValue.length }); /** * Fragment ranges bounds should be decreased by end index, because end is the index of the first character of the new block @@ -456,21 +484,26 @@ export class DOMBlockToolAdapter extends BlockToolAdapter { fragment.range[1] -= end; }); - this.#api.text.remove(this.blockId, key, start, currentValue.length); - this.#api.blocks.insert( + this.#api.text.remove({ + block: this.blockId, + key, + start, + end: currentValue.length, + }); + this.#api.blocks.insert({ /** * @todo when implementing split/merge, think of how to not use toolname here */ - this.#toolName, - { + type: this.#toolName, + data: { [key]: { $t: 't', value: newValueAfter, fragments: relatedFragments, }, }, - currentBlockIndex + 1 - ); + index: currentBlockIndex + 1, + }); /** * Raf is needed to ensure that the new block is added so caret can be moved to it diff --git a/packages/dom-adapters/src/FormattingAdapter/index.ts b/packages/dom-adapters/src/FormattingAdapter/index.ts index 5c70eb81..eb2e91d1 100644 --- a/packages/dom-adapters/src/FormattingAdapter/index.ts +++ b/packages/dom-adapters/src/FormattingAdapter/index.ts @@ -155,7 +155,12 @@ export class FormattingAdapter { const rangeStart = Math.max(0, textRange[0] - 1); const rangeEnd = inputContent !== null ? Math.min(inputContent.length, textRange[1] + 1) : 0; - const affectedFragments = this.#api.text.getFragments(blockIndex, dataKey as string, rangeStart, rangeEnd); + const affectedFragments = this.#api.text.getFragments({ + block: blockIndex, + key: dataKey as string, + start: rangeStart, + end: rangeEnd, + }); const leftBoundary = affectedFragments[0]?.range[0] ?? textRange[0]; let rightBoundary = textRange[1]; diff --git a/packages/sdk/src/api/BlocksAPI.ts b/packages/sdk/src/api/BlocksAPI.ts index 4b14b1b1..81d2ef28 100644 --- a/packages/sdk/src/api/BlocksAPI.ts +++ b/packages/sdk/src/api/BlocksAPI.ts @@ -15,21 +15,28 @@ export interface BlocksAPI { /** * Inserts a new block to the editor * @todo return block api? - * @param type - Block tool name to insert - * @param data - Block's initial data - * @param index - index to insert block at - * @param focus - flag indicates if new block should be focused @todo implement - * @param replace - flag indicates if block at index should be replaced @todo implement - * @param id - id of the inserted block @todo implement - */ - insert( - type?: string, - data?: BlockToolData, - index?: number, - focus?: boolean, - replace?: boolean, - id?: string - ): void; + * @param [params] - optional insert parameters + * @param [params.type] - Block tool name to insert, inserts default block if not specified + * @param [params.data] - Block's initial data + * @param [params.index] - index to insert block at + * @param [params.focus] - flag indicates if new block should be focused @todo implement + * @param [params.replace] - flag indicates if block at index should be replaced @todo implement + * @param [params.id] - id of the inserted block @todo implement + */ + insert(params?: { + /** Block tool name to insert */ + type?: string; + /** Block's initial data */ + data?: BlockToolData; + /** Index to insert block at */ + index?: number; + /** Flag indicates if new block should be focused */ + focus?: boolean; + /** Flag indicates if block at index should be replaced */ + replace?: boolean; + /** Id of the inserted block */ + id?: string; + }): void; /** * Remove all blocks from Document @@ -50,17 +57,27 @@ export interface BlocksAPI { // renderFromHTML(data: string): Promise; /** - * Removes Block by index, or current block if index is not passed - * @param indexOrId - index or id of a block to delete + * Removes Block by index or id, or current block if params are not passed + * @param [params] - optional delete parameters + * @param [params.block] - index or id of a block to delete */ - delete(indexOrId?: number | string): void; + delete(params?: { + /** Index or id of a block to delete */ + block?: number | string; + }): void; /** * Moves a block to a new index - * @param toIndex - index where the block is moved to - * @param [fromIndex] - block to move. Current block if not passed + * @param params - move parameters + * @param params.toIndex - index where the block is moved to + * @param [params.fromIndex] - block to move. Current block if not passed */ - move(toIndex: number, fromIndex?: number): void; + move(params: { + /** Index where the block is moved to */ + toIndex: number; + /** Block to move. Current block if not passed */ + fromIndex?: number; + }): void; /** * Returns Block API object by passed Block index @@ -98,13 +115,16 @@ export interface BlocksAPI { /** * Inserts several Blocks to specified index - * @param blocks - array of blocks to insert - * @param [index] - index to insert blocks at. If undefined, inserts at the end + * @param params - insertMany parameters + * @param params.blocks - array of blocks to insert + * @param [params.index] - index to insert blocks at. If undefined, inserts at the end */ - insertMany( - blocks: BlockNodeInit[], - index?: number, - ): void; // BlockAPI[]; + insertMany(params: { + /** Array of blocks to insert */ + blocks: BlockNodeInit[]; + /** Index to insert blocks at. If undefined, inserts at the end */ + index?: number; + }): void; /** * Returns block's index by its id @@ -120,33 +140,61 @@ export interface BlocksAPI { /** * Returns serialized data for provided data key - * @param indexOrId - index or id of the block - * @param dataKey - data key to get + * @param params - getData parameters + * @param params.block - index or id of the block + * @param params.key - data key to get */ - getData(indexOrId: number | string, dataKey: string): TextNodeSerialized | ValueSerialized | undefined; + getData(params: { + /** Index or id of the block */ + block: number | string; + /** Data key to get */ + key: string; + }): TextNodeSerialized | ValueSerialized | undefined; /** * Removes data by the data key - * @param indexOrId - index or id of the block - * @param dataKey - data key to remove + * @param params - removeData parameters + * @param params.block - index or id of the block + * @param params.key - data key to remove */ - removeData(indexOrId: number | string, dataKey: string): void; + removeData(params: { + /** Index or id of the block */ + block: number | string; + /** Data key to remove */ + key: string; + }): void; /** * Creates data node with the given key - * @param indexOrId - index or id of the block - * @param dataKey - data key to create - * @param [initialData] - optional initial data + * @param params - createData parameters + * @param params.block - index or id of the block + * @param params.key - data key to create + * @param [params.initialData] - optional initial data */ - createData(indexOrId: number | string, dataKey: string, initialData?: TextNodeSerialized | ValueSerialized): void; + createData(params: { + /** Index or id of the block */ + block: number | string; + /** Data key to create */ + key: string; + /** Optional initial data */ + initialData?: TextNodeSerialized | ValueSerialized; + }): void; /** * Updates value by the given key - * @param indexOrId - index or id of the block - * @param dataKey - data key to update - * @param value - new value - */ - updateValue(indexOrId: number | string, dataKey: string, value: V): void; + * @param params - updateValue parameters + * @param params.block - index or id of the block + * @param params.key - data key to update + * @param params.value - new value + */ + updateValue(params: { + /** Index or id of the block */ + block: number | string; + /** Data key to update */ + key: string; + /** New value */ + value: V; + }): void; /** * Creates data of an empty block with a passed type. diff --git a/packages/sdk/src/api/SelectionAPI.ts b/packages/sdk/src/api/SelectionAPI.ts index c7cfe305..6d2bb551 100644 --- a/packages/sdk/src/api/SelectionAPI.ts +++ b/packages/sdk/src/api/SelectionAPI.ts @@ -1,4 +1,4 @@ -import type { Caret, CaretManagerEvents, InlineToolName } from '@editorjs/model'; +import type { Caret, CaretManagerEvents } from '@editorjs/model'; /** * Selection API interface @@ -7,10 +7,12 @@ import type { Caret, CaretManagerEvents, InlineToolName } from '@editorjs/model' export interface SelectionAPI { /** * Applies inline tool for the current selection - * @param tool - name of the inline tool to apply - * @param data - optional data for the inline tool + * @param params - method parameters + * @param params.tool - name of the inline tool to apply + * @param [params.data] - optional data for the inline tool */ - applyInlineToolForCurrentSelection(tool: InlineToolName, data?: Record): void; + // eslint-disable-next-line jsdoc/require-jsdoc,@stylistic/object-property-newline -- type declaration + applyInlineTool({ tool, data }: { tool: string; data?: Record }): void; /** * Registers a callback for CaretManager updates. Returns a cleanup function diff --git a/packages/sdk/src/api/TextAPI.ts b/packages/sdk/src/api/TextAPI.ts index 2d95f9ab..fe3bc6b5 100644 --- a/packages/sdk/src/api/TextAPI.ts +++ b/packages/sdk/src/api/TextAPI.ts @@ -2,95 +2,109 @@ import type { InlineFragment } from '@editorjs/model'; +/** + * Interface representing text position within a block + */ +interface TextPosition { + /** + * Block identifier or index + */ + block: string | number; + /** + * Data key + */ + key: string; + /** + * Text range start + */ + start?: number; + /** + * Text range end + */ + end?: number; +} + +/** + * Interface representing text content + */ +interface TextContent { + /** + * Text content + */ + text: string; +} + +/** + * Interface representing Inline Tool Data + */ +interface InlineToolData { + /** + * Tool name + */ + tool: string; + /** + * Tool data + */ + data?: Record; +} + /** * Editor's TextAPI to work with text content of the document */ export interface TextAPI { /** * Inserts text at a given position - * @param text - new text to insert - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - start offset + * @param params.text - new text to insert + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param [params.start] - start offset */ - insert( - text: string, - blockIndexOrId: number | string, - dataKey: string, - start?: number - ): void; + insert(params: TextContent & Omit): void; /** * Removes text from a given range - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - range start - * @param end - range end + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param [params.start] - range start + * @param [params.end] - range end */ - remove( - blockIndexOrId: number | string, - dataKey: string, - start?: number, - end?: number - ): string; + remove(params: TextPosition): string; /** * Formats the given range - * @param tool - tool to apply - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - range start - * @param end - range end - * @param data - optional tool's data + * @param params.tool - tool to apply + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param params.start - range start + * @param params.end - range end + * @param [params.data] - optional tool's data */ - format( - tool: string, - blockIndexOrId: number | string, - dataKey: string, - start: number, - end: number, - data?: Record, - ): void; + format(params: InlineToolData & Required): void; /** * Unformats the given range - * @param tool - tool to remove - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - range start - * @param end - range end + * @param params.tool - tool to remove + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param params.start - range start + * @param params.end - range end */ - unformat( - tool: string, - blockIndexOrId: number | string, - dataKey: string, - start: number, - end: number - ): void; + unformat(params: Pick & Required): void; /** * Returns applied inline fragments for a given range - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node - * @param start - range start - * @param end - range end - * @param tool - optional filter tool. If provided, will return only fragments of the given tool + * @param params.block - block index or identifier + * @param params.key - data key of the text node + * @param params.start - range start + * @param params.end - range end + * @param [params.tool] - optional filter tool. If provided, will return only fragments of the given tool */ - getFragments( - blockIndexOrId: number | string, - dataKey: string, - start?: number, - end?: number, - tool?: string - ): InlineFragment[]; + getFragments(params: Partial> & TextPosition): InlineFragment[]; /** * Returns text content of the text node - * @param blockIndexOrId - block index or identifier - * @param dataKey - data key of the text node + * @param params.block - block index or identifier + * @param params.key - data key of the text node */ - get( - blockIndexOrId: number | string, - dataKey: string - ): string; + get(params: Pick): string; } diff --git a/packages/sdk/src/entities/BlockToolAdapter.ts b/packages/sdk/src/entities/BlockToolAdapter.ts index 10059cb4..62534edd 100644 --- a/packages/sdk/src/entities/BlockToolAdapter.ts +++ b/packages/sdk/src/entities/BlockToolAdapter.ts @@ -113,7 +113,11 @@ export abstract class BlockToolAdapter extends EventTarget { this.#createDataNode(key, initialData); return (newValue: V) => { - this.#api.blocks.updateValue(this.blockId, key, newValue); + this.#api.blocks.updateValue({ + block: this.blockId, + key, + value: newValue, + }); }; } @@ -122,11 +126,17 @@ export abstract class BlockToolAdapter extends EventTarget { * @param key - key of the node to remove */ public removeKey(key: string): void { - if (this.#api.blocks.getData(this.blockId, key) === undefined) { + if (this.#api.blocks.getData({ + block: this.blockId, + key, + }) === undefined) { return; } - this.#api.blocks.removeData(this.blockId, key); + this.#api.blocks.removeData({ + block: this.blockId, + key, + }); } /** @@ -141,11 +151,18 @@ export abstract class BlockToolAdapter extends EventTarget { * this.#createDataNode(createDataKey('items[0].content'), { $t: 'v', value: 'Item text' }); */ #createDataNode(key: string, initialData?: TextNodeSerialized | ValueSerialized): void { - if (this.#api.blocks.getData(this.blockId, key) !== undefined) { + if (this.#api.blocks.getData({ + block: this.blockId, + key, + }) !== undefined) { return; } - this.#api.blocks.createData(this.blockId, key, initialData); + this.#api.blocks.createData({ + block: this.blockId, + key, + initialData, + }); } /** diff --git a/packages/ui/src/InlineToolbar/InlineToolbar.ts b/packages/ui/src/InlineToolbar/InlineToolbar.ts index d6fc0070..dcb6d7ec 100644 --- a/packages/ui/src/InlineToolbar/InlineToolbar.ts +++ b/packages/ui/src/InlineToolbar/InlineToolbar.ts @@ -185,7 +185,7 @@ export class InlineToolbarUI implements EditorjsPlugin { }); } else { button.addEventListener('click', () => { - this.#api.selection.applyInlineToolForCurrentSelection(name); + this.#api.selection.applyInlineTool({ tool: name }); }); } @@ -200,7 +200,10 @@ export class InlineToolbarUI implements EditorjsPlugin { */ #renderToolActions(name: InlineToolName, tool: InlineTool): void { const { element } = tool.renderActions?.((data: InlineToolFormatData) => { - this.#api.selection.applyInlineToolForCurrentSelection(name, data); + this.#api.selection.applyInlineTool({ + tool: name, + data, + }); }) ?? { element: null }; if (element === null) { diff --git a/packages/ui/src/Toolbox/Toolbox.ts b/packages/ui/src/Toolbox/Toolbox.ts index 9c3b25fc..d3df296a 100644 --- a/packages/ui/src/Toolbox/Toolbox.ts +++ b/packages/ui/src/Toolbox/Toolbox.ts @@ -144,12 +144,12 @@ export class ToolboxUI implements EditorjsPlugin { ...toolbox, closeOnActivate: true, onActivate: () => { - void this.#api.blocks.insert( - tool.name, - toolbox.data ?? {}, - this.#selectedBlockIndex === -1 ? undefined : this.#selectedBlockIndex + 1, - true - ); + void this.#api.blocks.insert({ + type: tool.name, + data: toolbox.data ?? {}, + index: this.#selectedBlockIndex === -1 ? undefined : this.#selectedBlockIndex + 1, + focus: true, + }); }, } ); From 5fbf7cf9eb46561873e9b528d7965e83c8577667 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Fri, 15 May 2026 22:13:24 +0100 Subject: [PATCH 5/5] Copilot suggestions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/core/src/api/SelectionAPI.ts | 2 +- packages/core/src/api/TextAPI.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/api/SelectionAPI.ts b/packages/core/src/api/SelectionAPI.ts index f7bfa9f8..7c968e40 100644 --- a/packages/core/src/api/SelectionAPI.ts +++ b/packages/core/src/api/SelectionAPI.ts @@ -5,7 +5,7 @@ import { SelectionManager } from '../components/SelectionManager.js'; import { Caret, CaretManagerEvents, createInlineToolName, EditorJSModel, EventType } from '@editorjs/model'; import { CoreConfigValidated } from '@editorjs/sdk'; import { SelectionAPI as SelectionApiInterface } from '@editorjs/sdk'; -import { TOKENS } from '../tokens'; +import { TOKENS } from '../tokens.js'; /** * Selection API class diff --git a/packages/core/src/api/TextAPI.ts b/packages/core/src/api/TextAPI.ts index 1e51d74e..3948fa66 100644 --- a/packages/core/src/api/TextAPI.ts +++ b/packages/core/src/api/TextAPI.ts @@ -7,7 +7,7 @@ import { } from '@editorjs/model'; import type { CoreConfigValidated } from '@editorjs/sdk'; import { inject, injectable } from 'inversify'; -import { TOKENS } from '../tokens'; +import { TOKENS } from '../tokens.js'; import { TextAPI as TextAPIInterface } from '@editorjs/sdk'; /**