diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 519bc99d..5573c109 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -import { EditorJSModel } from '@editorjs/model'; +import { EditorJSModel, EventType } from '@editorjs/model'; import type { ContainerInstance } from 'typedi'; import { Container } from 'typedi'; import { composeDataFromVersion2 } from './utils/composeDataFromVersion2.js'; @@ -62,8 +62,10 @@ export default class Core { * Inline toolbar is responsible for handling selection changes * When model selection changes, it determines, whenever to show toolbar element, * Which calls apply format method of the adapter + * + * null when inline toolbar is not initialized */ - #inlineToolbar: InlineToolbar; + #inlineToolbar: InlineToolbar | null = null; /** * @param config - Editor configuration @@ -92,19 +94,26 @@ export default class Core { this.#formattingAdapter = new FormattingAdapter(this.#model, this.#caretAdapter); this.#iocContainer.set(FormattingAdapter, this.#formattingAdapter); - this.#inlineToolbar = new InlineToolbar(this.#model, this.#formattingAdapter, this.#toolsManager.inlineTools, this.#config.holder); - this.#iocContainer.set(InlineToolbar, this.#inlineToolbar); + this.#iocContainer.get(BlocksManager); - this.#prepareUI(); + if (config.onModelUpdate !== undefined) { + this.#model.addEventListener(EventType.Changed, () => { + config.onModelUpdate?.(this.#model); + }); + } - this.#iocContainer.get(BlocksManager); + this.#prepareUI(); - /** - * @todo avait when isReady API is implemented - */ - void this.#toolsManager.prepareTools(); + this.#toolsManager.prepareTools() + .then(() => { + this.#inlineToolbar = new InlineToolbar(this.#model, this.#formattingAdapter, this.#toolsManager.inlineTools, this.#config.holder); + this.#iocContainer.set(InlineToolbar, this.#inlineToolbar); - this.#model.initializeDocument({ blocks }); + this.#model.initializeDocument({ blocks }); + }) + .catch((error) => { + console.error('Editor.js initialization failed', error); + }); } /** diff --git a/packages/core/src/utils/composeDataFromVersion2.ts b/packages/core/src/utils/composeDataFromVersion2.ts index 3249c2ce..972a8f1c 100644 --- a/packages/core/src/utils/composeDataFromVersion2.ts +++ b/packages/core/src/utils/composeDataFromVersion2.ts @@ -2,6 +2,14 @@ import type { OutputData } from '@editorjs/editorjs'; import type { InlineFragment } from '@editorjs/model'; import { createInlineToolData, createInlineToolName, TextNode, ValueNode, type BlockNodeSerialized } from '@editorjs/model'; +/** + * Removes HTML tags from the input string + * @param input - any string with HTML tags like 'bold link' + */ +function stripTags(input: string): string { + return input.replace(/<\/?[^>]+(>|$)/g, ''); +} + /** * Extracts inline fragments from the HTML string * @param html - any html string like 'bold link' @@ -97,7 +105,7 @@ export function composeDataFromVersion2(data: OutputData): { if (typeof value === 'string') { const fragments = extractFragments(value); const textNode = new TextNode({ - value, + value: stripTags(value), fragments, }); diff --git a/packages/dom-adapters/src/BlockToolAdapter/index.ts b/packages/dom-adapters/src/BlockToolAdapter/index.ts index 87ee4699..2222c06d 100644 --- a/packages/dom-adapters/src/BlockToolAdapter/index.ts +++ b/packages/dom-adapters/src/BlockToolAdapter/index.ts @@ -89,11 +89,15 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { this.#caretAdapter.attachInput(input, builder.build()); try { + const value = this.#model.getText(this.#blockIndex, key); const fragments = this.#model.getFragments(this.#blockIndex, key); + input.textContent = value; + + const nodeToFormat = input.firstChild as HTMLElement; // we just set textContent, so it's always a TextNode + fragments.forEach(fragment => { - console.log('fragment', fragment); - // this.#formattingAdapter.formatElementContent(input, fragment); + this.#formattingAdapter.formatElementContent(nodeToFormat, fragment); }); } catch (_) { // do nothing — TextNode is not created yet as there is no initial data in the model diff --git a/packages/dom-adapters/src/FormattingAdapter/index.ts b/packages/dom-adapters/src/FormattingAdapter/index.ts index cf32f09a..550e488d 100644 --- a/packages/dom-adapters/src/FormattingAdapter/index.ts +++ b/packages/dom-adapters/src/FormattingAdapter/index.ts @@ -101,17 +101,21 @@ export class FormattingAdapter { const [start, end] = index; - /** - * Create range with positions specified in index - */ - const range = document.createRange(); + try { + /** + * Create range with positions specified in index + */ + const range = document.createRange(); - range.setStart(input, start); - range.setEnd(input, end); + range.setStart(input, start); + range.setEnd(input, end); - const inlineElement = tool.createWrapper(toolData); + const inlineElement = tool.createWrapper(toolData); - surround(range, inlineElement); + surround(range, inlineElement); + } catch (e) { + console.error('Error while formatting element content', e); + } } /** diff --git a/packages/model/src/EditorJSModel.spec.ts b/packages/model/src/EditorJSModel.spec.ts index 53d9fc48..4f362303 100644 --- a/packages/model/src/EditorJSModel.spec.ts +++ b/packages/model/src/EditorJSModel.spec.ts @@ -17,6 +17,7 @@ describe('EditorJSModel', () => { 'updateValue', 'removeBlock', 'moveBlock', + 'getText', 'insertText', 'removeText', 'format', @@ -25,6 +26,7 @@ describe('EditorJSModel', () => { 'createCaret', 'updateCaret', 'removeCaret', + 'devModeGetDocument', ]; const ownProperties = Object.getOwnPropertyNames(EditorJSModel.prototype); diff --git a/packages/model/src/EditorJSModel.ts b/packages/model/src/EditorJSModel.ts index eca41d02..44194f02 100644 --- a/packages/model/src/EditorJSModel.ts +++ b/packages/model/src/EditorJSModel.ts @@ -234,6 +234,17 @@ export class EditorJSModel extends EventBus { return this.#document.updateTuneData(...parameters); } + /** + * Returns a text from the specified block and data key + * + * @param parameters - getText method parameters + * @param parameters.blockIndex - index of the block + * @param parameters.dataKey - key of the data + */ + public getText(...parameters: Parameters): ReturnType { + return this.#document.getText(...parameters); + } + /** * Inserts text to the specified block * @@ -303,6 +314,15 @@ export class EditorJSModel extends EventBus { return this.#document.getFragments(...parameters); } + /** + * Exposing document for dev-tools + * + * USE ONLY FOR DEV PURPOSES + */ + public devModeGetDocument(): EditorDocument { + return this.#document; + } + /** * Listens to BlockNode events and bubbles re-emits them from the EditorJSModel instance * diff --git a/packages/model/src/entities/BlockNode/BlockNode.spec.ts b/packages/model/src/entities/BlockNode/BlockNode.spec.ts index d13e0a9d..ff26fcb8 100644 --- a/packages/model/src/entities/BlockNode/BlockNode.spec.ts +++ b/packages/model/src/entities/BlockNode/BlockNode.spec.ts @@ -11,7 +11,7 @@ import type { EditorDocument } from '../EditorDocument'; import type { ValueNodeConstructorParameters } from '../ValueNode'; import type { InlineFragment, InlineToolData, InlineToolName } from '../inline-fragments'; import { TextNode } from '../inline-fragments/index.js'; -import type { BlockNodeData, BlockNodeDataSerialized } from './types'; +import type { BlockNodeData, BlockNodeDataSerialized, DataKey } from './types'; import { BlockChildType } from './types/index.js'; import { NODE_TYPE_HIDDEN_PROP } from './consts.js'; import { TextAddedEvent, TuneModifiedEvent, ValueModifiedEvent } from '../../EventBus/events/index.js'; @@ -574,6 +574,31 @@ describe('BlockNode', () => { }); }); + describe('.getText()', () => { + it('should call .serialized getter of the TextNode', () => { + const spy = jest.spyOn(TextNode.prototype, 'serialized', 'get'); + const node = createBlockNodeWithData({ + text: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); + + node.getText(createDataKey('text')); + + expect(spy) + .toHaveBeenCalled(); + }); + + it('should throw an error if data key is invalid', () => { + const node = createBlockNodeWithData({}); + + expect(() => node.getText('invalid-key' as DataKey)) + .toThrow(); + }); + }); + describe('.insertText()', () => { const dataKey = createDataKey('text'); const text = 'Some text'; diff --git a/packages/model/src/entities/BlockNode/__mocks__/index.ts b/packages/model/src/entities/BlockNode/__mocks__/index.ts index 692a765e..be52ddde 100644 --- a/packages/model/src/entities/BlockNode/__mocks__/index.ts +++ b/packages/model/src/entities/BlockNode/__mocks__/index.ts @@ -18,6 +18,13 @@ export class BlockNode extends EventBus { return; } + /** + * Mock method + */ + public getText(): string { + return 'mocked text'; + } + /** * Mock method */ diff --git a/packages/model/src/entities/BlockNode/index.ts b/packages/model/src/entities/BlockNode/index.ts index e30de268..ab958450 100644 --- a/packages/model/src/entities/BlockNode/index.ts +++ b/packages/model/src/entities/BlockNode/index.ts @@ -191,6 +191,19 @@ export class BlockNode extends EventBus { node.update(value); } + /** + * Returns a text value for the specified data key + * + * @param dataKey - key of the data + */ + public getText(dataKey: DataKey): string { + this.#validateKey(dataKey, TextNode); + + const node = get(this.#data, dataKey as string) as TextNode; + + return node.serialized.value; + } + /** * Inserts text to the specified text node by index, by default appends text to the end of the current value * diff --git a/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts b/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts index cc9c8b1b..20e574c7 100644 --- a/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts +++ b/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts @@ -729,6 +729,42 @@ describe('EditorDocument', () => { }); }); + describe('.getText()', () => { + let document: EditorDocument; + const dataKey = 'text' as DataKey; + const text = 'Some text'; + let block: BlockNode; + + beforeEach(() => { + const blockData = { + name: 'text' as BlockToolName, + data: { + [dataKey]: text, + }, + }; + + document = new EditorDocument(); + + document.initialize([ blockData ]); + + block = document.getBlock(0); + }); + + it('should call .getText() method of the BlockNode if index and data key are correct', () => { + const spy = jest.spyOn(block, 'getText'); + + document.getText(0, dataKey); + + expect(spy) + .toHaveBeenCalledWith(dataKey); + }); + + it('should throw an error if index is out of bounds', () => { + expect(() => document.getText(document.length + 1, dataKey)) + .toThrow('Index out of bounds'); + }); + }); + describe('.insertText()', () => { let document: EditorDocument; const dataKey = 'text' as DataKey; diff --git a/packages/model/src/entities/EditorDocument/index.ts b/packages/model/src/entities/EditorDocument/index.ts index 60590faa..67059b0a 100644 --- a/packages/model/src/entities/EditorDocument/index.ts +++ b/packages/model/src/entities/EditorDocument/index.ts @@ -238,6 +238,18 @@ export class EditorDocument extends EventBus { this.#children[blockIndex].updateTuneData(tuneName, data); } + /** + * Returns text for the specified block and data key + * + * @param blockIndex - index of the block + * @param dataKey - key of the data containing the text + */ + public getText(blockIndex: number, dataKey: DataKey): string { + this.#checkIndexOutOfBounds(blockIndex, this.length - 1); + + return this.#children[blockIndex].getText(dataKey); + } + /** * Inserts text to the specified block * diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index 0c8d0ccc..1f12564d 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -1,61 +1,23 @@