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..1db62120 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -5,21 +5,19 @@ 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 + - name: Cache node modules + uses: actions/cache@v1 with: - node-version: 18 - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ~/.npm + path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} restore-keys: | - ${{ runner.os }}-node- + ${{ runner.OS }}-build-${{ env.cache-name }}- + ${{ runner.OS }}-build- + ${{ runner.OS }}- - run: yarn - run: yarn lint 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/.gitmodules b/.gitmodules new file mode 100644 index 00000000..44e1a2ff --- /dev/null +++ b/.gitmodules @@ -0,0 +1,48 @@ +[submodule "example/tools/inline-code"] + path = example/tools/inline-code + url = https://github.com/editor-js/inline-code +[submodule "example/tools/header"] + path = example/tools/header + url = https://github.com/editor-js/header +[submodule "example/tools/delimiter"] + path = example/tools/delimiter + url = https://github.com/editor-js/delimiter +[submodule "example/tools/quote"] + path = example/tools/quote + url = https://github.com/editor-js/quote +[submodule "example/tools/simple-image"] + path = example/tools/simple-image + url = https://github.com/editor-js/simple-image +[submodule "example/tools/marker"] + path = example/tools/marker + url = https://github.com/editor-js/marker +[submodule "example/tools/code"] + path = example/tools/code + url = https://github.com/editor-js/code +[submodule "example/tools/image"] + path = example/tools/image + url = https://github.com/editor-js/image +[submodule "example/tools/embed"] + path = example/tools/embed + url = https://github.com/editor-js/embed +[submodule "example/tools/table"] + path = example/tools/table + url = https://github.com/editor-js/table +[submodule "example/tools/link"] + path = example/tools/link + url = https://github.com/editor-js/link +[submodule "example/tools/raw"] + path = example/tools/raw + url = https://github.com/editor-js/raw +[submodule "example/tools/warning"] + path = example/tools/warning + url = https://github.com/editor-js/warning +[submodule "example/tools/underline"] + path = example/tools/underline + url = https://github.com/editor-js/underline +[submodule "example/tools/text-variant-tune"] + path = example/tools/text-variant-tune + url = https://github.com/editor-js/text-variant-tune +[submodule "example/tools/list"] + path = example/tools/list + url = https://github.com/editor-js/list 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/devserver.js b/devserver.js new file mode 100644 index 00000000..5087a7b4 --- /dev/null +++ b/devserver.js @@ -0,0 +1,128 @@ +/** + * Server for testing example page on mobile devices. + * + * Usage: + * 1. run `yarn devserver:start` + * 2. Open `http://{ip_address}:3000/example/example-dev.html` + * where {ip_address} is IP of your machine. + * + * Also, can serve static files from `/example` or `/dist` on any device in local network. + */ +const path = require('path'); +const fs = require('fs'); +const http = require('http'); +const { networkInterfaces } = require('os'); + +const port = 3000; +const localhost = '127.0.0.1'; +const nonRoutableAddress = '0.0.0.0'; +const host = getHost(); +const server = http.createServer(serveStatic([ + '/example', + '/dist', +])); + +server.listen(port, nonRoutableAddress, () => { + console.log(` + +${wrapInColor('Editor.js 💖', consoleColors.hiColor)} devserver is running ᕕ(⌐■_■)ᕗ ✨ +--------------------------------------------- +${wrapInColor('http://' + host + ':' + port + '/example/example-dev.html', consoleColors.fgGreen)} +--------------------------------------------- +Page can be opened from any device connected to the same local network. +`); + + if (host === localhost) { + console.log(wrapInColor('Looks like you are not connected to any Network so you couldn\'t debug the Editor on your mobile device at the moment.', consoleColors.fgRed)); + } +}); + +/** + * Serves files from specified directories + * + * @param {string[]} paths - directories files from which should be served + * @returns {Function} + */ +function serveStatic(paths) { + return (request, response) => { + const resource = request.url; + const isPathAllowed = paths.find(p => resource.startsWith(p)); + + if (!isPathAllowed) { + response.writeHead(404); + response.end(); + + return; + } + const filePath = path.join(__dirname, resource); + + try { + const stat = fs.statSync(filePath); + + response.writeHead(200, { + 'Content-Length': stat.size, + }); + const readStream = fs.createReadStream(filePath); + + readStream.on('error', e => { + throw e; + }); + readStream.pipe(response); + } catch (e) { + response.writeHead(500); + response.end(e.toString()); + } + }; +} + +/** + * Returns IP address of a machine + * + * @returns {string} + */ +function getHost() { + const nets = networkInterfaces(); + const results = {}; + + for (const name of Object.keys(nets)) { + for (const net of nets[name]) { + // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses + if (net.family === 'IPv4' && !net.internal) { + if (!results[name]) { + results[name] = []; + } + results[name].push(net.address); + } + } + } + + /** + * Offline case + */ + if (Object.keys(results).length === 0) { + return localhost; + } + + return results['en0'][0]; +} + +/** + * Terminal output colors + */ +const consoleColors = { + fgMagenta: 35, + fgRed: 31, + fgGreen: 32, + hiColor: 1, +}; + +/** + * Set a terminal color to the message + * + * @param {string} msg - text to wrap + * @param {string} color - color + * @returns {string} + */ +function wrapInColor(msg, color) { + return '\x1b[' + color + 'm' + msg + '\x1b[0m'; +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3367bec6..33aa0a09 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 @@ -31,16 +11,6 @@ - `Improvement` - The current block reference will be updated in read-only mode when blocks are clicked - `Fix` - codex-notifier and codex-tooltip moved from devDependencies to dependencies in package.json to solve type errors - `Fix` - Handle whitespace input in empty placeholder elements to prevent caret from moving unexpectedly to the end of the placeholder -- `Fix` - Fix the memory leak issue in `Shortcuts` class -- `Fix` - Fix when / overides selected text outside of the editor -- `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-dev.html b/example/example-dev.html new file mode 100644 index 00000000..05d71317 --- /dev/null +++ b/example/example-dev.html @@ -0,0 +1,422 @@ + + + + + + Editor.js 🤩🧦🤨 example + + + + + + + +
+
+ + + +
+
+
+
+ No core bundle file found. Run yarn build +
+
+ No submodules found. Run yarn pull_tools && yarn tools:update +
+
+ editor.save() +
+
+
+ Readonly: + + Off + +   +
+ toggle +
+
+
+
+ Show + blocks boundaries +
+
+
+
+ Enable + thin mode +
+
+
+
+
+
+
+
+
+

+
+      
+    
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + 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..abeab23b 100644 --- a/example/example-rtl.html +++ b/example/example-rtl.html @@ -53,21 +53,21 @@ Read more in Tool's README file. For example: https://github.com/editor-js/header#installation --> - - - - - - - - - - - - + + + + + + + + + + + + - - + + @@ -120,6 +120,12 @@ inlineToolbar: ['link'], }, + list: { + class: List, + inlineToolbar: true, + shortcut: 'CMD+SHIFT+L' + }, + checklist: { class: Checklist, inlineToolbar: true, diff --git a/example/tools/code b/example/tools/code new file mode 160000 index 00000000..82402cb8 --- /dev/null +++ b/example/tools/code @@ -0,0 +1 @@ +Subproject commit 82402cb86a327e3c64bef653d953533e2c3613dd diff --git a/example/tools/delimiter b/example/tools/delimiter new file mode 160000 index 00000000..95a5eb90 --- /dev/null +++ b/example/tools/delimiter @@ -0,0 +1 @@ +Subproject commit 95a5eb90dd2e2e8ab153eb66b59a70cdafdf2d7f diff --git a/example/tools/embed b/example/tools/embed new file mode 160000 index 00000000..801580fb --- /dev/null +++ b/example/tools/embed @@ -0,0 +1 @@ +Subproject commit 801580fbdb7ab0ad1e975cfdaab38ada6625e301 diff --git a/example/tools/header b/example/tools/header new file mode 160000 index 00000000..3e457cba --- /dev/null +++ b/example/tools/header @@ -0,0 +1 @@ +Subproject commit 3e457cbac2c5da53fff1b02b99ddaccaa577f401 diff --git a/example/tools/image b/example/tools/image new file mode 160000 index 00000000..c8236e57 --- /dev/null +++ b/example/tools/image @@ -0,0 +1 @@ +Subproject commit c8236e5765294f6b6590573910a68d3826671838 diff --git a/example/tools/inline-code b/example/tools/inline-code new file mode 160000 index 00000000..31a086d7 --- /dev/null +++ b/example/tools/inline-code @@ -0,0 +1 @@ +Subproject commit 31a086d7dc97169de34b9c191735cba7d63562d6 diff --git a/example/tools/link b/example/tools/link new file mode 160000 index 00000000..aaa69d54 --- /dev/null +++ b/example/tools/link @@ -0,0 +1 @@ +Subproject commit aaa69d5408bad34027d6252a3892d40f9fa121be diff --git a/example/tools/list b/example/tools/list new file mode 160000 index 00000000..bbc46d55 --- /dev/null +++ b/example/tools/list @@ -0,0 +1 @@ +Subproject commit bbc46d557bb5711dd27517272ae2754e1da04697 diff --git a/example/tools/marker b/example/tools/marker new file mode 160000 index 00000000..8d6897fc --- /dev/null +++ b/example/tools/marker @@ -0,0 +1 @@ +Subproject commit 8d6897fca43e387bcdf4a681380be975fe8f2a07 diff --git a/example/tools/quote b/example/tools/quote new file mode 160000 index 00000000..78f70cf2 --- /dev/null +++ b/example/tools/quote @@ -0,0 +1 @@ +Subproject commit 78f70cf2391cc8aaf2d2e59615de3ad833d180c3 diff --git a/example/tools/raw b/example/tools/raw new file mode 160000 index 00000000..84b7d56b --- /dev/null +++ b/example/tools/raw @@ -0,0 +1 @@ +Subproject commit 84b7d56b26a66d121edb6682ca205bf995d39034 diff --git a/example/tools/simple-image b/example/tools/simple-image new file mode 160000 index 00000000..96388352 --- /dev/null +++ b/example/tools/simple-image @@ -0,0 +1 @@ +Subproject commit 963883520c7bbe5040366335c9a37bbdc7cf60fd diff --git a/example/tools/table b/example/tools/table new file mode 160000 index 00000000..5a57621c --- /dev/null +++ b/example/tools/table @@ -0,0 +1 @@ +Subproject commit 5a57621c4e1abb884fd03e70862cb05b10bfe405 diff --git a/example/tools/text-variant-tune b/example/tools/text-variant-tune new file mode 160000 index 00000000..7f51a16d --- /dev/null +++ b/example/tools/text-variant-tune @@ -0,0 +1 @@ +Subproject commit 7f51a16d4ab46eff9364f09cea52e09518896d2a diff --git a/example/tools/warning b/example/tools/warning new file mode 160000 index 00000000..0f3ec98b --- /dev/null +++ b/example/tools/warning @@ -0,0 +1 @@ +Subproject commit 0f3ec98b9a3b0ea8a9a71cffcb4e596e5c98aecb diff --git a/package.json b/package.json index ee5f1499..8081225f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.31.5", + "version": "2.31.0-rc.7", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", @@ -21,8 +21,16 @@ "lint:errors": "eslint src/ --ext .ts --quiet", "lint:fix": "eslint src/ --ext .ts --fix", "lint:tests": "eslint test/ --ext .ts", + "pull_tools": "git submodule update --init --recursive", + "_tools:checkout": "git submodule foreach \"git checkout master || git checkout main\"", + "_tools:pull": "git submodule foreach git pull", + "_tools:yarn": "git submodule foreach yarn", + "_tools:build": "git submodule foreach yarn build", + "_tools:make": "yarn _tools:yarn && yarn _tools:build", + "tools:update": "yarn _tools:checkout && yarn _tools:pull && yarn _tools:make", "test:e2e": "yarn build:test && cypress run", - "test:e2e:open": "yarn build:test && cypress open" + "test:e2e:open": "yarn build:test && cypress open", + "devserver:start": "yarn build && node ./devserver.js" }, "author": "CodeX", "license": "Apache-2.0", diff --git a/src/components/dom.ts b/src/components/dom.ts index 2f67e49a..24104131 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 * @@ -597,80 +587,6 @@ export default class Dom { right: left + rect.width, }; } - - /** - * Find text node and offset by total content offset - * - * @param {Node} root - root node to start search from - * @param {number} totalOffset - offset relative to the root node content - * @returns {{node: Node | null, offset: number}} - node and offset inside node - */ - public static getNodeByOffset(root: Node, totalOffset: number): {node: Node | null; offset: number} { - let currentOffset = 0; - let lastTextNode: Node | null = null; - - const walker = document.createTreeWalker( - root, - NodeFilter.SHOW_TEXT, - null - ); - - let node: Node | null = walker.nextNode(); - - while (node) { - const textContent = node.textContent; - const nodeLength = textContent === null ? 0 : textContent.length; - - lastTextNode = node; - - if (currentOffset + nodeLength >= totalOffset) { - break; - } - - currentOffset += nodeLength; - node = walker.nextNode(); - } - - /** - * 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 { - node: null, - offset: 0, - }; - } - - 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, - offset: 0, - }; - } - - /** - * Calculate offset inside found node - */ - const nodeOffset = Math.min(totalOffset - currentOffset, textContent.length); - - return { - node: lastTextNode, - offset: nodeOffset, - }; - } } /** diff --git a/src/components/flipper.ts b/src/components/flipper.ts index 5b6e0435..516e2b62 100644 --- a/src/components/flipper.ts +++ b/src/components/flipper.ts @@ -199,23 +199,13 @@ export default class Flipper { * * @param event - keydown event */ - private onKeyDown = (event: KeyboardEvent): void => { + private onKeyDown = (event): void => { const isReady = this.isEventReadyForHandling(event); if (!isReady) { return; } - const isShiftKey = event.shiftKey; - - /** - * If shift key is pressed, do nothing - * Allows to select next/prev lines of text using keyboard - */ - if (isShiftKey === true) { - return; - } - /** * Prevent only used keys default behaviour * (allows to navigate by ARROW DOWN, for example) 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/blockEvents.ts b/src/components/modules/blockEvents.ts index 40e0973e..c48bba53 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -237,12 +237,6 @@ export default class BlockEvents extends Module { * @param event - keydown */ private slashPressed(event: KeyboardEvent): void { - const wasEventTriggeredInsideEditor = this.Editor.UI.nodes.wrapper.contains(event.target as Node); - - if (!wasEventTriggeredInsideEditor) { - return; - } - const currentBlock = this.Editor.BlockManager.currentBlock; const canOpenToolbox = currentBlock.isEmpty; 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/caret.ts b/src/components/modules/caret.ts index db8a4f3b..276eef4b 100644 --- a/src/components/modules/caret.ts +++ b/src/components/modules/caret.ts @@ -43,7 +43,7 @@ export default class Caret extends Module { * @param {Block} block - Block class * @param {string} position - position where to set caret. * If default - leave default behaviour and apply offset if it's passed - * @param {number} offset - caret offset regarding to the block content + * @param {number} offset - caret offset regarding to the text node */ public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void { const { BlockManager, BlockSelection } = this.Editor; @@ -88,32 +88,23 @@ export default class Caret extends Module { return; } - let nodeToSet: Node; - let offsetToSet = offset; + const nodeToSet = $.getDeepestNode(element, position === this.positions.END); + const contentLength = $.getContentLength(nodeToSet); - if (position === this.positions.START) { - nodeToSet = $.getDeepestNode(element, false) as Node; - offsetToSet = 0; - } else if (position === this.positions.END) { - nodeToSet = $.getDeepestNode(element, true) as Node; - offsetToSet = $.getContentLength(nodeToSet); - } else { - const { node, offset: nodeOffset } = $.getNodeByOffset(element, offset); - - if (node) { - nodeToSet = node; - offsetToSet = nodeOffset; - } else { // case for empty block's input - nodeToSet = $.getDeepestNode(element, false) as Node; - offsetToSet = 0; - } + switch (true) { + case position === this.positions.START: + offset = 0; + break; + case position === this.positions.END: + case offset > contentLength: + offset = contentLength; + break; } - this.set(nodeToSet as HTMLElement, offsetToSet); + this.set(nodeToSet as HTMLElement, offset); BlockManager.setCurrentBlockByChildNode(block.holder); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - BlockManager.currentBlock!.currentInput = element; + BlockManager.currentBlock.currentInput = element; } /** 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/src/components/utils/shortcuts.ts b/src/components/utils/shortcuts.ts index 967243d2..8cf51ff9 100644 --- a/src/components/utils/shortcuts.ts +++ b/src/components/utils/shortcuts.ts @@ -86,15 +86,7 @@ class Shortcuts { const shortcuts = this.registeredShortcuts.get(element); - const filteredShortcuts = shortcuts.filter(el => el !== shortcut); - - if (filteredShortcuts.length === 0) { - this.registeredShortcuts.delete(element); - - return; - } - - this.registeredShortcuts.set(element, filteredShortcuts); + this.registeredShortcuts.set(element, shortcuts.filter(el => el !== shortcut)); } /** diff --git a/test/cypress/support/utils/createParagraphMock.ts b/test/cypress/support/utils/createParagraphMock.ts deleted file mode 100644 index 30166e87..00000000 --- a/test/cypress/support/utils/createParagraphMock.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { nanoid } from 'nanoid'; - -/** - * Creates a paragraph mock - * - * @param text - text for the paragraph - * @returns paragraph mock - */ -export function createParagraphMock(text: string): { - id: string; - type: string; - data: { text: string }; -} { - return { - id: nanoid(), - type: 'paragraph', - data: { text }, - }; -} \ No newline at end of file diff --git a/test/cypress/tests/api/caret.cy.ts b/test/cypress/tests/api/caret.cy.ts index 53a7d1fc..882bc153 100644 --- a/test/cypress/tests/api/caret.cy.ts +++ b/test/cypress/tests/api/caret.cy.ts @@ -1,249 +1,113 @@ -import { createParagraphMock } from '../../support/utils/createParagraphMock'; import type EditorJS from '../../../../types'; /** * Test cases for Caret API */ describe('Caret API', () => { + const paragraphDataMock = { + id: 'bwnFX5LoX7', + type: 'paragraph', + data: { + text: 'The first block content mock.', + }, + }; + describe('.setToBlock()', () => { - describe('first argument', () => { - const paragraphDataMock = createParagraphMock('The first block content mock.'); + /** + * The arrange part of the following tests are the same: + * - create an editor + * - move caret out of the block by default + */ + beforeEach(() => { + cy.createEditor({ + data: { + blocks: [ + paragraphDataMock, + ], + }, + }).as('editorInstance'); /** - * The arrange part of the following tests are the same: - * - create an editor - * - move caret out of the block by default + * Blur caret from the block before setting via api */ - beforeEach(() => { - cy.createEditor({ - data: { - blocks: [ - paragraphDataMock, - ], - }, - }).as('editorInstance'); - - /** - * Blur caret from the block before setting via api - */ - cy.get('[data-cy=editorjs]') - .click(); - }); - it('should set caret to a block (and return true) if block index is passed as argument', () => { - cy.get('@editorInstance') - .then(async (editor) => { - const returnedValue = editor.caret.setToBlock(0); - - /** - * Check that caret belongs block - */ - cy.window() - .then((window) => { - const selection = window.getSelection(); - const range = selection.getRangeAt(0); - - cy.get('[data-cy=editorjs]') - .find('.ce-block') - .first() - .should(($block) => { - expect($block[0].contains(range.startContainer)).to.be.true; - }); - }); - - expect(returnedValue).to.be.true; - }); - }); - - it('should set caret to a block (and return true) if block id is passed as argument', () => { - cy.get('@editorInstance') - .then(async (editor) => { - const returnedValue = editor.caret.setToBlock(paragraphDataMock.id); - - /** - * Check that caret belongs block - */ - cy.window() - .then((window) => { - const selection = window.getSelection(); - const range = selection.getRangeAt(0); - - cy.get('[data-cy=editorjs]') - .find('.ce-block') - .first() - .should(($block) => { - expect($block[0].contains(range.startContainer)).to.be.true; - }); - }); - - expect(returnedValue).to.be.true; - }); - }); - - it('should set caret to a block (and return true) if Block API is passed as argument', () => { - cy.get('@editorInstance') - .then(async (editor) => { - const block = editor.blocks.getById(paragraphDataMock.id); - const returnedValue = editor.caret.setToBlock(block); - - /** - * Check that caret belongs block - */ - cy.window() - .then((window) => { - const selection = window.getSelection(); - const range = selection.getRangeAt(0); - - cy.get('[data-cy=editorjs]') - .find('.ce-block') - .first() - .should(($block) => { - expect($block[0].contains(range.startContainer)).to.be.true; - }); - }); - - expect(returnedValue).to.be.true; - }); - }); + cy.get('[data-cy=editorjs]') + .click(); }); - describe('offset', () => { - it('should set caret at specific offset in text content', () => { - const paragraphDataMock = createParagraphMock('Plain text content.'); + it('should set caret to a block (and return true) if block index is passed as argument', () => { + cy.get('@editorInstance') + .then(async (editor) => { + const returnedValue = editor.caret.setToBlock(0); - cy.createEditor({ - data: { - blocks: [ - paragraphDataMock, - ], - }, - }).as('editorInstance'); + /** + * Check that caret belongs block + */ + cy.window() + .then((window) => { + const selection = window.getSelection(); + const range = selection.getRangeAt(0); - cy.get('@editorInstance') - .then(async (editor) => { - const block = editor.blocks.getById(paragraphDataMock.id); + cy.get('[data-cy=editorjs]') + .find('.ce-block') + .first() + .should(($block) => { + expect($block[0].contains(range.startContainer)).to.be.true; + }); + }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - editor.caret.setToBlock(block!, 'default', 5); + expect(returnedValue).to.be.true; + }); + }); - cy.window() - .then((window) => { - const selection = window.getSelection(); + it('should set caret to a block (and return true) if block id is passed as argument', () => { + cy.get('@editorInstance') + .then(async (editor) => { + const returnedValue = editor.caret.setToBlock(paragraphDataMock.id); - if (!selection) { - throw new Error('Selection not found'); - } - const range = selection.getRangeAt(0); + /** + * Check that caret belongs block + */ + cy.window() + .then((window) => { + const selection = window.getSelection(); + const range = selection.getRangeAt(0); - expect(range.startOffset).to.equal(5); - }); - }); - }); + cy.get('[data-cy=editorjs]') + .find('.ce-block') + .first() + .should(($block) => { + expect($block[0].contains(range.startContainer)).to.be.true; + }); + }); - it('should set caret at correct offset when text contains HTML elements', () => { - const paragraphDataMock = createParagraphMock('1234567!'); + expect(returnedValue).to.be.true; + }); + }); - cy.createEditor({ - data: { - blocks: [ - paragraphDataMock, - ], - }, - }).as('editorInstance'); + it('should set caret to a block (and return true) if Block API is passed as argument', () => { + cy.get('@editorInstance') + .then(async (editor) => { + const block = editor.blocks.getById(paragraphDataMock.id); + const returnedValue = editor.caret.setToBlock(block); - cy.get('@editorInstance') - .then(async (editor) => { - const block = editor.blocks.getById(paragraphDataMock.id); + /** + * Check that caret belongs block + */ + cy.window() + .then((window) => { + const selection = window.getSelection(); + const range = selection.getRangeAt(0); - // Set caret after "12345" - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - editor.caret.setToBlock(block!, 'default', 6); + cy.get('[data-cy=editorjs]') + .find('.ce-block') + .first() + .should(($block) => { + expect($block[0].contains(range.startContainer)).to.be.true; + }); + }); - cy.window() - .then((window) => { - const selection = window.getSelection(); - - if (!selection) { - throw new Error('Selection not found'); - } - const range = selection.getRangeAt(0); - - expect(range.startContainer.textContent).to.equal('567'); - expect(range.startOffset).to.equal(2); - }); - }); - }); - - it('should handle offset beyond content length', () => { - const paragraphDataMock = createParagraphMock('1234567890'); - - cy.createEditor({ - data: { - blocks: [ - paragraphDataMock, - ], - }, - }).as('editorInstance'); - - cy.get('@editorInstance') - .then(async (editor) => { - const block = editor.blocks.getById(paragraphDataMock.id); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const contentLength = block!.holder.textContent?.length ?? 0; - - // Set caret beyond content length - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - editor.caret.setToBlock(block!, 'default', contentLength + 10); - - cy.window() - .then((window) => { - const selection = window.getSelection(); - - if (!selection) { - throw new Error('Selection not found'); - } - const range = selection.getRangeAt(0); - - // Should be at the end of content - expect(range.startOffset).to.equal(contentLength); - }); - }); - }); - - it('should handle offset in nested HTML structure', () => { - const paragraphDataMock = createParagraphMock('123456789!'); - - cy.createEditor({ - data: { - blocks: [ - paragraphDataMock, - ], - }, - }).as('editorInstance'); - - cy.get('@editorInstance') - .then(async (editor) => { - const block = editor.blocks.getById(paragraphDataMock.id); - - - // Set caret after "8" - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - editor.caret.setToBlock(block!, 'default', 8); - - cy.window() - .then((window) => { - const selection = window.getSelection(); - - if (!selection) { - throw new Error('Selection not found'); - } - const range = selection.getRangeAt(0); - - expect(range.startContainer.textContent).to.equal('789'); - expect(range.startOffset).to.equal(2); - }); - }); - }); + expect(returnedValue).to.be.true; + }); }); }); }); 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/'); - }); }); diff --git a/test/cypress/tests/modules/BlockEvents/Slash.cy.ts b/test/cypress/tests/modules/BlockEvents/Slash.cy.ts index 49fe1fdd..0d9db5fc 100644 --- a/test/cypress/tests/modules/BlockEvents/Slash.cy.ts +++ b/test/cypress/tests/modules/BlockEvents/Slash.cy.ts @@ -92,60 +92,6 @@ describe('Slash keydown', function () { .should('eq', 'Hello/'); }); }); - - describe('pressed outside editor', function () { - it('should not modify any text outside editor when text block is selected', () => { - cy.createEditor({ - data: { - blocks: [ - { - type: 'paragraph', - data: { - text: '', - }, - }, - ], - }, - }); - - cy.document().then((doc) => { - const title = doc.querySelector('h1'); - - if (title) { - title.setAttribute('data-cy', 'page-title'); - } - }); - - // Step 1 - // Click on the plus button and select the text option - cy.get('[data-cy=editorjs]') - .find('.ce-paragraph') - .click(); - cy.get('[data-cy=editorjs]') - .find('.ce-toolbar__plus') - .click({ force: true }); - cy.get('[data-cy="toolbox"] .ce-popover__container') - .contains('Text') - .click(); - - // Step 2 - // Select the 'Editor.js test page' text - cy.get('[data-cy=page-title]') - .invoke('attr', 'contenteditable', 'true') - .click() - .type('{selectall}') - .invoke('removeAttr', 'contenteditable'); - - // Step 3 - // Press the Slash key - cy.get('[data-cy=page-title]') - .trigger('keydown', { key: '/', - code: 'Slash', - which: 191 }); - - cy.get('[data-cy=page-title]').should('have.text', 'Editor.js test page'); - }); - }); }); describe('CMD+Slash keydown', function () { diff --git a/test/cypress/tests/utils/flipper.cy.ts b/test/cypress/tests/utils/flipper.cy.ts index 1fad530d..114a38e1 100644 --- a/test/cypress/tests/utils/flipper.cy.ts +++ b/test/cypress/tests/utils/flipper.cy.ts @@ -46,10 +46,10 @@ class SomePlugin { } describe('Flipper', () => { - const ARROW_DOWN_KEY_CODE = 40; - const ENTER_KEY_CODE = 13; - it('should prevent plugins event handlers from being called while keyboard navigation', () => { + const ARROW_DOWN_KEY_CODE = 40; + const ENTER_KEY_CODE = 13; + const sampleText = 'sample text'; cy.createEditor({ @@ -101,40 +101,4 @@ describe('Flipper', () => { expect(SomePlugin.pluginInternalKeydownHandler).to.have.not.been.called; }); - - it('should not flip when shift key is pressed', () => { - cy.createEditor({ - data: { - blocks: [ - { - type: 'paragraph', - data: { - text: 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.', - }, - }, - ], - }, - autofocus: true, - }); - - cy.get('[data-cy=editorjs]') - .get('.ce-paragraph') - .as('paragraph') - .selectTextByOffset([0, 10]) - .wait(200); - - cy.get('@paragraph') - .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE, - shiftKey: true }); - - // eslint-disable-next-line cypress/require-data-selectors - cy.get('[data-cy="inline-toolbar"]') - .get('.ce-popover--opened') - .as('popover') - .should('exist'); - - cy.get('@popover') - .get('.ce-popover-item--focused') - .should('not.exist'); - }); });