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

feat(dom-adapters): Inline tool adapter check for tool required data #87

Merged
merged 77 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
8994e03
Implmenet global CaretAdapter
gohabereg Jan 24, 2024
6fa8279
Handle native inputs
gohabereg Jan 28, 2024
4f0de03
Merge branch 'main' of github.com:editor-js/document-model into featu…
gohabereg Feb 13, 2024
3f93468
Pass input type to Input component props
gohabereg Jun 3, 2024
7cde75e
Use class to represent index
gohabereg Aug 26, 2024
0679745
Fix lint in dom-adapters
gohabereg Aug 26, 2024
1f89c57
fix linter
gohabereg Aug 26, 2024
a7ecfe3
Merge branch 'main' of github.com:editor-js/document-model into featu…
gohabereg Aug 26, 2024
a6a8aa6
added inline tool adapter
e11sy Aug 27, 2024
713b8b9
Merge remote-tracking branch 'origin/feature/global-caret-adapter' in…
e11sy Aug 27, 2024
a9db501
implement model updates
e11sy Aug 27, 2024
06bacaa
update from main
e11sy Aug 28, 2024
d858afc
lint fix
e11sy Aug 28, 2024
83701e7
fix index
e11sy Aug 28, 2024
96bf644
adapter renders inline tools
e11sy Aug 28, 2024
d364498
lint fix and clean up
e11sy Aug 28, 2024
a4e26c9
jsdoc
e11sy Aug 28, 2024
bc01d1b
clean up
e11sy Aug 28, 2024
240affe
jsdoc
e11sy Aug 28, 2024
6853d2a
jsdoc
e11sy Aug 28, 2024
a68e8ea
Merge branch 'main' into inline-tool-adapter
e11sy Aug 28, 2024
5d8bbd1
surround content replaced
e11sy Aug 28, 2024
5c49a09
suggestions
e11sy Aug 28, 2024
01df103
lint fix
e11sy Aug 28, 2024
dea8caf
jsdoc
e11sy Aug 28, 2024
cc00ff3
Merge branch 'main' into inline-tool-adapter
e11sy Aug 28, 2024
9057d1a
added bold and italic inline tools into core package
e11sy Aug 28, 2024
a84d732
naming
e11sy Aug 28, 2024
da5ca7c
naming
e11sy Aug 28, 2024
cf5b177
added inline toolbar and inlineToolAdapter init into core
e11sy Aug 28, 2024
6998adc
update packages and lock
e11sy Aug 28, 2024
e6c4f7c
Merge branch 'main' into inline-tool-adapter
e11sy Aug 28, 2024
185d3f4
build fix
e11sy Aug 29, 2024
6e5034c
implement inline tool adapter to core
e11sy Aug 29, 2024
a629a5b
clean up
e11sy Aug 29, 2024
c0c9b84
Merge branch 'main' into inline-tool-adapter
e11sy Aug 29, 2024
e7c0e80
jsdoc and naming improvements
e11sy Aug 29, 2024
59d8840
naming
e11sy Aug 29, 2024
a2d5e30
naming
e11sy Aug 29, 2024
1adf8c5
renaming
e11sy Aug 29, 2024
e39d5bc
fix hardcoded
e11sy Aug 29, 2024
7f66686
tools are initialized inside of the inline toolbar initialization
e11sy Aug 29, 2024
d52475c
fixed inline tool attaching
e11sy Aug 29, 2024
ff16243
jsdoc
e11sy Aug 29, 2024
e9626bb
naming fix
e11sy Aug 29, 2024
eb4411f
fixed imports
e11sy Aug 29, 2024
4ccc47b
lint fix
e11sy Aug 29, 2024
530b09d
try build fix
e11sy Aug 29, 2024
3c9238b
install dependencies
e11sy Aug 29, 2024
91a1fef
add sdk package
e11sy Aug 29, 2024
cee8b33
fix build for core
e11sy Aug 29, 2024
f9725a1
change package name in actions
e11sy Aug 29, 2024
98a5644
add references
e11sy Aug 29, 2024
be50940
typo
e11sy Aug 29, 2024
6db4d90
fix build
e11sy Aug 29, 2024
6337cfa
added inline tool data former
e11sy Aug 29, 2024
4760931
fix lint
e11sy Aug 29, 2024
77ac4d7
update from main
e11sy Aug 29, 2024
758a317
rm unwanted changes
e11sy Aug 30, 2024
5345c8b
lint fix
e11sy Aug 30, 2024
8452550
fixed build
e11sy Aug 30, 2024
126bb5d
docs improved
e11sy Aug 30, 2024
4a24e82
Merge branch 'main' into inline-tool-adapter
e11sy Aug 30, 2024
cbef761
fix build
e11sy Aug 30, 2024
904ba7b
naming improved
e11sy Aug 30, 2024
6e3b42b
Update packages/core/src/ui/InlineToolbar/index.ts
e11sy Aug 30, 2024
70d078c
Update packages/dom-adapters/src/FormattingAdapter/index.ts
e11sy Aug 30, 2024
58335b4
rm unwanted change
e11sy Aug 30, 2024
f0e1335
Merge branch 'inline-tool-adapter' of https://github.com/editor-js/do…
e11sy Aug 30, 2024
e5fe984
naming
e11sy Aug 30, 2024
f03b984
separated renderToolActions and apply method in formatting adapter
e11sy Aug 30, 2024
0f23dda
naming
e11sy Aug 30, 2024
0e904a8
moved surround to utils
e11sy Aug 30, 2024
4efb7c4
lint fix
e11sy Aug 30, 2024
24307b0
Merge branch 'main' into inline-tool-adapter
e11sy Aug 30, 2024
3a3d488
last naming fix 🤞
e11sy Aug 30, 2024
e610765
made renderToolActions method private
e11sy Aug 30, 2024
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
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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,
gohabereg marked this conversation as resolved.
Show resolved Hide resolved
isInternal: true,
},
};
}
}
96 changes: 96 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,96 @@
import type { DataFormElementWithOptions, FormattingActionWithRange, 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 index - index of current text selection
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
* @param fragments - all fragments of the bold inline tool inside of the current input
*/
public getAction(index: TextRange, fragments: InlineFragment[]): FormattingActionWithRange {
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
return {
action: this.isActive(index, fragments) ? FormattingAction.Unformat : FormattingAction.Format,
range: index,
};
};

/**
* Returns state of the bold inline tool
* @param index - index 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 {
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]) {
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): DataFormElementWithOptions | null {
const dataFormerElement = make('input') as HTMLInputElement;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const dataFormerElement = make('input') as HTMLInputElement;
const linkInput = make('input') as HTMLInputElement;


dataFormerElement.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
callback({ link: dataFormerElement.value });
}
});

return { element: dataFormerElement };
}
}
47 changes: 40 additions & 7 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,10 @@ export class InlineToolbar {
*/
#toolbar: HTMLElement | undefined = undefined;

#dataFormElement: HTMLElement | undefined = undefined;
/**
* Holder element of the editor
*/
#holder: HTMLElement;

/**
Expand Down Expand Up @@ -129,7 +134,7 @@ export class InlineToolbar {
inlineElementButton.innerHTML = toolName;

inlineElementButton.addEventListener('click', (_event) => {
this.apply(toolName as InlineToolName);
this.renderToolActions(createInlineToolName(toolName));
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
});
if (this.#toolbar !== undefined) {
this.#toolbar.appendChild(inlineElementButton);
Expand All @@ -147,13 +152,41 @@ 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 {
public renderToolActions(nameOfTheTool: InlineToolName): void {
Copy link
Member

Choose a reason for hiding this comment

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

Should it be public?

const elementWithOptions = this.#formattingAdapter.formatData(nameOfTheTool, (data: InlineToolFormatData): void => {
this.apply(nameOfTheTool, data);
});

/**
* @todo pass to applyFormat inline tool data formed in toolbar
* Do not render toolbar data former if element with options is null
e11sy marked this conversation as resolved.
Show resolved Hide resolved
* It means, that tool does not need any data, and callback will be triggered in adapter
*/
this.#formattingAdapter.applyFormat(toolName, createInlineToolData({}));
if (elementWithOptions === null) {
return;
}

this.#dataFormElement = elementWithOptions.element;
neSpecc marked this conversation as resolved.
Show resolved Hide resolved

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

this.#holder.appendChild(this.#dataFormElement);
};

/**
* 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 {
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
this.#dataFormElement?.remove();

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
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@

import type {
EditorJSModel,
InlineFragment,
InlineToolData,
InlineToolName,
ModelEvents
} from '@editorjs/model';
import { createInlineToolData } from '@editorjs/model';
import {
EventType,
TextFormattedEvent
} from '@editorjs/model';
import type { CaretAdapter } from '../CaretAdapter/index.js';
import { FormattingAction } from '@editorjs/model';
import type { InlineTool } from '@editorjs/sdk';
import type { InlineTool, InlineToolFormatData, DataFormElementWithOptions } from '@editorjs/sdk';

/**
* Class handles on format model events and renders inline tools
Expand Down Expand Up @@ -48,6 +51,7 @@ export class FormattingAdapter {
}

/**
* @todo move event handling to BlockToolAdapter
* Handles text format and unformat model events
*
* @param event - model change event
Expand All @@ -68,17 +72,62 @@ export class FormattingAdapter {
if (selection) {
const range = selection.getRangeAt(0);

const inlineElement = tool.createWrapper();
const inlineElement = tool.createWrapper(event.detail.data.data);

const extracted = range.extractContents();

/**
* Insert contents from range to new inline element and put created element in range
*/
inlineElement.appendChild(range.extractContents());
inlineElement.appendChild(extracted);

range.insertNode(inlineElement);
}
}
}

/**
* Format content of the contenteditable element
e11sy marked this conversation as resolved.
Show resolved Hide resolved
*
* @param input - input element to apply format to
* @param inlineFragment - instance that contains index, toolName and toolData
* @param inlineFragment.index - text range inside of the input element
* @param inlineFragment.toolName - name of the tool, which format to apply
* @param inlineFragment.toolData - additional data for the tool
*/
public formatElementContent(input: HTMLElement, inlineFragment: InlineFragment): void {
const toolName = inlineFragment.tool;
const toolData = inlineFragment.data;
const index = inlineFragment.range;

const tool = this.#tools.get(toolName);

if (tool === undefined) {
throw new Error(`FormattingAdapter: tool ${toolName} is not attached`);
}

const [start, end] = index;

/**
* Create range with positions specified in index
*/
const range = document.createRange();

range.setStart(input, start);
range.setEnd(input, end);

const inlineElement = tool.createWrapper(toolData);

const extracted = range.extractContents();

/**
* Insert contents from range to new inline element and put created element in range
*/
inlineElement.appendChild(extracted);

range.insertNode(inlineElement);
Copy link
Member

Choose a reason for hiding this comment

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

This could be extracted to a separate method to re-use in #handleModelUpdate method as well

}

/**
* Attaches InlineTool to the adapter
*
Expand All @@ -98,6 +147,37 @@ export class FormattingAdapter {
this.#tools.delete(toolName);
}

/**
* Function that call tool's render actions method if it is specified, otherwise triggers callback
* If any data for tool is required - return rendered by tool data form element with options required in toolbar
* If data for tool is not required (tool don't need any data to apply format) - trigger callback with empty data
*
* @param toolName - name of the tool to check if data is required
* @param callback - callback function that should be triggered, when data completely formed
* @returns {DataFormElementWithOptions | null} rendered data form element with options required in toolbar or null if no data required
*/
public formatData(toolName: InlineToolName, callback: (data: InlineToolFormatData) => void): DataFormElementWithOptions | null {
const currentTool = this.#tools.get(toolName);

if (currentTool === undefined) {
throw new Error(`FormattingAdapter: tool ${toolName} was not attached`);
}

/**
* If renderActions method specified, render element and return it
*/
if (currentTool.renderActions !== undefined) {
return currentTool.renderActions(callback);
}
/**
* If renderActions method is not specidied, then no data required for the tool
* Trigger callback
*/
callback(createInlineToolData({}));

return null;
}

/**
* Format model according to action formed by inline tool instance
*
Expand Down
2 changes: 1 addition & 1 deletion packages/dom-adapters/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './CaretAdapter/index.js';
export * from './BlockToolAdapter/index.js';
export * from './InlineToolsAdapter/index.js';
export * from './FormattingAdapter/index.js';
4 changes: 4 additions & 0 deletions packages/playground/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ onMounted(() => {
<pre>{{ serialized }}</pre>
</div>
<div :class="$style.output">
<div
id="editorjs"
:class="$style.editor"
/>
<Node
:node="editorDocument"
/>
Expand Down
Loading
Loading