Skip to content

Commit

Permalink
feat(dom-adapters): Inline tool adapter check for tool required data (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>

* Update packages/dom-adapters/src/FormattingAdapter/index.ts

Co-authored-by: Peter <[email protected]>

* 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 <[email protected]>
Co-authored-by: George Berezhnoy <[email protected]>
Co-authored-by: Peter <[email protected]>
  • Loading branch information
4 people authored Aug 30, 2024
1 parent c04c0ea commit 83662ff
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 46 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export default class Core {
this.#iocContainer = Container.of(Math.floor(Math.random() * 1e10).toString());

this.validateConfig(config);

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unnecessary-type-assertion
this.#config = config as CoreConfigValidated;

this.#iocContainer.set('EditorConfig', this.#config);
Expand Down
16 changes: 12 additions & 4 deletions packages/core/src/tools/ToolsManager.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { BlockToolConstructor } from '@editorjs/sdk';
import 'reflect-metadata';
import { deepMerge, isFunction, isObject, PromiseQueue } from '@editorjs/helpers';
import { Inject, Service } from 'typedi';
import type { BlockToolFacade, BlockTuneFacade,
InlineToolFacade } from './facades/index.js';
import {
BlockToolFacade, BlockTuneFacade,
InlineToolFacade,
ToolsCollection,
ToolsFactory
} from './facades/index.js';
Expand All @@ -13,10 +14,11 @@ import type {
ToolConstructable,
ToolSettings
} from '@editorjs/editorjs';
import { InlineTool, InlineToolConstructor } from '@editorjs/sdk';
import type { UnifiedToolConfig } from '../entities/index.js';
import BoldInlineTool from './internal/inline-tools/bold/index.js';
import ItalicInlineTool from './internal/inline-tools/italic/index.js';
import { BlockToolConstructor, InlineTool, InlineToolConstructor } from '@editorjs/sdk';
import { UnifiedToolConfig } from '../entities/index.js';
import LinkInlineTool from './internal/inline-tools/link/index.js';

/**
* Works with tools
Expand All @@ -25,6 +27,8 @@ import { UnifiedToolConfig } from '../entities/index.js';
*/
@Service()
export default class ToolsManager {
#tools: EditorConfig['tools'];

/**
* ToolsFactory instance
*/
Expand Down Expand Up @@ -234,6 +238,10 @@ export default class ToolsManager {
class: ItalicInlineTool as unknown as InlineToolConstructor,
isInternal: true,
},
link: {
class: LinkInlineTool as unknown as InlineToolConstructor,
isInternal: true,
},
};
}
}
7 changes: 7 additions & 0 deletions packages/core/src/tools/facades/InlineToolFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ 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
16 changes: 8 additions & 8 deletions packages/core/src/tools/internal/inline-tools/bold/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FormattingActionWithRange, InlineTool } from '@editorjs/sdk';
import type { ToolFormattingOptions, InlineTool } from '@editorjs/sdk';
import type { InlineFragment, TextRange } from '@editorjs/model';
import { FormattingAction } from '@editorjs/model';
import { IntersectType } from '@editorjs/model';
Expand Down Expand Up @@ -34,30 +34,30 @@ export default class BoldInlineTool implements InlineTool {

/**
* Returns formatting action and range for it to be applied
* @param index - index of current text selection
* @param range - range of current text selection
* @param fragments - all fragments of the bold inline tool inside of the current input
*/
public getAction(index: TextRange, fragments: InlineFragment[]): FormattingActionWithRange {
public getFormattingOptions(range: TextRange, fragments: InlineFragment[]): ToolFormattingOptions {
return {
action: this.isActive(index, fragments) ? FormattingAction.Unformat : FormattingAction.Format,
range: index,
action: this.isActive(range, fragments) ? FormattingAction.Unformat : FormattingAction.Format,
range,
};
};

/**
* Returns state of the bold inline tool
* @param index - index of current selection
* @param range - range of current selection
* @param fragments - all fragments of the bold inline tool inside of the current input
* @returns true if tool is active, false otherwise
*/
public isActive(index: TextRange, fragments: InlineFragment[]): boolean {
public isActive(range: TextRange, fragments: InlineFragment[]): boolean {
let isActive = false;

fragments.forEach((fragment) => {
/**
* Check if current index is inside of model fragment
*/
if (index[0] >= fragment.range[0] && index[1] <= fragment.range[1]) {
if (range[0] >= fragment.range[0] && range[1] <= fragment.range[1]) {
isActive = true;

/**
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/tools/internal/inline-tools/italic/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FormattingActionWithRange, InlineTool } from '@editorjs/sdk';
import type { ToolFormattingOptions, InlineTool } from '@editorjs/sdk';
import type { InlineFragment, TextRange } from '@editorjs/model';
import { FormattingAction } from '@editorjs/model';
import { IntersectType } from '@editorjs/model';
Expand Down Expand Up @@ -34,30 +34,30 @@ export default class ItalicInlineTool implements InlineTool {

/**
* Returns formatting action and range for it to be applied
* @param index - index of current text selection
* @param range - range of current text selection
* @param fragments - all fragments of the bold inline tool inside of the current input
*/
public getAction(index: TextRange, fragments: InlineFragment[]): FormattingActionWithRange {
public getFormattingOptions(range: TextRange, fragments: InlineFragment[]): ToolFormattingOptions {
return {
action: this.isActive(index, fragments) ? FormattingAction.Unformat : FormattingAction.Format,
range: index,
action: this.isActive(range, fragments) ? FormattingAction.Unformat : FormattingAction.Format,
range,
};
};

/**
* Returns state of the bold inline tool
* @param index - index of current selection
* @param range - range of current selection
* @param fragments - all fragments of the bold inline tool inside of the current input
* @returns true if tool is active, false otherwise
*/
public isActive(index: TextRange, fragments: InlineFragment[]): boolean {
public isActive(range: TextRange, fragments: InlineFragment[]): boolean {
let isActive = false;

fragments.forEach((fragment) => {
/**
* Check if current index is inside of model fragment
*/
if (index[0] >= fragment.range[0] && index[1] <= fragment.range[1]) {
if (range[0] >= fragment.range[0] && range[1] <= fragment.range[1]) {
isActive = true;

/**
Expand Down
101 changes: 101 additions & 0 deletions packages/core/src/tools/internal/inline-tools/link/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { ActionsElementWithOptions, ToolFormattingOptions, InlineTool, InlineToolFormatData } from '@editorjs/sdk';
import type { InlineFragment, TextRange } from '@editorjs/model';
import { FormattingAction } from '@editorjs/model';
import { IntersectType } from '@editorjs/model';
import { make } from '@editorjs/dom';

/**
* Link Tool
*
* Inline Toolbar Tool
*
* Makes selected text linked
*/
export default class LinkInlineTool implements InlineTool {
/**
* Specifies Tool as Inline Toolbar Tool
* @returns {boolean}
*/
public static isInline = true;

/**
* Type of behaviour of the tool if new selection range intersect with existing fragment
* If two fragment intersect, existing fragment should be replaced with new one
*/
public intersectType: IntersectType = IntersectType.Replace;

/**
* Renders wrapper for tool without actual content
* @param data - inline tool data formed in toolbar
* @returns Created html element
*/
public createWrapper(data: InlineToolFormatData): HTMLElement {
const linkElement = make('a') as HTMLLinkElement;

if (typeof data.link === 'string') {
linkElement.href = data.link;
}

return linkElement;
}

/**
* Returns formatting action and range for it to be applied
* @param range - range of current text selection
* @param fragments - all fragments of the bold inline tool inside of the current input
*/
public getFormattingOptions(range: TextRange, fragments: InlineFragment[]): ToolFormattingOptions {
return {
action: this.isActive(range, fragments) ? FormattingAction.Unformat : FormattingAction.Format,
range,
};
};

/**
* Returns state of the bold inline tool
* @param range - range of current selection
* @param fragments - all fragments of the bold inline tool inside of the current input
* @returns true if tool is active, false otherwise
*/
public isActive(range: TextRange, fragments: InlineFragment[]): boolean {
let isActive = false;

fragments.forEach((fragment) => {
/**
* Check if current index is inside of model fragment
*/
if (range[0] === fragment.range[0] && range[1] === fragment.range[1]) {
isActive = true;

/**
* No need to check other fragments if state already chaned
*/
return;
}
});

return isActive;
}

/**
* Function that is responsible for rendering data form element
* @param callback function that should be triggered, when data completely formed
* @returns rendered data form element with options required in toolbar
*/
public renderActions(callback: (data: InlineToolFormatData) => void): ActionsElementWithOptions {
const linkInput = make('input') as HTMLInputElement;

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

callback({ link: linkInput.value });
}
});

return { element: linkInput };
}
}
65 changes: 55 additions & 10 deletions packages/core/src/ui/InlineToolbar/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { FormattingAdapter } from '@editorjs/dom-adapters';
import type { InlineToolFormatData } from '@editorjs/sdk';
import type { InlineToolName } from '@editorjs/model';
import { type EditorJSModel, type TextRange, createInlineToolData, Index } from '@editorjs/model';
import { type EditorJSModel, type TextRange, createInlineToolData, createInlineToolName, Index } from '@editorjs/model';
import { EventType } from '@editorjs/model';
import { make } from '@editorjs/dom';
import type { InlineToolFacade, ToolsCollection } from '../../tools/facades/index.js';
Expand Down Expand Up @@ -38,6 +39,14 @@ export class InlineToolbar {
*/
#toolbar: HTMLElement | undefined = undefined;

/**
* Actions of the current tool html element rendered inside of the toolbar element
*/
#actionsElement: HTMLElement | undefined = undefined;

/**
* Holder element of the editor
*/
#holder: HTMLElement;

/**
Expand Down Expand Up @@ -123,14 +132,25 @@ export class InlineToolbar {

this.#toolbar = make('div');

Array.from(this.#tools.keys()).forEach((toolName) => {
this.#tools.forEach((tool, toolName) => {
const inlineElementButton = make('button');

inlineElementButton.innerHTML = toolName;

inlineElementButton.addEventListener('click', (_event) => {
this.apply(toolName as InlineToolName);
});
/**
* If tool has actions, then on click of the element button we should render actions element
* If tool has no action, then on click of the element button we should apply format
*/
if (tool.hasActions) {
inlineElementButton.addEventListener('click', (_event) => {
this.#renderToolActions(createInlineToolName(toolName));
});
} else {
inlineElementButton.addEventListener('click', (_event) => {
this.apply(createInlineToolName(toolName), createInlineToolData({}));
});
}

if (this.#toolbar !== undefined) {
this.#toolbar.appendChild(inlineElementButton);
}
Expand All @@ -147,13 +167,38 @@ export class InlineToolbar {
}

/**
* Apply format with data formed in toolbar
* @param toolName - name of the inline tool, whose format would be applied
* Render actions to form data, which is required in tool
* Element that is used for forming data is rendered inside of the tool instance
* This function adds actions element to the toolbar
* @param nameOfTheTool - name of the inline tool, whose format would be applied
*/
public apply(toolName: InlineToolName): void {
#renderToolActions(nameOfTheTool: InlineToolName): void {
const elementWithOptions = this.#formattingAdapter.createToolActions(nameOfTheTool, (data: InlineToolFormatData): void => {
this.apply(nameOfTheTool, data);
});

if (this.#toolbar === undefined) {
throw new Error('InlineToolbar: can not show tool actions without toolbar');
}

/**
* @todo pass to applyFormat inline tool data formed in toolbar
* If actions element already exists, replace it with new one
* This check is needed to prevent displaying of several actions elements
*/
this.#formattingAdapter.applyFormat(toolName, createInlineToolData({}));
if (this.#actionsElement !== undefined) {
this.#actionsElement.remove();
}

this.#actionsElement = elementWithOptions.element;
this.#holder.appendChild(this.#actionsElement);
};

/**
* Apply format of the inline tool to the model
* @param toolName - name of the tool which format would be applied
* @param formatData - formed data required in the inline tool
*/
public apply(toolName: InlineToolName, formatData: InlineToolFormatData): void {
this.#formattingAdapter.applyFormat(toolName, createInlineToolData(formatData));
}
}
2 changes: 1 addition & 1 deletion packages/dom-adapters/src/BlockToolAdapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from '../utils/index.js';
import { InputType } from './types/InputType.js';
import type { BlockToolAdapter as BlockToolAdapterInterface } from '@editorjs/sdk';
import type { FormattingAdapter } from '../InlineToolsAdapter/index.js';
import type { FormattingAdapter } from '../FormattingAdapter/index.js';

/**
* BlockToolAdapter is using inside Block tools to connect browser DOM elements to the model
Expand Down
Loading

2 comments on commit 83662ff

@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 100% 769/769
🟢 Branches 99.51% 203/204
🟢 Functions 98.39% 183/186
🟢 Lines 100% 741/741

Test suite run success

389 tests passing in 24 suites.

Report generated by 🧪jest coverage report action from 83662ff

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

Please sign in to comment.