diff --git a/.github/workflows/bump-version-on-merge-next.yml b/.github/workflows/bump-version-on-merge-next.yml
index 2a592e3c..023bf809 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-20.04
+ runs-on: ubuntu-latest
permissions:
actions: write
steps:
diff --git a/.github/workflows/create-a-release-draft.yml b/.github/workflows/create-a-release-draft.yml
index ec728b8a..02ffb1b3 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-20.04
+ runs-on: ubuntu-latest
permissions:
actions: write
steps:
diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml
index 51718006..e9ed6ae2 100644
--- a/.github/workflows/cypress.yml
+++ b/.github/workflows/cypress.yml
@@ -8,12 +8,19 @@ jobs:
matrix:
browser: [firefox, chrome, edge]
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
+ - uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 18
- - uses: actions/checkout@v4
+
+ - name: Setup Firefox
+ if: matrix.browser == 'firefox'
+ uses: browser-actions/setup-firefox@v1
+ with:
+ firefox-version: '115.0esr'
+
- uses: cypress-io/github-action@v6
with:
config: video=false
diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml
index 1db62120..c85e5ca9 100644
--- a/.github/workflows/eslint.yml
+++ b/.github/workflows/eslint.yml
@@ -5,19 +5,21 @@ on: [pull_request]
jobs:
lint:
name: ESlint
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - name: Cache node modules
- uses: actions/cache@v1
+ - uses: actions/setup-node@v3
with:
- path: node_modules
+ node-version: 18
+
+ - name: Cache dependencies
+ uses: actions/cache@v4
+ with:
+ path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
- ${{ runner.OS }}-build-${{ env.cache-name }}-
- ${{ runner.OS }}-build-
- ${{ runner.OS }}-
+ ${{ runner.os }}-node-
- run: yarn
- run: yarn lint
diff --git a/.github/workflows/publish-package-to-npm.yml b/.github/workflows/publish-package-to-npm.yml
index b6ec6935..e867a7ed 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-20.04
+ runs-on: ubuntu-latest
steps:
# Checkout to target branch
- uses: actions/checkout@v4
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 44e1a2ff..00000000
--- a/.gitmodules
+++ /dev/null
@@ -1,48 +0,0 @@
-[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
new file mode 100644
index 00000000..ef33d651
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+v18.20.1
diff --git a/devserver.js b/devserver.js
deleted file mode 100644
index 5087a7b4..00000000
--- a/devserver.js
+++ /dev/null
@@ -1,128 +0,0 @@
-/**
- * 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 33aa0a09..3367bec6 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,5 +1,25 @@
# 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
@@ -11,6 +31,16 @@
- `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
deleted file mode 100644
index 05d71317..00000000
--- a/example/example-dev.html
+++ /dev/null
@@ -1,422 +0,0 @@
-
-
-
-
-
- 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 bc1aaf7a..1c40d370 100644
--- a/example/example-i18n.html
+++ b/example/example-i18n.html
@@ -50,7 +50,6 @@
-
@@ -107,16 +106,11 @@
image: ImageTool,
list: {
- class: List,
+ class: EditorjsList,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+L'
},
- checklist: {
- class: Checklist,
- inlineToolbar: true,
- },
-
quote: {
class: Quote,
inlineToolbar: true,
@@ -198,7 +192,11 @@
},
"popover": {
"Filter": "Поиск",
- "Nothing found": "Ничего не найдено"
+ "Nothing found": "Ничего не найдено",
+ /**
+ * Translation of "Convert To" at the Block Tunes Popover
+ */
+ "Convert to": "Конвертировать в",
}
},
@@ -208,7 +206,8 @@
"toolNames": {
"Text": "Параграф",
"Heading": "Заголовок",
- "List": "Список",
+ "Ordered List": "Нумерованный список",
+ "Unordered List": "Маркированный список",
"Warning": "Примечание",
"Checklist": "Чеклист",
"Quote": "Цитата",
@@ -221,7 +220,7 @@
"Bold": "Полужирный",
"Italic": "Курсив",
"InlineCode": "Моноширинный",
- "Image": "Картинка"
+ "Image": "Картинка",
},
/**
@@ -266,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": "Введите текст"
@@ -274,7 +278,14 @@
"list": {
"Ordered": "Нумерованный",
"Unordered": "Маркированный",
- }
+ "Checklist": "Чеклист",
+ },
+ /**
+ * Translation of "Convert To" at the Inline Toolbar hint
+ */
+ "convertTo": {
+ "Convert to": "Конвертировать в"
+ },
},
/**
@@ -288,14 +299,15 @@
* Also, there are few internal block tunes: "delete", "moveUp" and "moveDown"
*/
"delete": {
- "Delete": "Удалить"
+ "Delete": "Удалить",
+ "Click to delete": "Подтвердить удаление"
},
"moveUp": {
"Move up": "Переместить вверх"
},
"moveDown": {
"Move down": "Переместить вниз"
- }
+ },
},
}
},
diff --git a/example/example-rtl.html b/example/example-rtl.html
index abeab23b..548c4f4e 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,12 +120,6 @@
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
deleted file mode 160000
index 82402cb8..00000000
--- a/example/tools/code
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 82402cb86a327e3c64bef653d953533e2c3613dd
diff --git a/example/tools/delimiter b/example/tools/delimiter
deleted file mode 160000
index 95a5eb90..00000000
--- a/example/tools/delimiter
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 95a5eb90dd2e2e8ab153eb66b59a70cdafdf2d7f
diff --git a/example/tools/embed b/example/tools/embed
deleted file mode 160000
index 801580fb..00000000
--- a/example/tools/embed
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 801580fbdb7ab0ad1e975cfdaab38ada6625e301
diff --git a/example/tools/header b/example/tools/header
deleted file mode 160000
index 3e457cba..00000000
--- a/example/tools/header
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 3e457cbac2c5da53fff1b02b99ddaccaa577f401
diff --git a/example/tools/image b/example/tools/image
deleted file mode 160000
index c8236e57..00000000
--- a/example/tools/image
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c8236e5765294f6b6590573910a68d3826671838
diff --git a/example/tools/inline-code b/example/tools/inline-code
deleted file mode 160000
index 31a086d7..00000000
--- a/example/tools/inline-code
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 31a086d7dc97169de34b9c191735cba7d63562d6
diff --git a/example/tools/link b/example/tools/link
deleted file mode 160000
index aaa69d54..00000000
--- a/example/tools/link
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit aaa69d5408bad34027d6252a3892d40f9fa121be
diff --git a/example/tools/list b/example/tools/list
deleted file mode 160000
index bbc46d55..00000000
--- a/example/tools/list
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit bbc46d557bb5711dd27517272ae2754e1da04697
diff --git a/example/tools/marker b/example/tools/marker
deleted file mode 160000
index 8d6897fc..00000000
--- a/example/tools/marker
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 8d6897fca43e387bcdf4a681380be975fe8f2a07
diff --git a/example/tools/quote b/example/tools/quote
deleted file mode 160000
index 78f70cf2..00000000
--- a/example/tools/quote
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 78f70cf2391cc8aaf2d2e59615de3ad833d180c3
diff --git a/example/tools/raw b/example/tools/raw
deleted file mode 160000
index 84b7d56b..00000000
--- a/example/tools/raw
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 84b7d56b26a66d121edb6682ca205bf995d39034
diff --git a/example/tools/simple-image b/example/tools/simple-image
deleted file mode 160000
index 96388352..00000000
--- a/example/tools/simple-image
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 963883520c7bbe5040366335c9a37bbdc7cf60fd
diff --git a/example/tools/table b/example/tools/table
deleted file mode 160000
index 5a57621c..00000000
--- a/example/tools/table
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 5a57621c4e1abb884fd03e70862cb05b10bfe405
diff --git a/example/tools/text-variant-tune b/example/tools/text-variant-tune
deleted file mode 160000
index 7f51a16d..00000000
--- a/example/tools/text-variant-tune
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 7f51a16d4ab46eff9364f09cea52e09518896d2a
diff --git a/example/tools/warning b/example/tools/warning
deleted file mode 160000
index 0f3ec98b..00000000
--- a/example/tools/warning
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 0f3ec98b9a3b0ea8a9a71cffcb4e596e5c98aecb
diff --git a/package.json b/package.json
index 8081225f..ee5f1499 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
- "version": "2.31.0-rc.7",
+ "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",
@@ -21,16 +21,8 @@
"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",
- "devserver:start": "yarn build && node ./devserver.js"
+ "test:e2e:open": "yarn build:test && cypress open"
},
"author": "CodeX",
"license": "Apache-2.0",
diff --git a/src/components/dom.ts b/src/components/dom.ts
index 24104131..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
*
@@ -587,6 +597,80 @@ 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 516e2b62..5b6e0435 100644
--- a/src/components/flipper.ts
+++ b/src/components/flipper.ts
@@ -199,13 +199,23 @@ export default class Flipper {
*
* @param event - keydown event
*/
- private onKeyDown = (event): void => {
+ private onKeyDown = (event: KeyboardEvent): 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 9b413a56..0bef25c7 100644
--- a/src/components/inline-tools/inline-tool-link.ts
+++ b/src/components/inline-tools/inline-tool-link.ts
@@ -172,11 +172,21 @@ export default class LinkInlineTool implements InlineTool {
* Unlink icon pressed
*/
if (parentAnchor) {
- this.selection.expandToTag(parentAnchor);
- this.unlink();
- this.closeActions();
- this.checkState();
- this.toolbar.close();
+ /**
+ * 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();
+ }
return;
}
@@ -202,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/api/blocks.ts b/src/components/modules/api/blocks.ts
index f9297d5d..9ad22176 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 renderFromHTML(data: string): Promise {
- this.Editor.BlockManager.clear();
+ public async renderFromHTML(data: string): Promise {
+ await 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 c48bba53..40e0973e 100644
--- a/src/components/modules/blockEvents.ts
+++ b/src/components/modules/blockEvents.ts
@@ -237,6 +237,12 @@ 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 48fec049..be8e1e24 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');
}
- block.destroy();
this._blocks.remove(index);
+ block.destroy();
/**
* Force call of didMutated event on Block removal
@@ -894,7 +894,10 @@ export default class BlockManager extends Module {
public async clear(needToAddDefaultBlock = false): Promise {
const queue = new PromiseQueue();
- this.blocks.forEach((block) => {
+ // Create a copy of the blocks array to avoid issues with array modification during iteration
+ const blocksToRemove = [...this.blocks];
+
+ blocksToRemove.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 276eef4b..db8a4f3b 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 text node
+ * @param {number} offset - caret offset regarding to the block content
*/
public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void {
const { BlockManager, BlockSelection } = this.Editor;
@@ -88,23 +88,32 @@ export default class Caret extends Module {
return;
}
- const nodeToSet = $.getDeepestNode(element, position === this.positions.END);
- const contentLength = $.getContentLength(nodeToSet);
+ let nodeToSet: Node;
+ let offsetToSet = offset;
- switch (true) {
- case position === this.positions.START:
- offset = 0;
- break;
- case position === this.positions.END:
- case offset > contentLength:
- offset = contentLength;
- break;
+ 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;
+ }
}
- this.set(nodeToSet as HTMLElement, offset);
+ this.set(nodeToSet as HTMLElement, offsetToSet);
BlockManager.setCurrentBlockByChildNode(block.holder);
- BlockManager.currentBlock.currentInput = element;
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ BlockManager.currentBlock!.currentInput = element;
}
/**
diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts
index 88f861b4..2b1e4035 100644
--- a/src/components/modules/toolbar/blockSettings.ts
+++ b/src/components/modules/toolbar/blockSettings.ts
@@ -68,6 +68,11 @@ 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
*/
@@ -92,6 +97,7 @@ export default class BlockSettings extends Module {
}
this.eventsDispatcher.on(EditorMobileLayoutToggled, this.close);
+ this.hasMobileLayoutToggleListener = true;
}
/**
@@ -100,7 +106,11 @@ export default class BlockSettings extends Module {
public destroy(): void {
this.removeAllNodes();
this.listeners.destroy();
- this.eventsDispatcher.off(EditorMobileLayoutToggled, this.close);
+
+ if (this.hasMobileLayoutToggleListener) {
+ this.eventsDispatcher.off(EditorMobileLayoutToggled, this.close);
+ this.hasMobileLayoutToggleListener = false;
+ }
}
/**
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/src/components/utils/shortcuts.ts b/src/components/utils/shortcuts.ts
index 8cf51ff9..967243d2 100644
--- a/src/components/utils/shortcuts.ts
+++ b/src/components/utils/shortcuts.ts
@@ -86,7 +86,15 @@ class Shortcuts {
const shortcuts = this.registeredShortcuts.get(element);
- this.registeredShortcuts.set(element, shortcuts.filter(el => el !== shortcut));
+ const filteredShortcuts = shortcuts.filter(el => el !== shortcut);
+
+ if (filteredShortcuts.length === 0) {
+ this.registeredShortcuts.delete(element);
+
+ return;
+ }
+
+ this.registeredShortcuts.set(element, filteredShortcuts);
}
/**
diff --git a/test/cypress/support/utils/createParagraphMock.ts b/test/cypress/support/utils/createParagraphMock.ts
new file mode 100644
index 00000000..30166e87
--- /dev/null
+++ b/test/cypress/support/utils/createParagraphMock.ts
@@ -0,0 +1,19 @@
+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 882bc153..53a7d1fc 100644
--- a/test/cypress/tests/api/caret.cy.ts
+++ b/test/cypress/tests/api/caret.cy.ts
@@ -1,113 +1,249 @@
+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()', () => {
- /**
- * 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');
+ describe('first argument', () => {
+ const paragraphDataMock = createParagraphMock('The first block content mock.');
/**
- * Blur caret from the block before setting via api
+ * The arrange part of the following tests are the same:
+ * - create an editor
+ * - move caret out of the block by default
*/
- cy.get('[data-cy=editorjs]')
- .click();
+ 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;
+ });
+ });
});
- 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);
+ describe('offset', () => {
+ it('should set caret at specific offset in text content', () => {
+ const paragraphDataMock = createParagraphMock('Plain text content.');
- /**
- * Check that caret belongs block
- */
- cy.window()
- .then((window) => {
- const selection = window.getSelection();
- const range = selection.getRangeAt(0);
+ cy.createEditor({
+ data: {
+ blocks: [
+ paragraphDataMock,
+ ],
+ },
+ }).as('editorInstance');
- cy.get('[data-cy=editorjs]')
- .find('.ce-block')
- .first()
- .should(($block) => {
- expect($block[0].contains(range.startContainer)).to.be.true;
- });
- });
+ cy.get('@editorInstance')
+ .then(async (editor) => {
+ const block = editor.blocks.getById(paragraphDataMock.id);
- expect(returnedValue).to.be.true;
- });
- });
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ editor.caret.setToBlock(block!, 'default', 5);
- 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);
+ cy.window()
+ .then((window) => {
+ const selection = window.getSelection();
- /**
- * Check that caret belongs block
- */
- cy.window()
- .then((window) => {
- const selection = window.getSelection();
- const range = selection.getRangeAt(0);
+ if (!selection) {
+ throw new Error('Selection not found');
+ }
+ 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(range.startOffset).to.equal(5);
+ });
+ });
+ });
- expect(returnedValue).to.be.true;
- });
- });
+ it('should set caret at correct offset when text contains HTML elements', () => {
+ const paragraphDataMock = createParagraphMock('1234567!');
- 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.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;
- });
- });
+ // Set caret after "12345"
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ editor.caret.setToBlock(block!, 'default', 6);
- expect(returnedValue).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);
+ });
+ });
+ });
});
});
});
diff --git a/test/cypress/tests/inline-tools/link.cy.ts b/test/cypress/tests/inline-tools/link.cy.ts
index 3077e7d8..7d3fa121 100644
--- a/test/cypress/tests/inline-tools/link.cy.ts
+++ b/test/cypress/tests/inline-tools/link.cy.ts
@@ -71,4 +71,171 @@ 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 0d9db5fc..49fe1fdd 100644
--- a/test/cypress/tests/modules/BlockEvents/Slash.cy.ts
+++ b/test/cypress/tests/modules/BlockEvents/Slash.cy.ts
@@ -92,6 +92,60 @@ 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 114a38e1..1fad530d 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', () => {
- it('should prevent plugins event handlers from being called while keyboard navigation', () => {
- const ARROW_DOWN_KEY_CODE = 40;
- const ENTER_KEY_CODE = 13;
+ const ARROW_DOWN_KEY_CODE = 40;
+ const ENTER_KEY_CODE = 13;
+ it('should prevent plugins event handlers from being called while keyboard navigation', () => {
const sampleText = 'sample text';
cy.createEditor({
@@ -101,4 +101,40 @@ 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');
+ });
});