Skip to content

Commit

Permalink
Separate InlineToolbar UI and business logic (#91)
Browse files Browse the repository at this point in the history
* Separate InlineToolbar UI and business logic

* Remova hasActions from InlineToolFacade

* Fix lint

* Add docs after review

* Merge main
  • Loading branch information
gohabereg authored Aug 30, 2024
1 parent 906e7a6 commit e3d47f4
Show file tree
Hide file tree
Showing 15 changed files with 374 additions and 206 deletions.
34 changes: 34 additions & 0 deletions packages/core/src/api/SelectionAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'reflect-metadata';
import { Service } from 'typedi';

import { SelectionManager } from '../components/SelectionManager.js';
import { createInlineToolName } from '@editorjs/model';
import { InlineToolFormatData } from '@editorjs/sdk';

/**
* Selection API class
* - provides methods to work with selection
*/
@Service()
export class SelectionAPI {
#selectionManager: SelectionManager;

/**
* SelectionAPI class constructor
* @param selectionManager - SelectionManager instance to work with selection and inline fotmatting
*/
constructor(
selectionManager: SelectionManager
) {
this.#selectionManager = selectionManager;
};

/**
* 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)
*/
public applyInlineToolForCurrentSelection(toolName: string, data?: InlineToolFormatData): void {
this.#selectionManager.applyInlineToolForCurrentSelection(createInlineToolName(toolName), data);
}
}
22 changes: 22 additions & 0 deletions packages/core/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'reflect-metadata';
import { Inject, Service } from 'typedi';
import { BlocksAPI } from './BlocksAPI.js';
import { SelectionAPI } from './SelectionAPI.js';

/**
* Class gathers all Editor's APIs
*/
@Service()
export class EditorAPI {
/**
* Blocks API instance to work with blocks
*/
@Inject()
public blocks!: BlocksAPI;

/**
* Selection API instance to work with selection and inline formatting
*/
@Inject()
public selection!: SelectionAPI;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,15 @@ export enum CoreEventType {
/**
* Event is fired when a tool is loaded
*/
ToolLoaded = 'tool:loaded'
ToolLoaded = 'tool:loaded',

/**
* Event is fired when InlineTool instance is created
*/
InlineToolCreated = 'tool:inline-tool-created',

/**
* Event is fired when the selection is changed
*/
SelectionChanged = 'selection:changed'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { InlineTool } from '@editorjs/sdk';
import { CoreEventBase } from './CoreEventBase.js';
import { CoreEventType } from './CoreEventType.js';
import type { Index, InlineToolName } from '@editorjs/model';

/**
* Payload of SelectionChangedCoreEvent custom event
* Contains updated caret index and available inline tools
*/
export interface SelectionChangedCoreEventPayload {
/**
* Updated caret index
*/
readonly index: Index | null;

/**
* Inline tools available for the current selection
*/
readonly availableInlineTools: Map<InlineToolName, InlineTool>;
}

/**
* Class for event that is being fired after the selection is changed
*/
export class SelectionChangedCoreEvent extends CoreEventBase<SelectionChangedCoreEventPayload> {
/**
* SelectionChangedCoreEvent constructor function
* @param payload - SelectionChangedCoreEvent event payload with updated caret index
*/
constructor(payload: SelectionChangedCoreEventPayload) {
super(CoreEventType.SelectionChanged, payload);
}
}
1 change: 1 addition & 0 deletions packages/core/src/components/EventBus/core-events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './BlockAddedCoreEvent.js';
export * from './BlockRemovedCoreEvent.js';
export * from './ToolLoadedCoreEvent.js';
export * from './CoreEventType.js';
export * from './SelectionChangedCoreEvent.js';
101 changes: 101 additions & 0 deletions packages/core/src/components/SelectionManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import 'reflect-metadata';
import { FormattingAdapter } from '@editorjs/dom-adapters';
import type { CaretManagerEvents, InlineToolName } from '@editorjs/model';
import { CaretManagerCaretUpdatedEvent, Index, EditorJSModel, createInlineToolData, createInlineToolName } from '@editorjs/model';
import { EventType } from '@editorjs/model';
import { Service } from 'typedi';
import { CoreEventType, EventBus, ToolLoadedCoreEvent } from './EventBus/index.js';
import { SelectionChangedCoreEvent } from './EventBus/core-events/SelectionChangedCoreEvent.js';
import { InlineTool, InlineToolFormatData } from '@editorjs/sdk';

/**
* SelectionManager responsible for handling selection changes and applying inline tools formatting
*/
@Service()
export class SelectionManager {
/**
* Editor model instance
* Used for interactions with stored data
*/
#model: EditorJSModel;

/**
* FormattingAdapter instance
* Used for inline tools attaching and format apply
*/
#formattingAdapter: FormattingAdapter;

/**
* EventBus instance to exchange events between components
*/
#eventBus: EventBus;

/**
* Inline Tools instances available for use
*/
#inlineTools: Map<InlineToolName, InlineTool> = new Map();

/**
* @param model - editor model instance
* @param formattingAdapter - needed for applying format to the model
* @param eventBus - EventBus instance to exchange events between components
*/
constructor(
model: EditorJSModel,
formattingAdapter: FormattingAdapter,
eventBus: EventBus
) {
this.#model = model;
this.#formattingAdapter = formattingAdapter;
this.#eventBus = eventBus;

this.#eventBus.addEventListener(`core:${CoreEventType.ToolLoaded}`, (event: ToolLoadedCoreEvent) => {
const { tool } = event.detail;

if (!tool.isInline()) {
return;
}

const toolInstance = tool.create();
const name = createInlineToolName(tool.name);

this.#inlineTools.set(name, toolInstance);

this.#formattingAdapter.attachTool(name, toolInstance);
});

this.#model.addEventListener(EventType.CaretManagerUpdated, (event: CaretManagerEvents) => this.#handleCaretManagerUpdate(event));
}

/**
* Handle changes of the caret selection
* @param event - CaretManager event
*/
#handleCaretManagerUpdate(event: CaretManagerEvents): void {
switch (true) {
case event instanceof CaretManagerCaretUpdatedEvent:
this.#eventBus.dispatchEvent(new SelectionChangedCoreEvent({
index: event.detail.index !== null ? Index.parse(event.detail.index) : null,
/**
* @todo implement filter by current BlockTool configuration
*/
availableInlineTools: this.#inlineTools,
}));
break;
default:
break;
}
}

/**
* Apply format with data formed in toolbar
* @param toolName - name of the inline tool, whose format would be applied
* @param data - fragment data for the current selection
*/
public applyInlineToolForCurrentSelection(toolName: InlineToolName, data: InlineToolFormatData = {}): void {
/**
* @todo pass to applyFormat inline tool data formed in toolbar
*/
this.#formattingAdapter.applyFormat(toolName, createInlineToolData(data));
};
}
19 changes: 4 additions & 15 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { Container } from 'typedi';
import { composeDataFromVersion2 } from './utils/composeDataFromVersion2.js';
import ToolsManager from './tools/ToolsManager.js';
import { CaretAdapter, FormattingAdapter } from '@editorjs/dom-adapters';
import { InlineToolbar } from './ui/InlineToolbar/index.js';
import type { CoreConfigValidated } from './entities/Config.js';
import type { CoreConfig } from '@editorjs/sdk';
import { BlocksManager } from './components/BlockManager.js';
import { EditorUI } from './ui/Editor/index.js';
import { ToolboxUI } from './ui/Toolbox/index.js';
import { InlineToolbarUI } from './ui/InlineToolbar/index.js';
import { SelectionManager } from './components/SelectionManager.js';

/**
* If no holder is provided via config, the editor will be appended to the element with this id
Expand Down Expand Up @@ -57,16 +58,6 @@ export default class Core {
*/
#formattingAdapter: FormattingAdapter;

/**
* @todo inline toolbar should subscripe on selection change event called by EventBus
* 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 | null = null;

/**
* @param config - Editor configuration
*/
Expand All @@ -93,7 +84,7 @@ export default class Core {

this.#formattingAdapter = new FormattingAdapter(this.#model, this.#caretAdapter);
this.#iocContainer.set(FormattingAdapter, this.#formattingAdapter);

this.#iocContainer.get(SelectionManager);
this.#iocContainer.get(BlocksManager);

if (config.onModelUpdate !== undefined) {
Expand All @@ -106,9 +97,6 @@ export default class Core {

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 });
})
.catch((error) => {
Expand All @@ -123,6 +111,7 @@ export default class Core {
const editorUI = this.#iocContainer.get(EditorUI);

this.#iocContainer.get(ToolboxUI);
this.#iocContainer.get(InlineToolbarUI);

editorUI.render();
}
Expand Down
7 changes: 0 additions & 7 deletions packages/core/src/tools/facades/InlineToolFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@ export class InlineToolFacade extends BaseToolFacade<ToolType.Inline, IInlineToo
return this.constructable[InternalInlineToolSettings.Title];
}

/**
* Checks if actions element could be rendered by tool
*/
public get hasActions(): boolean {
return 'renderActions' in this.constructable.prototype;
}

/**
* Constructs new InlineTool instance from constructable
*/
Expand Down
5 changes: 0 additions & 5 deletions packages/core/src/tools/internal/inline-tools/link/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,6 @@ export default class LinkInlineTool implements InlineTool {

linkInput.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
/**
* Remove link input, when data formed and trigger callback
*/
linkInput.remove();

callback({ link: linkInput.value });
}
});
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/ui/Editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CoreConfigValidated } from '../../entities/index.js';
import { EventBus } from '../../components/EventBus/index.js';
import { BlockAddedCoreEvent, CoreEventType } from '../../components/EventBus/index.js';
import { ToolboxRenderedUIEvent } from '../Toolbox/index.js';
import { InlineToolbarRenderedUIEvent } from '../InlineToolbar/InlineToolbarRenderedUIEvent.js';

/**
* Editor's main UI renderer for HTML environment
Expand Down Expand Up @@ -48,6 +49,10 @@ export class EditorUI {
this.#eventBus.addEventListener(`ui:toolbox:rendered`, (event: ToolboxRenderedUIEvent) => {
this.#addToolbox(event.detail.toolbox);
});

this.#eventBus.addEventListener(`ui:inline-toolbar:rendered`, (event: InlineToolbarRenderedUIEvent) => {
this.#holder.appendChild(event.detail.toolbar);
});
}

/**
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/ui/InlineToolbar/InlineToolbarRenderedUIEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { UIEventBase } from '../../components/EventBus/index.js';

/**
* Payload of the InlineToolbarRenderedUIEvent
* Contains InlineToolbar HTML element
*/
export interface InlineToolbarRenderedUIEventPayload {
/**
* Toolbox HTML element
*/
readonly toolbar: HTMLElement;
}

/**
* Class for event that is being fired after the inline toolbar is rendered
*/
export class InlineToolbarRenderedUIEvent extends UIEventBase<InlineToolbarRenderedUIEventPayload> {
/**
* ToolboxRenderedUIEvent constructor function
* @param payload - ToolboxRendered event payload
*/
constructor(payload: InlineToolbarRenderedUIEventPayload) {
super('inline-toolbar:rendered', payload);
}
}
Loading

2 comments on commit e3d47f4

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for ./packages/model

St.
Category Percentage Covered / Total
🟢 Statements 96.74% 800/827
🟢 Branches 98.17% 214/218
🟢 Functions 88.07% 192/218
🟢 Lines 96.62% 772/799

Test suite run success

404 tests passing in 24 suites.

Report generated by 🧪jest coverage report action from e3d47f4

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for ./packages/collaboration-manager

St.
Category Percentage Covered / Total
🟢 Statements 86.11% 62/72
🟡 Branches 62.5% 15/24
🟢 Functions 100% 10/10
🟢 Lines 86.11% 62/72

Test suite run success

6 tests passing in 1 suite.

Report generated by 🧪jest coverage report action from e3d47f4

Please sign in to comment.