From efcfe2220b15b7b062407f0390806d1be8837d00 Mon Sep 17 00:00:00 2001 From: Nikita Melnikov Date: Fri, 30 Aug 2024 22:39:05 +0300 Subject: [PATCH] Add new model methods for inserting/removing data by index (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix tests * use new method in the model * add ci for collab manager * lint fixes * add getters for the index * fix tests * move methods to the document * lint * use helpers * add tests * Introduce DI & migrate tools builders (#80) * Introduce DI & migrate tools builders * Fixes after review * 'Fix' lint * Add unsaved files * Fix comments * Remove log & add docs * Update packages/core/src/entities/UnifiedToolConfig.ts Co-authored-by: Peter * Fixes after review --------- Co-authored-by: Peter * Add BlockManager and EditorUI classes (#85) * Add BlockManager and EditorUI classes * Add JSDocs * Add try catch for block.render() call * feat(core): inputs content rendering (#86) * passing formattingadapter to blocktooladapter * lint * Update BlockManager.ts * Update BlockManager.ts * Update App.vue * Add Toolbox & ToolboxUI (#88) * Add Toolbox & ToolboxUI * Add comments * Update tests for the model package * Fix lint * feat(dom-adapters): Inline tool adapter check for tool required data (#87) * Implmenet global CaretAdapter * Handle native inputs * Pass input type to Input component props * Use class to represent index * Fix lint in dom-adapters * fix linter * added inline tool adapter * implement model updates * lint fix * fix index * adapter renders inline tools * lint fix and clean up * jsdoc * clean up * jsdoc * jsdoc * surround content replaced * suggestions * lint fix * jsdoc * added bold and italic inline tools into core package * naming * naming * added inline toolbar and inlineToolAdapter init into core * update packages and lock * build fix * implement inline tool adapter to core - fully implement current realization of inline tool adapter to core - remove from the playground * clean up * jsdoc and naming improvements * naming * naming * renaming * fix hardcoded * tools are initialized inside of the inline toolbar initialization * fixed inline tool attaching * jsdoc * naming fix * fixed imports * lint fix * try build fix * install dependencies * add sdk package * fix build for core * change package name in actions * add references * typo * fix build * added inline tool data former * fix lint * rm unwanted changes * lint fix * fixed build * docs improved * fix build * naming improved * Update packages/core/src/ui/InlineToolbar/index.ts Co-authored-by: Peter * Update packages/dom-adapters/src/FormattingAdapter/index.ts Co-authored-by: Peter * rm unwanted change * naming * separated renderToolActions and apply method in formatting adapter * naming * moved surround to utils * lint fix * last naming fix 🤞 * made renderToolActions method private --------- Co-authored-by: George Berezhnoy Co-authored-by: George Berezhnoy Co-authored-by: Peter * fix linter --------- Co-authored-by: George Berezhnoy Co-authored-by: Peter Co-authored-by: e11sy <130844513+e11sy@users.noreply.github.com> Co-authored-by: George Berezhnoy --- .../src/CollaborationManager.ts | 4 +- packages/model/src/EditorJSModel.spec.ts | 2 + packages/model/src/EditorJSModel.ts | 20 ++++ .../EditorDocument/EditorDocument.spec.ts | 92 +++++++++++++++++++ .../src/entities/EditorDocument/index.ts | 41 +++++++++ .../model/src/entities/Index/Index.spec.ts | 62 +++++++++++++ packages/model/src/entities/Index/index.ts | 14 +++ 7 files changed, 233 insertions(+), 2 deletions(-) diff --git a/packages/collaboration-manager/src/CollaborationManager.ts b/packages/collaboration-manager/src/CollaborationManager.ts index 00d7e6c3..04857a12 100644 --- a/packages/collaboration-manager/src/CollaborationManager.ts +++ b/packages/collaboration-manager/src/CollaborationManager.ts @@ -86,10 +86,10 @@ export class CollaborationManager { switch (operation.type) { case OperationType.Insert: - this.#model.insertText(blockIndex, dataKey, operation.data.newValue, textRange[0]); + this.#model.insertData(operation.index, operation.data.newValue); break; case OperationType.Delete: - this.#model.removeText(blockIndex, dataKey, textRange[0], textRange[1]); + this.#model.removeData(operation.index); break; case OperationType.Modify: console.log('modify operation is not implemented yet'); diff --git a/packages/model/src/EditorJSModel.spec.ts b/packages/model/src/EditorJSModel.spec.ts index 7bd1af8b..53d9fc48 100644 --- a/packages/model/src/EditorJSModel.spec.ts +++ b/packages/model/src/EditorJSModel.spec.ts @@ -11,6 +11,8 @@ describe('EditorJSModel', () => { 'initializeDocument', 'setProperty', 'addBlock', + 'insertData', + 'removeData', 'updateTuneData', 'updateValue', 'removeBlock', diff --git a/packages/model/src/EditorJSModel.ts b/packages/model/src/EditorJSModel.ts index f90ab397..eca41d02 100644 --- a/packages/model/src/EditorJSModel.ts +++ b/packages/model/src/EditorJSModel.ts @@ -1,5 +1,6 @@ // Stryker disable all -- we don't count mutation test coverage fot this file as it just proxy calls to EditorDocument /* istanbul ignore file -- we don't count test coverage fot this file as it just proxy calls to EditorDocument */ +import type { Index } from './entities/index.js'; import { type BlockNodeSerialized, EditorDocument } from './entities/index.js'; import { EventBus, EventType } from './EventBus/index.js'; import type { ModelEvents, CaretManagerCaretUpdatedEvent, CaretManagerEvents } from './EventBus/index.js'; @@ -188,6 +189,25 @@ export class EditorJSModel extends EventBus { return this.#document.removeBlock(...parameters); } + /** + * Inserts data to the specified index + * + * @param index - index to insert data + * @param data - data to insert + */ + public insertData(index: Index, data: unknown): void { + this.#document.insertData(index, data); + } + + /** + * Removes data from the specified index + * + * @param index - index to remove data from + */ + public removeData(index: Index): void { + this.#document.removeData(index); + } + /** * Updates the ValueNode data associated with the BlockNode at the specified index. * diff --git a/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts b/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts index e913f7e6..cc9c8b1b 100644 --- a/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts +++ b/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts @@ -774,6 +774,98 @@ describe('EditorDocument', () => { }); }); + describe('.insertData()', () => { + let document: EditorDocument; + const dataKey = 'text' as DataKey; + const text = 'Some text'; + const blockIndex = 0; + let block: BlockNode; + + beforeEach(() => { + const blockData = { + name: 'header' as BlockToolName, + data: {}, + }; + + document = new EditorDocument(); + + document.initialize([ blockData ]); + + block = document.getBlock(0); + }); + + it('should call .insertText() method if text index provided', () => { + const spy = jest.spyOn(document, 'insertText'); + const index = new IndexBuilder().addBlockIndex(blockIndex) + .addDataKey(dataKey) + .addTextRange([0, 0]) + .build(); + + document.insertData(index, text); + + expect(spy) + .toHaveBeenCalledWith(blockIndex, dataKey, text, 0); + }); + + it('should call .addBlock() if block index is provided', () => { + const spy = jest.spyOn(document, 'addBlock'); + const index = new IndexBuilder() + .addBlockIndex(blockIndex) + .build(); + + + document.insertData(index, block); + + expect(spy) + .toHaveBeenCalledWith(block, blockIndex); + }); + }); + + describe('.removeData()', () => { + let document: EditorDocument; + const dataKey = 'text' as DataKey; + const blockIndex = 0; + + beforeEach(() => { + const blockData = { + name: 'header' as BlockToolName, + data: {}, + }; + + document = new EditorDocument(); + + document.initialize([ blockData ]); + }); + + it('should call .removeText() method if text index provided', () => { + const spy = jest.spyOn(document, 'removeText'); + const rangeEnd = 5; + const index = new IndexBuilder() + .addBlockIndex(blockIndex) + .addDataKey(dataKey) + .addTextRange([0, rangeEnd]) + .build(); + + document.removeData(index); + + expect(spy) + .toHaveBeenCalledWith(blockIndex, dataKey, 0, rangeEnd); + }); + + it('should call .removeBlock() if block index is provided', () => { + const spy = jest.spyOn(document, 'removeBlock'); + const index = new IndexBuilder() + .addBlockIndex(blockIndex) + .build(); + + + document.removeData(index); + + expect(spy) + .toHaveBeenCalledWith(blockIndex); + }); + }); + describe('.removeText()', () => { 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 9e16d177..60590faa 100644 --- a/packages/model/src/entities/EditorDocument/index.ts +++ b/packages/model/src/entities/EditorDocument/index.ts @@ -22,6 +22,7 @@ import { } from '../../EventBus/events/index.js'; import type { Constructor } from '../../utils/types.js'; import { BaseDocumentEvent } from '../../EventBus/events/BaseEvent.js'; +import type { Index } from '../Index/index.js'; /** * EditorDocument class represents the top-level container for a tree-like structure of BlockNodes in an editor document. @@ -323,6 +324,46 @@ export class EditorDocument extends EventBus { return this.#children[blockIndex].getFragments(dataKey, start, end, tool); } + /** + * Inserts data to the specified index + * + * @param index - index to insert data + * @param data - data to insert + */ + public insertData(index: Index, data: unknown): void { + switch (true) { + case index.isTextIndex: + this.insertText(index.blockIndex!, index.dataKey!, data as string, index.textRange![0]); + break; + + case index.isBlockIndex: + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + this.addBlock(data as Parameters[0], index.blockIndex); + break; + default: + throw new Error('Unsupported index'); + } + } + + /** + * Removes data from the specified index + * + * @param index - index to remove data from + */ + public removeData(index: Index): void { + switch (true) { + case index.isTextIndex: + this.removeText(index.blockIndex!, index.dataKey!, index.textRange![0], index.textRange![1]); + break; + + case index.isBlockIndex: + this.removeBlock(index.blockIndex!); + break; + default: + throw new Error('Unsupported index'); + } + } + /** * Listens to BlockNode events and bubbles them to the EditorDocument * diff --git a/packages/model/src/entities/Index/Index.spec.ts b/packages/model/src/entities/Index/Index.spec.ts index 8976aebb..0eea4c0e 100644 --- a/packages/model/src/entities/Index/Index.spec.ts +++ b/packages/model/src/entities/Index/Index.spec.ts @@ -1,6 +1,7 @@ import type { DocumentIndex } from '../../EventBus/index.js'; import type { DataKey } from '../BlockNode/index.js'; import type { BlockTuneName } from '../BlockTune/index.js'; +import { IndexBuilder } from '../index.js'; import { Index } from './index.js'; describe('Index', () => { @@ -198,4 +199,65 @@ describe('Index', () => { expect(index.validate()).toBe(true); }); }); + + describe('.isBlockIndex', () => { + it('should return true if index points to the block', () => { + const index = new IndexBuilder().addBlockIndex(0) + .build(); + + expect(index.isBlockIndex).toBe(true); + }); + + it('should return false if index does not include block index', () => { + const index = new Index(); + + expect(index.isBlockIndex).toBe(false); + }); + + it('should return false if index points to the text range', () => { + const index = new IndexBuilder().addBlockIndex(0) + .addDataKey('dataKey' as DataKey) + .addTextRange([0, 0]) + .build(); + + expect(index.isBlockIndex).toBe(false); + }); + + it('should return false if index points to the tune data', () => { + const index = new IndexBuilder().addBlockIndex(0) + .addTuneName('tuneName' as BlockTuneName) + .addTuneKey('tuneKey') + .build(); + + expect(index.isBlockIndex).toBe(false); + }); + }); + + describe('.isTextIndex', () => { + it('should return true if index points to the text', () => { + const index = new IndexBuilder().addBlockIndex(0) + .addDataKey('dataKey' as DataKey) + .addTextRange([0, 0]) + .build(); + + expect(index.isTextIndex).toBe(true); + }); + + it('should return false if index does not include text range', () => { + const index = new IndexBuilder().addBlockIndex(0) + .addDataKey('dataKey' as DataKey) + .build(); + + expect(index.isTextIndex).toBe(false); + }); + + it('should return false if index points to the tune data', () => { + const index = new IndexBuilder().addBlockIndex(0) + .addTuneName('tuneName' as BlockTuneName) + .addTuneKey('tuneKey') + .build(); + + expect(index.isTextIndex).toBe(false); + }); + }); }); diff --git a/packages/model/src/entities/Index/index.ts b/packages/model/src/entities/Index/index.ts index 04ebdf9e..274024bf 100644 --- a/packages/model/src/entities/Index/index.ts +++ b/packages/model/src/entities/Index/index.ts @@ -145,4 +145,18 @@ export class Index { return true; } } + + /** + * Returns true if index points to the text data + */ + public get isTextIndex(): boolean { + return this.blockIndex !== undefined && this.dataKey !== undefined && this.textRange !== undefined; + } + + /** + * Returns true if index points to the block node + */ + public get isBlockIndex(): boolean { + return this.blockIndex !== undefined && this.tuneName === undefined && this.dataKey === undefined && this.textRange === undefined; + } }