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] 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/'); + }); });