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 @@