diff --git a/.github/workflows/bump-version-on-merge-next.yml b/.github/workflows/bump-version-on-merge-next.yml index 023bf809..2a592e3c 100644 --- a/.github/workflows/bump-version-on-merge-next.yml +++ b/.github/workflows/bump-version-on-merge-next.yml @@ -17,7 +17,7 @@ jobs: # If pull request was merged then we should check for a package version update check-for-no-version-changing: if: github.event.pull_request.merged == true - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 permissions: actions: write steps: diff --git a/.github/workflows/create-a-release-draft.yml b/.github/workflows/create-a-release-draft.yml index 02ffb1b3..ec728b8a 100644 --- a/.github/workflows/create-a-release-draft.yml +++ b/.github/workflows/create-a-release-draft.yml @@ -17,7 +17,7 @@ jobs: # If pull request was merged then we should check for a package version update check-version-changing: if: github.event.pull_request.merged == true - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 permissions: actions: write steps: diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index e9ed6ae2..51718006 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -8,19 +8,12 @@ jobs: matrix: browser: [firefox, chrome, edge] - runs-on: ubuntu-22.04 + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 18 - - - name: Setup Firefox - if: matrix.browser == 'firefox' - uses: browser-actions/setup-firefox@v1 - with: - firefox-version: '115.0esr' - + - uses: actions/checkout@v4 - uses: cypress-io/github-action@v6 with: config: video=false diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index c85e5ca9..973af889 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -5,14 +5,10 @@ on: [pull_request] jobs: lint: name: ESlint - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v3 - with: - node-version: 18 - - name: Cache dependencies uses: actions/cache@v4 with: diff --git a/.github/workflows/publish-package-to-npm.yml b/.github/workflows/publish-package-to-npm.yml index e867a7ed..b6ec6935 100644 --- a/.github/workflows/publish-package-to-npm.yml +++ b/.github/workflows/publish-package-to-npm.yml @@ -7,7 +7,7 @@ on: jobs: publish: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: # Checkout to target branch - uses: actions/checkout@v4 diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index ef33d651..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v18.20.1 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3367bec6..5770a1e7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,25 +1,5 @@ # 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 - -### 2.31.1 - -- `Fix` - Prevent the warning from appearing when `readOnly` mode is initially set to `true` - ### 2.31.0 - `New` - Inline tools (those with `isReadOnlySupported` specified) can now be used in read-only mode @@ -36,11 +16,6 @@ - `DX` - Tools submodules removed from the repository - `Improvement` - Shift + Down/Up will allow to select next/previous line instead of Inline Toolbar flipping - `Improvement` - The API `caret.setToBlock()` offset now works across the entire block content, not just the first or last node. -- `Improvement` - The API `blocks.renderFromHTML()` became async and now can be awaited. -- `Fix` - `blocks.renderFromHTML()` — Error "Can't find a Block to remove." fixed -- `Fix` - The API `.clear()` index invalidation fixed - - ### 2.30.7 diff --git a/example/example-i18n.html b/example/example-i18n.html index 1c40d370..bc1aaf7a 100644 --- a/example/example-i18n.html +++ b/example/example-i18n.html @@ -50,6 +50,7 @@ + @@ -106,11 +107,16 @@ image: ImageTool, list: { - class: EditorjsList, + class: List, inlineToolbar: true, shortcut: 'CMD+SHIFT+L' }, + checklist: { + class: Checklist, + inlineToolbar: true, + }, + quote: { class: Quote, inlineToolbar: true, @@ -192,11 +198,7 @@ }, "popover": { "Filter": "Поиск", - "Nothing found": "Ничего не найдено", - /** - * Translation of "Convert To" at the Block Tunes Popover - */ - "Convert to": "Конвертировать в", + "Nothing found": "Ничего не найдено" } }, @@ -206,8 +208,7 @@ "toolNames": { "Text": "Параграф", "Heading": "Заголовок", - "Ordered List": "Нумерованный список", - "Unordered List": "Маркированный список", + "List": "Список", "Warning": "Примечание", "Checklist": "Чеклист", "Quote": "Цитата", @@ -220,7 +221,7 @@ "Bold": "Полужирный", "Italic": "Курсив", "InlineCode": "Моноширинный", - "Image": "Картинка", + "Image": "Картинка" }, /** @@ -265,12 +266,7 @@ "Wrong response format from the server": "Неполадки на сервере", }, "header": { - "Heading 1": "Заголовок 1", - "Heading 2": "Заголовок 2", - "Heading 3": "Заголовок 3", - "Heading 4": "Заголовок 4", - "Heading 5": "Заголовок 5", - "Heading 6": "Заголовок 6", + "Header": "Заголовок", }, "paragraph": { "Enter something": "Введите текст" @@ -278,14 +274,7 @@ "list": { "Ordered": "Нумерованный", "Unordered": "Маркированный", - "Checklist": "Чеклист", - }, - /** - * Translation of "Convert To" at the Inline Toolbar hint - */ - "convertTo": { - "Convert to": "Конвертировать в" - }, + } }, /** @@ -299,15 +288,14 @@ * Also, there are few internal block tunes: "delete", "moveUp" and "moveDown" */ "delete": { - "Delete": "Удалить", - "Click to delete": "Подтвердить удаление" + "Delete": "Удалить" }, "moveUp": { "Move up": "Переместить вверх" }, "moveDown": { "Move down": "Переместить вниз" - }, + } }, } }, diff --git a/example/example-rtl.html b/example/example-rtl.html index 548c4f4e..1a347316 100644 --- a/example/example-rtl.html +++ b/example/example-rtl.html @@ -120,6 +120,12 @@ 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 ee5f1499..8f60f1a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.5", + "version": "2.31.0-rc.10", "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 2f67e49a..67573555 100644 --- a/src/components/dom.ts +++ b/src/components/dom.ts @@ -566,16 +566,6 @@ 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 * @@ -633,9 +623,6 @@ 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 { @@ -646,14 +633,6 @@ 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 0bef25c7..9b413a56 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; } @@ -212,7 +202,7 @@ export default class LinkInlineTool implements InlineTool { */ const hrefAttr = anchorTag.getAttribute('href'); - this.nodes.input.defaultValue = hrefAttr !== 'null' ? hrefAttr : ''; + this.nodes.input.value = hrefAttr !== 'null' ? hrefAttr : ''; this.selection.save(); } else { diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index 9ad22176..f9297d5d 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -224,8 +224,8 @@ export default class BlocksAPI extends Module { * @param {string} data - HTML string to render * @returns {Promise} */ - public async renderFromHTML(data: string): Promise { - await this.Editor.BlockManager.clear(); + public renderFromHTML(data: string): Promise { + this.Editor.BlockManager.clear(); return this.Editor.Paste.processText(data, true); } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index be8e1e24..48fec049 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -533,8 +533,8 @@ export default class BlockManager extends Module { throw new Error('Can\'t find a Block to remove'); } - this._blocks.remove(index); block.destroy(); + this._blocks.remove(index); /** * Force call of didMutated event on Block removal @@ -894,10 +894,7 @@ export default class BlockManager extends Module { public async clear(needToAddDefaultBlock = false): Promise { const queue = new PromiseQueue(); - // Create a copy of the blocks array to avoid issues with array modification during iteration - const blocksToRemove = [...this.blocks]; - - blocksToRemove.forEach((block) => { + this.blocks.forEach((block) => { queue.add(async () => { await this.removeBlock(block, false); }); diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 2b1e4035..88f861b4 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -68,11 +68,6 @@ export default class BlockSettings extends Module { return 'flipper' in this.popover ? this.popover?.flipper : undefined; } - /** - * Flag that indicates whether the `EditorMobileLayoutToggled` event listener is attached. - */ - private hasMobileLayoutToggleListener = false; - /** * Page selection utils */ @@ -97,7 +92,6 @@ export default class BlockSettings extends Module { } this.eventsDispatcher.on(EditorMobileLayoutToggled, this.close); - this.hasMobileLayoutToggleListener = true; } /** @@ -106,11 +100,7 @@ export default class BlockSettings extends Module { public destroy(): void { this.removeAllNodes(); this.listeners.destroy(); - - if (this.hasMobileLayoutToggleListener) { - this.eventsDispatcher.off(EditorMobileLayoutToggled, this.close); - this.hasMobileLayoutToggleListener = false; - } + this.eventsDispatcher.off(EditorMobileLayoutToggled, this.close); } /** diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index d8dc3798..a4d3baad 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -773,13 +773,12 @@ export default class UI extends Module { */ const element = event.target as Element; const ctrlKey = event.metaKey || event.ctrlKey; - const anchor = $.getClosestAnchor(element); - - if (anchor && ctrlKey) { + + if ($.isAnchor(element) && ctrlKey) { event.stopImmediatePropagation(); event.stopPropagation(); - const href = anchor.getAttribute('href'); + const href = element.getAttribute('href'); const validUrl = _.getValidUrl(href); _.openTab(validUrl); diff --git a/src/components/selection.ts b/src/components/selection.ts index 40cceaeb..1ab2e568 100644 --- a/src/components/selection.ts +++ b/src/components/selection.ts @@ -57,9 +57,10 @@ export default class SelectionUtils { public isFakeBackgroundEnabled = false; /** - * Native Document's command for fake background + * Native Document's commands for fake background */ private readonly commandBackground: string = 'backColor'; + private readonly commandRemoveFormat: string = 'removeFormat'; /** * Editor styles @@ -415,9 +416,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 7d3fa121..3077e7d8 100644 --- a/test/cypress/tests/inline-tools/link.cy.ts +++ b/test/cypress/tests/inline-tools/link.cy.ts @@ -71,171 +71,4 @@ describe('Inline Tool Link', () => { .find('.ce-paragraph span[style]') .should('not.exist'); }); - - it('should preserve link when applying bold to linked text', () => { - cy.createEditor({ - data: { - blocks: [ - { - type: 'paragraph', - data: { - text: 'Text with link', - }, - }, - ], - }, - }); - - cy.get('[data-cy=editorjs]') - .find('.ce-paragraph') - .selectText('Text with link'); - - 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('a') - .should('have.attr', 'href', 'https://editorjs.io'); - - cy.get('[data-cy=editorjs]') - .find('div.ce-block') - .find('a') - .selectText('Text with link'); - - cy.get('[data-cy=editorjs]') - .find('[data-item-name=bold]') - .click(); - - cy.get('[data-cy=editorjs]') - .find('div.ce-block') - .find('a') - .should('have.attr', 'href', 'https://editorjs.io') - .find('b') - .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/'); - }); });