From ca4e0adf0f7187a497dd1fdfdbfb5c6d533a7c0f Mon Sep 17 00:00:00 2001 From: Eugeny Date: Mon, 20 Apr 2026 01:28:00 +0300 Subject: [PATCH 1/5] fix(link-tool): call clear function to reset link-tool state --- .../inline-tools/inline-tool-link.ts | 28 +++++++------------ src/components/modules/toolbar/inline.ts | 3 ++ .../components/popover-item/popover-item.ts | 15 ++++++++++ .../utils/popover/popover-inline.ts | 4 +-- types/utils/popover/popover-item.d.ts | 7 +++++ 5 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/components/inline-tools/inline-tool-link.ts b/src/components/inline-tools/inline-tool-link.ts index 0bef25c73..3ae5f835e 100644 --- a/src/components/inline-tools/inline-tool-link.ts +++ b/src/components/inline-tools/inline-tool-link.ts @@ -172,21 +172,11 @@ export default class LinkInlineTool implements InlineTool { * Unlink icon pressed */ if (parentAnchor) { - /** - * 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(); - } + this.selection.expandToTag(parentAnchor); + this.unlink(); + this.closeActions(); + this.checkState(); + this.toolbar.close(); return; } @@ -270,14 +260,16 @@ export default class LinkInlineTool implements InlineTool { if (this.selection.isFakeBackgroundEnabled) { // if actions is broken by other selection We need to save new selection const currentSelection = new SelectionUtils(); - currentSelection.save(); this.selection.restore(); this.selection.removeFakeBackground(); - // and recover new selection after removing fake background - currentSelection.restore(); + // check if other selection happend + if (!currentSelection.savedSelectionRange.collapsed) { + // and recover new selection after removing fake background + currentSelection.restore(); + } } this.nodes.input.classList.remove(this.CSS.inputShowed); diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 5aa5f7dab..cf938e57a 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -391,6 +391,9 @@ export default class InlineToolbar extends Module { onActivate: () => { this.toolClicked(instance); }, + onClear: () => { + instance?.clear?.(); + }, hint: { title: toolTitle, description: shortcutBeautified, diff --git a/src/components/utils/popover/components/popover-item/popover-item.ts b/src/components/utils/popover/components/popover-item/popover-item.ts index b211cab99..c6a93c628 100644 --- a/src/components/utils/popover/components/popover-item/popover-item.ts +++ b/src/components/utils/popover/components/popover-item/popover-item.ts @@ -25,6 +25,21 @@ export abstract class PopoverItem { } } + /** + * Calls instance clear function + */ + public clear(): void { + if (this.params === undefined) { + return; + } + + if (!('onClear' in this.params)) { + return; + } + + this.params.onClear?.(this.params); + } + /** * Destroys the instance */ diff --git a/src/components/utils/popover/popover-inline.ts b/src/components/utils/popover/popover-inline.ts index ebe91223c..6a3e7b471 100644 --- a/src/components/utils/popover/popover-inline.ts +++ b/src/components/utils/popover/popover-inline.ts @@ -167,9 +167,9 @@ export class PopoverInline extends PopoverDesktop { if (item !== this.nestedPopoverTriggerItem) { /** * In case tool had special handling for toggling button (like link tool which modifies selection) - * we need to call handleClick on nested popover trigger item + * we need to call clear on nested popover trigger item to restore initial state */ - this.nestedPopoverTriggerItem?.handleClick(); + this.nestedPopoverTriggerItem?.clear(); /** * Then close the nested popover diff --git a/types/utils/popover/popover-item.d.ts b/types/utils/popover/popover-item.d.ts index 3957fdae0..31d76ce72 100644 --- a/types/utils/popover/popover-item.d.ts +++ b/types/utils/popover/popover-item.d.ts @@ -178,6 +178,13 @@ export interface PopoverItemDefaultBaseParams { * @param event - event that initiated item activation */ onActivate: (item: PopoverItemParams, event?: PointerEvent) => void; + + /** + * Popover item clear handler + * + * @param item - item to be cleared + */ + onClear: (item: PopoverItemParams) => void; } /** From 684c3ab8dd0726cd2d0c3759f3037813c61b143a Mon Sep 17 00:00:00 2001 From: Eugeny Date: Tue, 21 Apr 2026 23:55:57 +0300 Subject: [PATCH 2/5] refactor(link-tool): add JSDoc. Add function to check selectionTarget --- .../inline-tools/inline-tool-link.ts | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/components/inline-tools/inline-tool-link.ts b/src/components/inline-tools/inline-tool-link.ts index 3ae5f835e..88825191c 100644 --- a/src/components/inline-tools/inline-tool-link.ts +++ b/src/components/inline-tools/inline-tool-link.ts @@ -51,6 +51,12 @@ export default class LinkInlineTool implements InlineTool { */ private readonly ENTER_KEY: number = 13; + /** + * Popover item block CSS class + */ + private readonly POPOVER_ITEM_CLASSNAME = "ce-popover-item-html"; + + /** * Styles */ @@ -264,12 +270,12 @@ export default class LinkInlineTool implements InlineTool { this.selection.restore(); this.selection.removeFakeBackground(); - - // check if other selection happend - if (!currentSelection.savedSelectionRange.collapsed) { - // and recover new selection after removing fake background + + // check if other selection happend outside popover element + if (this.checkSelectionTarget(currentSelection.savedSelectionRange)) { + // and recover new selection currentSelection.restore(); - } + } } this.nodes.input.classList.remove(this.CSS.inputShowed); @@ -406,4 +412,22 @@ export default class LinkInlineTool implements InlineTool { private unlink(): void { document.execCommand(this.commandUnlink); } + + /** + * Checks if the current selection range starts outside + * of a popover item element + * + * @param range - The DOM Range to evaluate. + * @returns `true` if popover item block is selection target otherwise - `false`. + */ + private checkSelectionTarget(range: Range): boolean { + const container = range.startContainer; + if (container instanceof HTMLElement) { + if (container.classList.contains(this.POPOVER_ITEM_CLASSNAME)) { + return false; + } + } + + return true; + } } From 3a4c71a3b0c3ddce4430790e513949148783275b Mon Sep 17 00:00:00 2001 From: Eugeny Date: Wed, 22 Apr 2026 00:29:17 +0300 Subject: [PATCH 3/5] add tests --- test/cypress/tests/inline-tools/link.cy.ts | 104 +++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/test/cypress/tests/inline-tools/link.cy.ts b/test/cypress/tests/inline-tools/link.cy.ts index 7d3fa121a..cd96f83c0 100644 --- a/test/cypress/tests/inline-tools/link.cy.ts +++ b/test/cypress/tests/inline-tools/link.cy.ts @@ -238,4 +238,108 @@ describe('Inline Tool Link', () => { cy.get('@windowOpen').should('be.calledWith', 'https://test.io/'); }); + + it('should unlink on popover item block click', () => { + 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=link]') + .click(); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('a') + .should("not.exist"); + }); + + it('should hide popover on selection change', () => { + 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]').click(0, 0); + + cy.get('[data-cy=editorjs]') + .find('.ce-popover') + .should('not.be.visible'); + }); + + it('should restore selection and apply formatting on other popover item block click', () => { + 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('[data-item-name=bold]') + .click(); + + cy.get('[data-cy=editorjs]') + .find('div.ce-block') + .find('b') + .should("exist") + .and('have.text', 'Link text'); + }); }); From df3993e1db5e74b40277badea001728325147ade Mon Sep 17 00:00:00 2001 From: Eugeny Date: Wed, 22 Apr 2026 00:29:30 +0300 Subject: [PATCH 4/5] fix lint --- src/components/inline-tools/inline-tool-link.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/inline-tools/inline-tool-link.ts b/src/components/inline-tools/inline-tool-link.ts index 88825191c..2cdd87ae7 100644 --- a/src/components/inline-tools/inline-tool-link.ts +++ b/src/components/inline-tools/inline-tool-link.ts @@ -270,10 +270,10 @@ export default class LinkInlineTool implements InlineTool { this.selection.restore(); this.selection.removeFakeBackground(); - + // check if other selection happend outside popover element if (this.checkSelectionTarget(currentSelection.savedSelectionRange)) { - // and recover new selection + // and recover new selection currentSelection.restore(); } } From 9eb5f5402f1de26d77a1cd8476e5d2f43aa7c9e9 Mon Sep 17 00:00:00 2001 From: Eugeny Date: Wed, 22 Apr 2026 01:06:25 +0300 Subject: [PATCH 5/5] fix tests --- test/cypress/tests/inline-tools/link.cy.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/cypress/tests/inline-tools/link.cy.ts b/test/cypress/tests/inline-tools/link.cy.ts index cd96f83c0..8ea5d0cfb 100644 --- a/test/cypress/tests/inline-tools/link.cy.ts +++ b/test/cypress/tests/inline-tools/link.cy.ts @@ -303,11 +303,18 @@ describe('Inline Tool Link', () => { .find('[data-item-name=link]') .click(); - cy.get('[data-cy=editorjs]').click(0, 0); + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .click(); cy.get('[data-cy=editorjs]') - .find('.ce-popover') - .should('not.be.visible'); + .find('[data-item-name=link]') + .should('not.exist'); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .find('span') + .should('not.exist'); }); it('should restore selection and apply formatting on other popover item block click', () => {