Skip to content
Open
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### 2.31.7

- `Fix` - Multiple EditorJS instances on the same page now properly register inline tool shortcuts

### 2.31.6

- `Fix` - Widen `sanitize` type on `BlockTool` and `BaseToolConstructable` to accept per-field `SanitizerConfig`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
"version": "2.31.6",
"version": "2.31.7",
"description": "Editor.js — open source block-style WYSIWYG editor with JSON output",
"main": "dist/editorjs.umd.js",
"module": "dist/editorjs.mjs",
Expand Down
2 changes: 1 addition & 1 deletion src/components/modules/toolbar/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
const shortcut = this.getToolShortcut(tool.name);

if (shortcut !== undefined) {
Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut);
Shortcuts.remove(document, shortcut);
}

/**
Expand Down
16 changes: 4 additions & 12 deletions src/components/utils/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,16 @@ class Shortcuts {
/**
* All registered shortcuts
*
* @type {Map<Element, Shortcut[]>}
* @type {Map<HTMLElement | Document, Shortcut[]>}
*/
private registeredShortcuts: Map<Element, Shortcut[]> = new Map();
private registeredShortcuts: Map<HTMLElement | Document, Shortcut[]> = new Map();

/**
* Register shortcut
*
* @param shortcut - shortcut options
*/
public add(shortcut: ShortcutData): void {
const foundShortcut = this.findShortcut(shortcut.on, shortcut.name);

if (foundShortcut) {
throw Error(
`Shortcut ${shortcut.name} is already registered for ${shortcut.on}. Please remove it before add a new handler.`
);
}

const newShortcut = new Shortcut({
name: shortcut.name,
on: shortcut.on,
Expand All @@ -75,7 +67,7 @@ class Shortcuts {
* @param element - Element shortcut is set for
* @param name - shortcut name
*/
public remove(element: Element, name: string): void {
public remove(element: HTMLElement | Document, name: string): void {
const shortcut = this.findShortcut(element, name);

if (!shortcut) {
Expand Down Expand Up @@ -104,7 +96,7 @@ class Shortcuts {
* @param shortcut - shortcut name
* @returns {number} index - shortcut index if exist
*/
private findShortcut(element: Element, shortcut: string): Shortcut | void {
private findShortcut(element: HTMLElement | Document, shortcut: string): Shortcut | void {
const shortcuts = this.registeredShortcuts.get(element) || [];

return shortcuts.find(({ name }) => name === shortcut);
Expand Down
90 changes: 90 additions & 0 deletions test/cypress/tests/ui/InlineToolbar.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,96 @@ describe('Inline Toolbar', () => {
});

describe('Shortcuts', () => {
it('should activate the focused editor\'s tool when shortcut is pressed with multiple instances on the page', () => {
const toolActivated1 = cy.stub().as('toolActivated1');
const toolActivated2 = cy.stub().as('toolActivated2');

/* eslint-disable jsdoc/require-jsdoc */
class Marker1 implements InlineTool {
public static isInline = true;
public static shortcut = 'CMD+SHIFT+M';
public render(): MenuConfig {
return {
icon: 'm',
title: 'Marker',
onActivate: () => { toolActivated1(); },
};
}
}
class Marker2 implements InlineTool {
public static isInline = true;
public static shortcut = 'CMD+SHIFT+M';
public render(): MenuConfig {
return {
icon: 'm',
title: 'Marker',
onActivate: () => { toolActivated2(); },
};
}
}
/* eslint-enable jsdoc/require-jsdoc */

/** Create first editor */
cy.createEditor({
data: {
blocks: [ { type: 'paragraph', data: { text: 'First editor text' } } ],
},
tools: { marker: Marker1 },
});

/** Create second editor with a different holder */
cy.window().then((win) => {
const holder = win.document.createElement('div');

holder.id = 'editorjs2';
holder.dataset.cy = 'editorjs2';
win.document.body.appendChild(holder);

return new Promise<void>((resolve) => {
const editor2 = new win.EditorJS({
holder: 'editorjs2',
data: {
blocks: [ { type: 'paragraph', data: { text: 'Second editor text' } } ],
},
tools: { marker: Marker2 },
});

editor2.isReady.then(() => resolve());
});
});

/** Select text in first editor to open its inline toolbar and register its shortcuts */
cy.get('[data-cy=editorjs]')
.find('.ce-paragraph')
.selectText('editor');

/** Select text in second editor — closes the first editor's toolbar and opens the second's */
cy.get('[data-cy=editorjs2]')
.find('.ce-paragraph')
.selectText('editor');

/** Wait for the second editor's inline toolbar to be visible before dispatching the shortcut */
cy.get('[data-cy=editorjs2] [data-cy="inline-toolbar"] .ce-popover__container')
.should('be.visible');

cy.document().then((doc) => {
doc.dispatchEvent(new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: 'M',
code: 'KeyM',
keyCode: 77,
which: 77,
metaKey: true,
shiftKey: true,
}));
});

/** Second editor's shortcut should fire, first editor's should not */
cy.get('@toolActivated2').should('have.been.called');
cy.get('@toolActivated1').should('not.have.been.called');
Comment on lines +293 to +294
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot Look like if you will remove your changes from "src/components/utils/shortcuts.ts", this test will still be passed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in c5bff13. The test now uses cy.clock() to freeze the selectionchange debounce after editor 1's shortcut is registered, then calls editor2.inlineToolbar.open() via the public API before that debounce fires. At that moment editor 1's CMD+SHIFT+M handler is still live on document, so enableShortcuts() hits the duplicate-registration path. Without the shortcuts.ts fix (the removed throw) it silently fails to register editor 2's shortcut → toolActivated2 never fires → test fails. Ticking past the debounce afterwards also exercises the inline.ts fix, confirming editor 1's shortcut is removed correctly.

});

it('should work in read-only mode', () => {
const toolSurround = cy.stub().as('toolSurround');

Expand Down
Loading