From 628f2188e75036505f39ba0a9a562a6f647104ea Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sat, 6 Sep 2025 16:14:27 +0300 Subject: [PATCH 01/12] Create .nvmrc --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..ef33d651 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.20.1 From df7d3a7883100a66c8db352aa615003706b5df98 Mon Sep 17 00:00:00 2001 From: narpat-ps Date: Sat, 6 Sep 2025 18:50:08 +0530 Subject: [PATCH 02/12] resolve "Can't find a Block to remove" error in renderFromHTML (#2941) * fix(blocks):Error occurred when calling renderFromHTML: Can't find a Block to remove. * fix: resolve "Can't find a Block to remove" error in renderFromHTML - Make renderFromHTML async and await BlockManager.clear() to prevent race condition - Change removeBlock order: remove from array before destroy to prevent index invalidation - Fix clear() method to copy blocks array before iteration to avoid modification during loop Fixes issue where renderFromHTML would fail with "Can't find a Block to remove" error due to concurrent block removal operations and array modification during iteration. Resolves #2518 --- src/components/modules/api/blocks.ts | 4 ++-- src/components/modules/blockManager.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index f9297d5d..9ad22176 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -224,8 +224,8 @@ export default class BlocksAPI extends Module { * @param {string} data - HTML string to render * @returns {Promise} */ - public renderFromHTML(data: string): Promise { - this.Editor.BlockManager.clear(); + public async renderFromHTML(data: string): Promise { + await this.Editor.BlockManager.clear(); return this.Editor.Paste.processText(data, true); } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 48fec049..be8e1e24 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -533,8 +533,8 @@ export default class BlockManager extends Module { throw new Error('Can\'t find a Block to remove'); } - block.destroy(); this._blocks.remove(index); + block.destroy(); /** * Force call of didMutated event on Block removal @@ -894,7 +894,10 @@ export default class BlockManager extends Module { public async clear(needToAddDefaultBlock = false): Promise { const queue = new PromiseQueue(); - this.blocks.forEach((block) => { + // Create a copy of the blocks array to avoid issues with array modification during iteration + const blocksToRemove = [...this.blocks]; + + blocksToRemove.forEach((block) => { queue.add(async () => { await this.removeBlock(block, false); }); From 7b6b78235a344abc598c4564f7294fd815431831 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sat, 6 Sep 2025 16:20:35 +0300 Subject: [PATCH 03/12] add changelog --- docs/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5770a1e7..7eff455e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,11 @@ - `DX` - Tools submodules removed from the repository - `Improvement` - Shift + Down/Up will allow to select next/previous line instead of Inline Toolbar flipping - `Improvement` - The API `caret.setToBlock()` offset now works across the entire block content, not just the first or last node. +- `Improvement` - The API `blocks.renderFromHTML()` became async and now can be awaited. +- `Fix` - `blocks.renderFromHTML()` — Error "Can't find a Block to remove." fixed +- `Fix` - The API `.clear()` index invalidation fixed + + ### 2.30.7 From 9612e0d2470554ac6e36daaedd45911280900501 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 6 Sep 2025 16:31:44 +0300 Subject: [PATCH 04/12] release 2.31 (#2956) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8f60f1a0..5039584f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0-rc.10", + "version": "2.31.0", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", From a04c37b30b6e2dcdd301752a7236829f53da8620 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 15 Sep 2025 20:14:25 +0300 Subject: [PATCH 05/12] add example of how to use i18n for Convert To buttons --- example/example-i18n.html | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/example/example-i18n.html b/example/example-i18n.html index bc1aaf7a..f765f1e9 100644 --- a/example/example-i18n.html +++ b/example/example-i18n.html @@ -107,7 +107,7 @@ image: ImageTool, list: { - class: List, + class: EditorjsList, inlineToolbar: true, shortcut: 'CMD+SHIFT+L' }, @@ -198,7 +198,11 @@ }, "popover": { "Filter": "Поиск", - "Nothing found": "Ничего не найдено" + "Nothing found": "Ничего не найдено", + /** + * Translation of "Convert To" at the Block Tunes Popover + */ + "Convert to": "Конвертировать в" } }, @@ -221,7 +225,7 @@ "Bold": "Полужирный", "Italic": "Курсив", "InlineCode": "Моноширинный", - "Image": "Картинка" + "Image": "Картинка", }, /** @@ -274,7 +278,13 @@ "list": { "Ordered": "Нумерованный", "Unordered": "Маркированный", - } + }, + /** + * Translation of "Convert To" at the Inline Toolbar hint + */ + "convertTo": { + "Convert to": "Конвертировать в" + }, }, /** @@ -295,7 +305,7 @@ }, "moveDown": { "Move down": "Переместить вниз" - } + }, }, } }, From fb2bf8f116208727e0f932b87d35e690153b4219 Mon Sep 17 00:00:00 2001 From: Kuchizu <70284260+Kuchizu@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:54:34 +0300 Subject: [PATCH 06/12] ci: update deprecated GitHub Actions runners (#2974) * ci: update deprecated GitHub Actions runners ci: update GitHub Actions runners from ubuntu-20.04 to ubuntu-latest * fix: pin Node 18 in CI workflows for eslint compatibility * fix(ci): pin Firefox 115 ESR for Cypress compatibility --- .github/workflows/bump-version-on-merge-next.yml | 2 +- .github/workflows/create-a-release-draft.yml | 2 +- .github/workflows/cypress.yml | 11 +++++++++-- .github/workflows/eslint.yml | 6 +++++- .github/workflows/publish-package-to-npm.yml | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bump-version-on-merge-next.yml b/.github/workflows/bump-version-on-merge-next.yml index 2a592e3c..023bf809 100644 --- a/.github/workflows/bump-version-on-merge-next.yml +++ b/.github/workflows/bump-version-on-merge-next.yml @@ -17,7 +17,7 @@ jobs: # If pull request was merged then we should check for a package version update check-for-no-version-changing: if: github.event.pull_request.merged == true - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest permissions: actions: write steps: diff --git a/.github/workflows/create-a-release-draft.yml b/.github/workflows/create-a-release-draft.yml index ec728b8a..02ffb1b3 100644 --- a/.github/workflows/create-a-release-draft.yml +++ b/.github/workflows/create-a-release-draft.yml @@ -17,7 +17,7 @@ jobs: # If pull request was merged then we should check for a package version update check-version-changing: if: github.event.pull_request.merged == true - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest permissions: actions: write steps: diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 51718006..e9ed6ae2 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -8,12 +8,19 @@ jobs: matrix: browser: [firefox, chrome, edge] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 18 - - uses: actions/checkout@v4 + + - name: Setup Firefox + if: matrix.browser == 'firefox' + uses: browser-actions/setup-firefox@v1 + with: + firefox-version: '115.0esr' + - uses: cypress-io/github-action@v6 with: config: video=false diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 973af889..c85e5ca9 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -5,10 +5,14 @@ on: [pull_request] jobs: lint: name: ESlint - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Cache dependencies uses: actions/cache@v4 with: diff --git a/.github/workflows/publish-package-to-npm.yml b/.github/workflows/publish-package-to-npm.yml index b6ec6935..e867a7ed 100644 --- a/.github/workflows/publish-package-to-npm.yml +++ b/.github/workflows/publish-package-to-npm.yml @@ -7,7 +7,7 @@ on: jobs: publish: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: # Checkout to target branch - uses: actions/checkout@v4 From 90d6dec90ee38280965759019ea5bb18f3ad0125 Mon Sep 17 00:00:00 2001 From: KoshaevEugeny <103786108+akulistus@users.noreply.github.com> Date: Thu, 8 Jan 2026 01:44:19 +0300 Subject: [PATCH 07/12] fix(blockSettings): prevent warning on initial read-only mode toggle (#2969) Co-authored-by: Peter --- docs/CHANGELOG.md | 4 ++++ package.json | 2 +- src/components/modules/toolbar/blockSettings.ts | 12 +++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7eff455e..0a85fb77 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.31.1 + +- `Fix` - Prevent the warning from appearing when `readOnly` mode is initially set to `true` + ### 2.31.0 - `New` - Inline tools (those with `isReadOnlySupported` specified) can now be used in read-only mode diff --git a/package.json b/package.json index 5039584f..386aee8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.0", + "version": "2.31.1", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 88f861b4..2b1e4035 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -68,6 +68,11 @@ export default class BlockSettings extends Module { return 'flipper' in this.popover ? this.popover?.flipper : undefined; } + /** + * Flag that indicates whether the `EditorMobileLayoutToggled` event listener is attached. + */ + private hasMobileLayoutToggleListener = false; + /** * Page selection utils */ @@ -92,6 +97,7 @@ export default class BlockSettings extends Module { } this.eventsDispatcher.on(EditorMobileLayoutToggled, this.close); + this.hasMobileLayoutToggleListener = true; } /** @@ -100,7 +106,11 @@ export default class BlockSettings extends Module { public destroy(): void { this.removeAllNodes(); this.listeners.destroy(); - this.eventsDispatcher.off(EditorMobileLayoutToggled, this.close); + + if (this.hasMobileLayoutToggleListener) { + this.eventsDispatcher.off(EditorMobileLayoutToggled, this.close); + this.hasMobileLayoutToggleListener = false; + } } /** From 9f942ca72a1ff130d2b5d7f1820bf913633a24d2 Mon Sep 17 00:00:00 2001 From: Alex Gaillard Date: Thu, 12 Feb 2026 10:45:21 -0500 Subject: [PATCH 08/12] fix(LinkInlineTool): improve unlink behavior based on input state (#2979) * fix(LinkInlineTool): improve unlink behavior based on input state * 2.31.2-hotfix.0 * fix(linkTool): Add test case to ensure link preservation when applying bold to linked text * Revert "2.31.2-hotfix.0" This reverts commit c68ae54c77a402f124ecabc2872372f0fa18b257. * Add fix entry to changelog * Bump version * Revert "Add fix entry to changelog" This reverts commit 7e537d662a15c98c900cef49c7612c60bf6fd821. * Add fix entry to changelog without formatting * Refactor test for compatibility with firefox --- docs/CHANGELOG.md | 4 ++ package.json | 2 +- .../inline-tools/inline-tool-link.ts | 20 ++++++-- test/cypress/tests/inline-tools/link.cy.ts | 50 +++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0a85fb77..1fc371f9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.31.2 + +- `Fix` - Prevent link removal when applying bold to linked text + ### 2.31.1 - `Fix` - Prevent the warning from appearing when `readOnly` mode is initially set to `true` diff --git a/package.json b/package.json index 386aee8c..1005b9fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.1", + "version": "2.31.2", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", diff --git a/src/components/inline-tools/inline-tool-link.ts b/src/components/inline-tools/inline-tool-link.ts index 9b413a56..999a30c4 100644 --- a/src/components/inline-tools/inline-tool-link.ts +++ b/src/components/inline-tools/inline-tool-link.ts @@ -172,11 +172,21 @@ export default class LinkInlineTool implements InlineTool { * Unlink icon pressed */ if (parentAnchor) { - this.selection.expandToTag(parentAnchor); - this.unlink(); - this.closeActions(); - this.checkState(); - this.toolbar.close(); + /** + * If input is not opened, treat click as explicit unlink action. + * If input is opened (e.g., programmatic close when switching tools), avoid unlinking. + */ + if (!this.inputOpened) { + this.selection.expandToTag(parentAnchor); + this.unlink(); + this.closeActions(); + this.checkState(); + this.toolbar.close(); + } else { + /** Only close actions without clearing saved selection to preserve user state */ + this.closeActions(false); + this.checkState(); + } return; } diff --git a/test/cypress/tests/inline-tools/link.cy.ts b/test/cypress/tests/inline-tools/link.cy.ts index 3077e7d8..0c5e934a 100644 --- a/test/cypress/tests/inline-tools/link.cy.ts +++ b/test/cypress/tests/inline-tools/link.cy.ts @@ -71,4 +71,54 @@ describe('Inline Tool Link', () => { .find('.ce-paragraph span[style]') .should('not.exist'); }); + + it('should preserve link when applying bold to linked text', () => { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Text with link', + }, + }, + ], + }, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .selectText('Text with link'); + + cy.get('[data-cy=editorjs]') + .find('[data-item-name=link]') + .click(); + + cy.get('[data-cy=editorjs]') + .find('.ce-inline-tool-input') + .type('https://editorjs.io') + .type('{enter}'); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('a') + .should('have.attr', 'href', 'https://editorjs.io'); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('a') + .selectText('Text with link'); + + cy.get('[data-cy=editorjs]') + .find('[data-item-name=bold]') + .click(); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('a') + .should('have.attr', 'href', 'https://editorjs.io') + .find('b') + .should('exist') + .should('contain', 'Text with link'); + }); }); From cc624cad2e67cee01e90eb1af5e127ea4a1b678a Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Wed, 18 Feb 2026 15:32:29 +0300 Subject: [PATCH 09/12] i18n and rtl examples updated --- example/example-i18n.html | 22 ++++++++++++---------- example/example-rtl.html | 6 ------ src/components/dom.ts | 11 +++++++++++ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/example/example-i18n.html b/example/example-i18n.html index f765f1e9..1c40d370 100644 --- a/example/example-i18n.html +++ b/example/example-i18n.html @@ -50,7 +50,6 @@ - @@ -112,11 +111,6 @@ shortcut: 'CMD+SHIFT+L' }, - checklist: { - class: Checklist, - inlineToolbar: true, - }, - quote: { class: Quote, inlineToolbar: true, @@ -202,7 +196,7 @@ /** * Translation of "Convert To" at the Block Tunes Popover */ - "Convert to": "Конвертировать в" + "Convert to": "Конвертировать в", } }, @@ -212,7 +206,8 @@ "toolNames": { "Text": "Параграф", "Heading": "Заголовок", - "List": "Список", + "Ordered List": "Нумерованный список", + "Unordered List": "Маркированный список", "Warning": "Примечание", "Checklist": "Чеклист", "Quote": "Цитата", @@ -270,7 +265,12 @@ "Wrong response format from the server": "Неполадки на сервере", }, "header": { - "Header": "Заголовок", + "Heading 1": "Заголовок 1", + "Heading 2": "Заголовок 2", + "Heading 3": "Заголовок 3", + "Heading 4": "Заголовок 4", + "Heading 5": "Заголовок 5", + "Heading 6": "Заголовок 6", }, "paragraph": { "Enter something": "Введите текст" @@ -278,6 +278,7 @@ "list": { "Ordered": "Нумерованный", "Unordered": "Маркированный", + "Checklist": "Чеклист", }, /** * Translation of "Convert To" at the Inline Toolbar hint @@ -298,7 +299,8 @@ * Also, there are few internal block tunes: "delete", "moveUp" and "moveDown" */ "delete": { - "Delete": "Удалить" + "Delete": "Удалить", + "Click to delete": "Подтвердить удаление" }, "moveUp": { "Move up": "Переместить вверх" diff --git a/example/example-rtl.html b/example/example-rtl.html index 1a347316..548c4f4e 100644 --- a/example/example-rtl.html +++ b/example/example-rtl.html @@ -120,12 +120,6 @@ inlineToolbar: ['link'], }, - list: { - class: List, - inlineToolbar: true, - shortcut: 'CMD+SHIFT+L' - }, - checklist: { class: Checklist, inlineToolbar: true, diff --git a/src/components/dom.ts b/src/components/dom.ts index 67573555..0cbfbeda 100644 --- a/src/components/dom.ts +++ b/src/components/dom.ts @@ -623,6 +623,9 @@ export default class Dom { /** * If no node found or last node is empty, return null + * - The root node has no text nodes at all + * - The TreeWalker couldn't find any text nodes in the DOM tree + * - The root node itself is null or invalid */ if (!lastTextNode) { return { @@ -633,6 +636,14 @@ export default class Dom { const textContent = lastTextNode.textContent; + /** + * - The text node exists but has no content (textContent is null) + * - The text node exists but has empty content (textContent.length === 0) + * This could be due to: + * - Empty text nodes () + * - Nodes with only whitespace + * - Nodes that were cleared but not removed + */ if (textContent === null || textContent.length === 0) { return { node: null, From a89f3d0eda284490693abc9663c365b3d1f7556a Mon Sep 17 00:00:00 2001 From: KoshaevEugeny <103786108+akulistus@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:09:54 +0300 Subject: [PATCH 10/12] Fix link tool interaction with other tools (#2977) * fix(selection): removeFakeBackground no longer removes text formatting * fix(selection): fix jsdoc * fix(linkTool): add test case to ensure text formatting preservation when applying link * Add fix entry to changelog * bump version --- docs/CHANGELOG.md | 4 ++ package.json | 2 +- src/components/selection.ts | 5 +- test/cypress/tests/inline-tools/link.cy.ts | 71 ++++++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1fc371f9..75194262 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.31.3 + +- `Fix` - Prevent text formatting removal when applying link + ### 2.31.2 - `Fix` - Prevent link removal when applying bold to linked text diff --git a/package.json b/package.json index 1005b9fd..e17e6f2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.2", + "version": "2.31.3", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", diff --git a/src/components/selection.ts b/src/components/selection.ts index 1ab2e568..40cceaeb 100644 --- a/src/components/selection.ts +++ b/src/components/selection.ts @@ -57,10 +57,9 @@ export default class SelectionUtils { public isFakeBackgroundEnabled = false; /** - * Native Document's commands for fake background + * Native Document's command for fake background */ private readonly commandBackground: string = 'backColor'; - private readonly commandRemoveFormat: string = 'removeFormat'; /** * Editor styles @@ -416,9 +415,9 @@ export default class SelectionUtils { if (!this.isFakeBackgroundEnabled) { return; } + document.execCommand(this.commandBackground, false, 'transparent'); this.isFakeBackgroundEnabled = false; - document.execCommand(this.commandRemoveFormat); } /** diff --git a/test/cypress/tests/inline-tools/link.cy.ts b/test/cypress/tests/inline-tools/link.cy.ts index 0c5e934a..8ae7220d 100644 --- a/test/cypress/tests/inline-tools/link.cy.ts +++ b/test/cypress/tests/inline-tools/link.cy.ts @@ -121,4 +121,75 @@ describe('Inline Tool Link', () => { .should('exist') .should('contain', 'Text with link'); }); + + it('should preserve bold and italic when applying link', () => { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Bold and italic text', + }, + }, + ], + }, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .selectText('Bold and italic text'); + + cy.get('[data-cy=editorjs]') + .find('[data-item-name=bold]') + .click(); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('b') + .should('exist') + .should('contain', 'Bold and italic text'); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('b') + .selectText('Bold and italic text'); + + cy.get('[data-cy=editorjs]') + .find('[data-item-name=italic]') + .click(); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('b') + .should('exist') + .find('i') + .should('exist') + .should('contain', 'Bold and italic text'); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('b') + .find('i') + .selectText('Bold and italic text'); + + cy.get('[data-cy=editorjs]') + .find('[data-item-name=link]') + .click(); + + cy.get('[data-cy=editorjs]') + .find('.ce-inline-tool-input') + .type('https://editorjs.io') + .type('{enter}'); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('b') + .should('exist') + .find('i') + .should('exist') + .find('a') + .should('have.attr', 'href', 'https://editorjs.io') + .should('contain', 'Bold and italic text'); + }); }); From b69aa1ed25227311ae50e59f6310457b02f053ce Mon Sep 17 00:00:00 2001 From: KoshaevEugeny <103786108+akulistus@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:45:11 +0300 Subject: [PATCH 11/12] fix(inline-tool-link): use defaultValue to prevent selectionchange event (#2993) * fix(inline-tool-link): use defaultValue to prevent selectionchange event * fix(link-tool): handle formatted linked text clicks * fix test errors * Revert "fix test errors" This reverts commit 582e137b77a159aa7d9d6c6459e9694d426463ad. * Revert "fix(link-tool): handle formatted linked text clicks" This reverts commit ae90e03c602725259f262570746495c1eaa8107f. --- docs/CHANGELOG.md | 4 ++++ package.json | 2 +- src/components/inline-tools/inline-tool-link.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 75194262..ce1e6997 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.31.4 + +- `Fix` - Prevent inline-toolbar re-renders when linked text is selected + ### 2.31.3 - `Fix` - Prevent text formatting removal when applying link diff --git a/package.json b/package.json index e17e6f2a..49b61542 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.3", + "version": "2.31.4", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", diff --git a/src/components/inline-tools/inline-tool-link.ts b/src/components/inline-tools/inline-tool-link.ts index 999a30c4..0bef25c7 100644 --- a/src/components/inline-tools/inline-tool-link.ts +++ b/src/components/inline-tools/inline-tool-link.ts @@ -212,7 +212,7 @@ export default class LinkInlineTool implements InlineTool { */ const hrefAttr = anchorTag.getAttribute('href'); - this.nodes.input.value = hrefAttr !== 'null' ? hrefAttr : ''; + this.nodes.input.defaultValue = hrefAttr !== 'null' ? hrefAttr : ''; this.selection.save(); } else { From 530ec56bb87e603930983eaabf1ac460a238b9b3 Mon Sep 17 00:00:00 2001 From: KoshaevEugeny <103786108+akulistus@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:58:30 +0300 Subject: [PATCH 12/12] fix(link-tool): open new window with url when formatted link clicked via ctrl key (#2996) * fix(link-tool): open new window with url when formatted link clicked via ctrl key * add test * fix lint * bump version and add changelog * Apply suggestions from code review Co-authored-by: Peter Co-authored-by: KoshaevEugeny <103786108+akulistus@users.noreply.github.com> * Update test/cypress/tests/inline-tools/link.cy.ts Co-authored-by: Peter --------- Co-authored-by: Peter --- docs/CHANGELOG.md | 4 ++ package.json | 2 +- src/components/dom.ts | 10 +++++ src/components/modules/ui.ts | 7 ++-- test/cypress/tests/inline-tools/link.cy.ts | 46 ++++++++++++++++++++++ 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ce1e6997..3367bec6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 2.31.5 + +- `Fix` - Handle __Ctrl + click__ on links with inline styles applied (e.g., bold, italic) + ### 2.31.4 - `Fix` - Prevent inline-toolbar re-renders when linked text is selected diff --git a/package.json b/package.json index 49b61542..ee5f1499 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.4", + "version": "2.31.5", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", diff --git a/src/components/dom.ts b/src/components/dom.ts index 0cbfbeda..2f67e49a 100644 --- a/src/components/dom.ts +++ b/src/components/dom.ts @@ -566,6 +566,16 @@ export default class Dom { return element.tagName.toLowerCase() === 'a'; } + /** + * Returns the closest ancestor anchor (A tag) of the given element (including itself) + * + * @param element - element to check + * @returns {HTMLAnchorElement | null} + */ + public static getClosestAnchor(element: Element): HTMLAnchorElement | null { + return element.closest("a"); + } + /** * Return element's offset related to the document * diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index a4d3baad..d8dc3798 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -773,12 +773,13 @@ export default class UI extends Module { */ const element = event.target as Element; const ctrlKey = event.metaKey || event.ctrlKey; - - if ($.isAnchor(element) && ctrlKey) { + const anchor = $.getClosestAnchor(element); + + if (anchor && ctrlKey) { event.stopImmediatePropagation(); event.stopPropagation(); - const href = element.getAttribute('href'); + const href = anchor.getAttribute('href'); const validUrl = _.getValidUrl(href); _.openTab(validUrl); diff --git a/test/cypress/tests/inline-tools/link.cy.ts b/test/cypress/tests/inline-tools/link.cy.ts index 8ae7220d..7d3fa121 100644 --- a/test/cypress/tests/inline-tools/link.cy.ts +++ b/test/cypress/tests/inline-tools/link.cy.ts @@ -192,4 +192,50 @@ describe('Inline Tool Link', () => { .should('have.attr', 'href', 'https://editorjs.io') .should('contain', 'Bold and italic text'); }); + + it('should open a link if it is wrapped in another formatting', () => { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Link text', + }, + }, + ], + }, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .selectText('Link text'); + + cy.get('[data-cy=editorjs]') + .find('[data-item-name=link]') + .click(); + + cy.get('[data-cy=editorjs]') + .find('.ce-inline-tool-input') + .type('https://test.io/') + .type('{enter}'); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('a') + .selectText('Link text'); + + cy.get('[data-cy=editorjs]') + .find('[data-item-name=italic]') + .click(); + + cy.window().then((win) => { + cy.stub(win, 'open').as('windowOpen'); + }); + + cy.contains('[data-cy=editorjs] div.ce-block i', 'Link text') + .click({ ctrlKey: true }); + + cy.get('@windowOpen').should('be.calledWith', 'https://test.io/'); + }); });