Compare commits

..

No commits in common. "next" and "v2.31.0-rc.7" have entirely different histories.

44 changed files with 801 additions and 778 deletions

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

48
.gitmodules vendored Normal file
View file

@ -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

1
.nvmrc
View file

@ -1 +0,0 @@
v18.20.1

128
devserver.js Normal file
View file

@ -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';
}

View file

@ -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

422
example/example-dev.html Normal file
View file

@ -0,0 +1,422 @@
<!--
Use this page is for debugging purposes.
Editor Tools are loaded as git-submodules.
You can pull modules by running `yarn pull_tools` and start experimenting.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Editor.js 🤩🧦🤨 example</title>
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
<link href="../public/assets/demo.css" rel="stylesheet">
<script src="../public/assets/json-preview.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
<script>
if (localStorage.getItem('theme') === 'dark') {
document.body.classList.add("dark-mode");
}
</script>
<div class="ce-example">
<div class="ce-example__header">
<a class="ce-example__header-logo" href="https://codex.so/editor">Editor.js 🤩🧦🤨</a>
<div class="ce-example__header-menu">
<a href="https://github.com/editor-js" target="_blank">Plugins</a>
<a href="https://editorjs.io/usage" target="_blank">Usage</a>
<a href="https://editorjs.io/configuration" target="_blank">Configuration</a>
<a href="https://editorjs.io/creating-a-block-tool" target="_blank">API</a>
</div>
</div>
<div class="ce-example__content">
<div id="editorjs"></div>
<div id="hint-core" style="text-align: center;">
No core bundle file found. Run <code class="inline-code">yarn build</code>
</div>
<div id="hint-tools" style="text-align: center;">
No submodules found. Run <code class="inline-code">yarn pull_tools && yarn tools:update</code>
</div>
<div class="ce-example__button" id="saveButton">
editor.save()
</div>
<div class="ce-example__statusbar">
<div class="ce-example__statusbar-item">
Readonly:
<b id="readonly-state">
Off
</b>
&nbsp;
<div class="ce-example__statusbar-button" id="toggleReadOnlyButton">
toggle
</div>
</div>
<div class="ce-example__statusbar-item">
<div class="ce-example__statusbar-button" id="showBlocksBoundariesButton">
<span data-toggled-text="Hide">Show</span>
blocks boundaries
</div>
</div>
<div class="ce-example__statusbar-item">
<div class="ce-example__statusbar-button" id="enableThinModeButton">
<span data-toggled-text="Disable">Enable</span>
thin mode
</div>
</div>
<div class="ce-example__statusbar-item ce-example__statusbar-item--right">
<div class="ce-example__statusbar-toggler" id="darkThemeToggler">
</div>
</div>
</div>
</div>
<div class="ce-example__output">
<pre class="ce-example__output-content" id="output"></pre>
<div class="ce-example__output-footer">
<a href="https://codex.so" style="font-weight: bold;">Made by CodeX</a>
</div>
</div>
</div>
<!-- Load Tools -->
<!--
You can upload Tools to your project's directory and use as in example below.
Also you can load each Tool from CDN or use NPM/Yarn packages.
Read more in Tool's README file. For example:
https://github.com/editor-js/header#installation
-->
<script src="./tools/header/dist/header.umd.js" onload="document.getElementById('hint-tools').hidden = true"></script><!-- Header -->
<script src="./tools/simple-image/dist/simple-image.umd.js"></script><!-- Image -->
<script src="./tools/delimiter/dist/delimiter.umd.js"></script><!-- Delimiter -->
<!-- <script src="./tools/list/dist/list.umd.js"></script> List-->
<script src="./tools/nested-list/dist/nested-list.umd.js"></script><!-- Nested List -->
<script src="./tools/checklist/dist/checklist.umd.js"></script><!-- Checklist -->
<script src="./tools/quote/dist/quote.umd.js"></script><!-- Quote -->
<script src="./tools/code/dist/code.umd.js"></script><!-- Code -->
<script src="./tools/embed/dist/embed.umd.js"></script><!-- Embed -->
<script src="./tools/table/dist/table.umd.js"></script><!-- Table -->
<script src="./tools/link/dist/link.umd.js"></script><!-- Link -->
<script src="./tools/raw/dist/raw.umd.js"></script><!-- Raw -->
<script src="./tools/warning/dist/warning.umd.js"></script><!-- Warning -->
<script src="./tools/marker/dist/marker.umd.js"></script><!-- Marker -->
<script src="./tools/inline-code/dist/inline-code.umd.js"></script><!-- Inline Code -->
<!-- Load Editor.js's Core -->
<script src="../dist/editorjs.umd.js" onload="document.getElementById('hint-core').hidden = true;"></script>
<!-- Initialization -->
<script>
/**
* Editor init config
* @see https://editorjs.io/configuration
*/
const editorConfig = {
/**
* Enable/Disable the read only mode
*/
readOnly: false,
/**
* Wrapper of Editor
*/
holder: 'editorjs',
/**
* Common Inline Toolbar settings
* - if true (or not specified), the order from 'tool' property will be used
* - if an array of tool names, this order will be used
*/
// inlineToolbar: ['link', 'marker', 'bold', 'italic'],
// inlineToolbar: true,
/**
* Tools list
*/
tools: {
/**
* Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
*/
header: {
class: Header,
inlineToolbar: ['marker', 'link'],
config: {
placeholder: 'Header'
},
shortcut: 'CMD+SHIFT+H'
},
/**
* Or pass class directly without any configuration
*/
image: SimpleImage,
list: {
class: NestedList,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+L'
},
checklist: {
class: Checklist,
inlineToolbar: true,
},
quote: {
class: Quote,
inlineToolbar: true,
config: {
quotePlaceholder: 'Enter a quote',
captionPlaceholder: 'Quote\'s author',
},
shortcut: 'CMD+SHIFT+O'
},
warning: Warning,
marker: {
class: Marker,
shortcut: 'CMD+SHIFT+M'
},
code: {
class: CodeTool,
shortcut: 'CMD+SHIFT+C'
},
delimiter: Delimiter,
inlineCode: {
class: InlineCode,
shortcut: 'CMD+SHIFT+C'
},
linkTool: LinkTool,
raw: RawTool,
embed: Embed,
table: {
class: Table,
inlineToolbar: true,
shortcut: 'CMD+ALT+T'
},
},
/**
* This Tool will be used as default
*/
// defaultBlock: 'paragraph',
/**
* Initial Editor data
*/
data: {
blocks: [
{
id: "zcKCF1S7X8",
type: "header",
data: {
text: "Editor.js",
level: 1
}
},
{
"id": "b6ji-DvaKb",
"type": "paragraph",
"data": {
"text": "Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration."
}
},
{
type: "header",
id: "7ItVl5biRo",
data: {
text: "Key features",
level: 2
}
},
{
type : 'list',
id: "SSBSguGvP7",
data : {
items : [
{
content: 'It is a block-styled editor',
items: []
},
{
content: 'It returns clean data output in JSON',
items: []
},
{
content: 'Designed to be extendable and pluggable with a simple API',
items: []
}
],
style: 'unordered'
}
},
{
type: "header",
id: "QZFox1m_ul",
data: {
text: "What does it mean «block-styled editor»",
level: 2
}
},
{
type : 'paragraph',
id: "bwnFX5LoX7",
data : {
text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js <mark class=\"cdx-marker\">workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc</mark>. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.'
}
},
{
type : 'paragraph',
id: "mTrPOHAQTe",
data : {
text : `There are dozens of <a href="https://github.com/editor-js">ready-to-use Blocks</a> and the <a href="https://editorjs.io/creating-a-block-tool">simple API</a> for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.`
}
},
{
type: "header",
id: "1sYMhUrznu",
data: {
text: "What does it mean clean data output",
level: 2
}
},
{
type : 'paragraph',
id: "jpd7WEXrJG",
data : {
text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below'
}
},
{
type : 'paragraph',
id: "0lOGNUKxqt",
data : {
text : `Given data can be used as you want: render with HTML for <code class="inline-code">Web clients</code>, render natively for <code class="inline-code">mobile apps</code>, create markup for <code class="inline-code">Facebook Instant Articles</code> or <code class="inline-code">Google AMP</code>, generate an <code class="inline-code">audio version</code> and so on.`
}
},
{
type : 'paragraph',
id: "WvX7kBjp0I",
data : {
text : 'Clean data is useful to sanitize, validate and process on the backend.'
}
},
{
type : 'delimiter',
id: "H9LWKQ3NYd",
data : {}
},
{
type : 'paragraph',
id: "h298akk2Ad",
data : {
text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏'
}
},
{
type: 'image',
id: "9802bjaAA2",
data: {
url: 'assets/codex2x.png',
caption: '',
stretched: false,
withBorder: true,
withBackground: false,
}
},
]
},
onReady: function(){
saveButton.click();
},
onChange: function(api, event) {
console.log('something changed', event);
},
}
/**
* To initialize the Editor, create a new instance with configuration object
* @see docs/installation.md for mode details
*/
var editor = new EditorJS(editorConfig);
/**
* Saving button
*/
const saveButton = document.getElementById('saveButton');
/**
* Toggle read-only button
*/
const toggleReadOnlyButton = document.getElementById('toggleReadOnlyButton');
const readOnlyIndicator = document.getElementById('readonly-state');
/**
* Saving example
*/
saveButton.addEventListener('click', function () {
editor.save()
.then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
})
.catch((error) => {
console.error('Saving error', error);
});
});
/**
* Toggle read-only example
*/
toggleReadOnlyButton.addEventListener('click', async () => {
const readOnlyState = await editor.readOnly.toggle();
readOnlyIndicator.textContent = readOnlyState ? 'On' : 'Off';
});
/**
* Button for displaying blocks borders. Useful for UI development
*/
const showBlocksBoundariesButton = document.getElementById("showBlocksBoundariesButton");
showBlocksBoundariesButton.addEventListener('click', () => {
document.body.classList.toggle("show-block-boundaries")
})
/**
* Button for enabling the 'Thin' mode
*/
const enableThinModeButton = document.getElementById("enableThinModeButton");
enableThinModeButton.addEventListener('click', () => {
document.body.classList.toggle("thin-mode")
editor.destroy();
editor = new EditorJS(editorConfig);
})
/**
* Toggler for toggling the dark mode
*/
const darkThemeToggler = document.getElementById("darkThemeToggler");
darkThemeToggler.addEventListener('click', () => {
document.body.classList.toggle("dark-mode");
localStorage.setItem('theme', document.body.classList.contains("dark-mode") ? 'dark' : 'default');
})
</script>
</body>
</html>

View file

@ -50,6 +50,7 @@
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@latest"></script><!-- Image -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script><!-- List -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script><!-- Checklist -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed -->
@ -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": "Переместить вниз"
},
}
},
}
},

View file

@ -53,21 +53,21 @@
Read more in Tool's README file. For example:
https://github.com/editor-js/header#installation
-->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/nested-list@latest"></script><!-- List -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script><!-- Checklist -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@latest"></script><!-- Table -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/link@latest"></script><!-- Link -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/warning@latest"></script><!-- Warning -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/raw@latest"></script><!-- Raw -->
<script src="./tools/header/dist/bundle.js" onload="document.getElementById('hint-tools').hidden = true"></script><!-- Header -->
<script src="./tools/simple-image/dist/bundle.js"></script><!-- Image -->
<script src="./tools/delimiter/dist/bundle.js"></script><!-- Delimiter -->
<script src="./tools/list/dist/bundle.js"></script><!-- List -->
<script src="./tools/checklist/dist/bundle.js"></script><!-- Checklist -->
<script src="./tools/quote/dist/bundle.js"></script><!-- Quote -->
<script src="./tools/code/dist/bundle.js"></script><!-- Code -->
<script src="./tools/embed/dist/bundle.js"></script><!-- Embed -->
<script src="./tools/table/dist/table.js"></script><!-- Table -->
<script src="./tools/link/dist/bundle.js"></script><!-- Link -->
<script src="./tools/raw/dist/bundle.js"></script><!-- Raw -->
<script src="./tools/warning/dist/bundle.js"></script><!-- Warning -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@latest"></script><!-- Marker -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest"></script><!-- Inline Code -->
<script src="./tools/marker/dist/bundle.js"></script><!-- Marker -->
<script src="./tools/inline-code/dist/bundle.js"></script><!-- Inline Code -->
<!-- Load Editor.js's Core -->
<script src="../dist/editorjs.umd.js" onload="document.getElementById('hint-core').hidden = true"></script>
@ -120,6 +120,12 @@
inlineToolbar: ['link'],
},
list: {
class: List,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+L'
},
checklist: {
class: Checklist,
inlineToolbar: true,

1
example/tools/code Submodule

@ -0,0 +1 @@
Subproject commit 82402cb86a327e3c64bef653d953533e2c3613dd

@ -0,0 +1 @@
Subproject commit 95a5eb90dd2e2e8ab153eb66b59a70cdafdf2d7f

1
example/tools/embed Submodule

@ -0,0 +1 @@
Subproject commit 801580fbdb7ab0ad1e975cfdaab38ada6625e301

1
example/tools/header Submodule

@ -0,0 +1 @@
Subproject commit 3e457cbac2c5da53fff1b02b99ddaccaa577f401

1
example/tools/image Submodule

@ -0,0 +1 @@
Subproject commit c8236e5765294f6b6590573910a68d3826671838

@ -0,0 +1 @@
Subproject commit 31a086d7dc97169de34b9c191735cba7d63562d6

1
example/tools/link Submodule

@ -0,0 +1 @@
Subproject commit aaa69d5408bad34027d6252a3892d40f9fa121be

1
example/tools/list Submodule

@ -0,0 +1 @@
Subproject commit bbc46d557bb5711dd27517272ae2754e1da04697

1
example/tools/marker Submodule

@ -0,0 +1 @@
Subproject commit 8d6897fca43e387bcdf4a681380be975fe8f2a07

1
example/tools/quote Submodule

@ -0,0 +1 @@
Subproject commit 78f70cf2391cc8aaf2d2e59615de3ad833d180c3

1
example/tools/raw Submodule

@ -0,0 +1 @@
Subproject commit 84b7d56b26a66d121edb6682ca205bf995d39034

@ -0,0 +1 @@
Subproject commit 963883520c7bbe5040366335c9a37bbdc7cf60fd

1
example/tools/table Submodule

@ -0,0 +1 @@
Subproject commit 5a57621c4e1abb884fd03e70862cb05b10bfe405

@ -0,0 +1 @@
Subproject commit 7f51a16d4ab46eff9364f09cea52e09518896d2a

1
example/tools/warning Submodule

@ -0,0 +1 @@
Subproject commit 0f3ec98b9a3b0ea8a9a71cffcb4e596e5c98aecb

View file

@ -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",

View file

@ -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 (<span></span>)
* - 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,
};
}
}
/**

View file

@ -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)

View file

@ -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 {

View file

@ -224,8 +224,8 @@ export default class BlocksAPI extends Module {
* @param {string} data - HTML string to render
* @returns {Promise<void>}
*/
public async renderFromHTML(data: string): Promise<void> {
await this.Editor.BlockManager.clear();
public renderFromHTML(data: string): Promise<void> {
this.Editor.BlockManager.clear();
return this.Editor.Paste.processText(data, true);
}

View file

@ -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;

View file

@ -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<void> {
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);
});

View file

@ -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;
}
/**

View file

@ -68,11 +68,6 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
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<BlockSettingsNodes> {
}
this.eventsDispatcher.on(EditorMobileLayoutToggled, this.close);
this.hasMobileLayoutToggleListener = true;
}
/**
@ -106,11 +100,7 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
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);
}
/**

View file

@ -773,13 +773,12 @@ export default class UI extends Module<UINodes> {
*/
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);

View file

@ -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);
}
/**

View file

@ -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));
}
/**

View file

@ -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 },
};
}

View file

@ -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<EditorJS>('@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<EditorJS>('@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<EditorJS>('@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<EditorJS>('@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<EditorJS>('@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<EditorJS>('@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('1234<b>567</b>!');
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<EditorJS>('@editorInstance')
.then(async (editor) => {
const block = editor.blocks.getById(paragraphDataMock.id);
const returnedValue = editor.caret.setToBlock(block);
cy.get<EditorJS>('@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<EditorJS>('@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('123<b>456<i>789</i></b>!');
cy.createEditor({
data: {
blocks: [
paragraphDataMock,
],
},
}).as('editorInstance');
cy.get<EditorJS>('@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;
});
});
});
});

View file

@ -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/');
});
});

View file

@ -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 () {

View file

@ -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');
});
});