diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1fc371f9..3367bec6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,17 @@ # 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 + +### 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/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/package.json b/package.json index 1005b9fd..ee5f1499 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.2", + "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 67573555..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 * @@ -623,6 +633,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 +646,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, 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 { 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/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..7d3fa121 100644 --- a/test/cypress/tests/inline-tools/link.cy.ts +++ b/test/cypress/tests/inline-tools/link.cy.ts @@ -121,4 +121,121 @@ 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'); + }); + + 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/'); + }); });