diff --git a/src/components/inline-tools/inline-tool-link.ts b/src/components/inline-tools/inline-tool-link.ts index 0bef25c73..2cdd87ae7 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 */ @@ -172,21 +178,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 +266,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 outside popover element + if (this.checkSelectionTarget(currentSelection.savedSelectionRange)) { + // and recover new selection + currentSelection.restore(); + } } this.nodes.input.classList.remove(this.CSS.inputShowed); @@ -414,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; + } } 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/test/cypress/tests/inline-tools/link.cy.ts b/test/cypress/tests/inline-tools/link.cy.ts index 7d3fa121a..8ea5d0cfb 100644 --- a/test/cypress/tests/inline-tools/link.cy.ts +++ b/test/cypress/tests/inline-tools/link.cy.ts @@ -238,4 +238,115 @@ 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]') + .find('.ce-paragraph') + .click(); + + cy.get('[data-cy=editorjs]') + .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', () => { + 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'); + }); }); 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; } /**