Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 35 additions & 19 deletions src/components/inline-tools/inline-tool-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Comment on lines +417 to +418
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.

Ideally, link tool should not know about Popover and its implementation. Explain, why it is needed.

*
* @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;
}
}
3 changes: 3 additions & 0 deletions src/components/modules/toolbar/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
onActivate: () => {
this.toolClicked(instance);
},
onClear: () => {
instance?.clear?.();
},
hint: {
title: toolTitle,
description: shortcutBeautified,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ export abstract class PopoverItem {
}
}

/**
* Calls instance clear function
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.

Describe the use case please.

*/
public clear(): void {
if (this.params === undefined) {
return;
}

if (!('onClear' in this.params)) {
return;
}

this.params.onClear?.(this.params);
}

/**
* Destroys the instance
*/
Expand Down
4 changes: 2 additions & 2 deletions src/components/utils/popover/popover-inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
111 changes: 111 additions & 0 deletions test/cypress/tests/inline-tools/link.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
7 changes: 7 additions & 0 deletions types/utils/popover/popover-item.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ export interface PopoverItemDefaultBaseParams {
* @param event - event that initiated item activation
*/
onActivate: (item: PopoverItemParams, event?: PointerEvent) => void;

/**
* Popover item clear handler
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.

same here

*
* @param item - item to be cleared
*/
onClear: (item: PopoverItemParams) => void;
}

/**
Expand Down
Loading