Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate InlineToolbar UI and business logic #91

Merged
merged 7 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Comment on lines +10 to +12
Copy link
Contributor

Choose a reason for hiding this comment

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

i think, that comment, about who would call selection api will be helpfull

Copy link
Member Author

Choose a reason for hiding this comment

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

Anybody who needs it

Copy link
Member Author

Choose a reason for hiding this comment

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

Anybody who needs it, that's public API

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();
Copy link
Contributor

Choose a reason for hiding this comment

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

comment required


/**
* @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));
};
}
16 changes: 4 additions & 12 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,14 +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
*/
#inlineToolbar: InlineToolbar;

/**
* @param config - Editor configuration
*/
Expand All @@ -91,9 +84,7 @@ 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(SelectionManager);

this.#prepareUI();

Expand All @@ -114,6 +105,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();

Comment on lines -90 to -94
Copy link
Contributor

Choose a reason for hiding this comment

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

?why this change is needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just to keep link input there, doesn't matter atm as UX will be updated anyway

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
Loading