From 05107db47d9b6553c15aa27d318f827558ea5f5b Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Mon, 2 Sep 2024 23:30:48 +0100 Subject: [PATCH] Fix InlineParentNode.insertAfter method & add isActive state for inline toolbar buttons --- .../core-events/SelectionChangedCoreEvent.ts | 7 ++++++- .../core/src/components/SelectionManager.ts | 17 ++++++++++++++--- packages/core/src/ui/InlineToolbar/index.ts | 16 ++++++++++++---- .../inline-fragments/mixins/ParentNode/index.ts | 8 ++++++-- .../inline-fragments/specs/ParentNode.spec.ts | 13 +++++++++++++ packages/playground/src/App.vue | 3 +++ 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/packages/core/src/components/EventBus/core-events/SelectionChangedCoreEvent.ts b/packages/core/src/components/EventBus/core-events/SelectionChangedCoreEvent.ts index 80c49519..cd29d849 100644 --- a/packages/core/src/components/EventBus/core-events/SelectionChangedCoreEvent.ts +++ b/packages/core/src/components/EventBus/core-events/SelectionChangedCoreEvent.ts @@ -1,7 +1,7 @@ import type { InlineTool } from '@editorjs/sdk'; import { CoreEventBase } from './CoreEventBase.js'; import { CoreEventType } from './CoreEventType.js'; -import type { Index, InlineToolName } from '@editorjs/model'; +import type { Index, InlineFragment, InlineToolName } from '@editorjs/model'; /** * Payload of SelectionChangedCoreEvent custom event @@ -17,6 +17,11 @@ export interface SelectionChangedCoreEventPayload { * Inline tools available for the current selection */ readonly availableInlineTools: Map; + + /** + * Inline fragments available for the current selection + */ + readonly fragments: InlineFragment[]; } /** diff --git a/packages/core/src/components/SelectionManager.ts b/packages/core/src/components/SelectionManager.ts index 3c2cbed7..be92c9b6 100644 --- a/packages/core/src/components/SelectionManager.ts +++ b/packages/core/src/components/SelectionManager.ts @@ -1,6 +1,6 @@ import 'reflect-metadata'; import { FormattingAdapter } from '@editorjs/dom-adapters'; -import type { CaretManagerEvents, InlineToolName } from '@editorjs/model'; +import type { CaretManagerEvents, InlineFragment, InlineToolName } from '@editorjs/model'; import { CaretManagerCaretUpdatedEvent, Index, EditorJSModel, createInlineToolData, createInlineToolName } from '@editorjs/model'; import { EventType } from '@editorjs/model'; import { Service } from 'typedi'; @@ -73,15 +73,26 @@ export class SelectionManager { */ #handleCaretManagerUpdate(event: CaretManagerEvents): void { switch (true) { - case event instanceof CaretManagerCaretUpdatedEvent: + case event instanceof CaretManagerCaretUpdatedEvent: { + const { index: serializedIndex } = event.detail; + const index = serializedIndex !== null ? Index.parse(serializedIndex) : null; + let fragments: InlineFragment[] = []; + + if (index !== null && index.blockIndex !== undefined && index.dataKey !== undefined && index.textRange !== undefined) { + fragments = this.#model.getFragments(index.blockIndex, index.dataKey, ...index.textRange); + } + this.#eventBus.dispatchEvent(new SelectionChangedCoreEvent({ - index: event.detail.index !== null ? Index.parse(event.detail.index) : null, + index, /** * @todo implement filter by current BlockTool configuration */ availableInlineTools: this.#inlineTools, + fragments, })); + break; + } default: break; } diff --git a/packages/core/src/ui/InlineToolbar/index.ts b/packages/core/src/ui/InlineToolbar/index.ts index 0f36dbe2..c4e84876 100644 --- a/packages/core/src/ui/InlineToolbar/index.ts +++ b/packages/core/src/ui/InlineToolbar/index.ts @@ -6,7 +6,7 @@ import { InlineToolbarRenderedUIEvent } from './InlineToolbarRenderedUIEvent.js' import { CoreEventType, EventBus, SelectionChangedCoreEvent } from '../../components/EventBus/index.js'; import { EditorAPI } from '../../api/index.js'; import { InlineTool, InlineToolFormatData } from '@editorjs/sdk'; -import { InlineToolName } from '@editorjs/model'; +import { InlineFragment, InlineToolName, TextRange } from '@editorjs/model'; import { CoreConfigValidated } from '../../entities/index.js'; /** @@ -56,7 +56,7 @@ export class InlineToolbarUI { * @param event - SelectionChangedCoreEvent event */ #handleSelectionChange(event: SelectionChangedCoreEvent): void { - const { availableInlineTools, index } = event.detail; + const { availableInlineTools, index, fragments } = event.detail; const selection = window.getSelection(); if ( @@ -75,7 +75,7 @@ export class InlineToolbarUI { return; } - this.#updateToolsList(availableInlineTools); + this.#updateToolsList(availableInlineTools, index.textRange, fragments); this.#move(); this.#show(); } @@ -137,8 +137,10 @@ export class InlineToolbarUI { /** * Renders the list of available inline tools in the Inline Toolbar * @param tools - Inline Tools available for the current selection + * @param textRange - current selection text range + * @param fragments - inline fragments for the current selection */ - #updateToolsList(tools: Map): void { + #updateToolsList(tools: Map, textRange: TextRange, fragments: InlineFragment[]): void { this.#nodes.buttons.innerHTML = ''; Array.from(tools.entries()).forEach(([name, tool]) => { @@ -146,6 +148,12 @@ export class InlineToolbarUI { button.textContent = name; + const isActive = tool.isActive(textRange, fragments.filter((fragment: InlineFragment) => fragment.tool === name)); + + if (isActive) { + button.style.fontWeight = 'bold'; + } + if (Object.hasOwnProperty.call(tool.constructor.prototype, 'renderActions')) { button.addEventListener('click', () => { this.#renderToolActions(name, tool); diff --git a/packages/model/src/entities/inline-fragments/mixins/ParentNode/index.ts b/packages/model/src/entities/inline-fragments/mixins/ParentNode/index.ts index 60b6777f..f07f00f5 100644 --- a/packages/model/src/entities/inline-fragments/mixins/ParentNode/index.ts +++ b/packages/model/src/entities/inline-fragments/mixins/ParentNode/index.ts @@ -137,6 +137,12 @@ export function ParentNode(constr * @param children - children nodes to insert */ public insertAfter(target: ChildNode, ...children: ChildNode[]): void { + + /** + * We need to get the index first before any manipulations with children array + */ + const index = this.children.indexOf(target); + /** * Append children to the parent to set their parent property */ @@ -154,8 +160,6 @@ export function ParentNode(constr /** * Insert added children to correct places */ - const index = this.children.indexOf(target); - this.children.splice(index + 1, 0, ...children); } diff --git a/packages/model/src/entities/inline-fragments/specs/ParentNode.spec.ts b/packages/model/src/entities/inline-fragments/specs/ParentNode.spec.ts index 23177423..a296f040 100644 --- a/packages/model/src/entities/inline-fragments/specs/ParentNode.spec.ts +++ b/packages/model/src/entities/inline-fragments/specs/ParentNode.spec.ts @@ -182,6 +182,19 @@ describe('ParentNode mixin', () => { expect(dummy.children).toEqual([childMock, anotherChildMock, childMockToInsert]); }); + + it('should correctly insert children when it contains the target child', () => { + const childMock = new ChildDummy(); + const anotherChildMock = new ChildDummy(); + const childMockToInsert = new ChildDummy(); + const anotherChildMockToInsert = new ChildDummy(); + + dummy.append(childMock, childMockToInsert, anotherChildMock); + + dummy.insertAfter(childMockToInsert, anotherChildMockToInsert, childMockToInsert); + + expect(dummy.children).toEqual([childMock, anotherChildMock, anotherChildMockToInsert, childMockToInsert]); + }); }); describe('.removeChild()', () => { diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index 1f12564d..750972f1 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -31,6 +31,7 @@ onMounted(() => { } ], }, onModelUpdate: (model: EditorJSModel) => { + window.model = model; serialized.value = model.serialized; editorDocument.value = model.devModeGetDocument(); }, @@ -132,6 +133,8 @@ onMounted(() => { background-color: #111; border-radius: 8px; padding: 10px; + + font-size: 2em; } .sectionHeading {