Merge branch 'next' of github.com:codex-team/editor.js into feature/clean-inline-fragments

This commit is contained in:
George Berezhnoy 2022-05-01 23:12:23 +01:00
commit aacc10f8cb
53 changed files with 2015 additions and 466 deletions

View file

@ -50,6 +50,7 @@
"DOMRect": true,
"ClientRect": true,
"ArrayLike": true,
"InputEvent": true,
"unknown": true
}
}

3
.gitmodules vendored
View file

@ -52,3 +52,6 @@
[submodule "example/tools/nested-list"]
path = example/tools/nested-list
url = https://github.com/editor-js/nested-list
[submodule "example/tools/text-variant-tune"]
path = example/tools/text-variant-tune
url = https://github.com/editor-js/text-variant-tune

View file

@ -19,3 +19,4 @@ tsconfig.json
tslint.json
webpack.config.js
yarn.lock
devserver.js

View file

@ -1 +1,2 @@
* @neSpecc @gohabereg @khaydarov
* @neSpecc @gohabereg @TatianaFomina @ilyamore88

View file

@ -8,9 +8,38 @@
[![](https://img.shields.io/npm/l/@editorjs/editorjs?style=flat-square)](https://www.npmjs.com/package/@editorjs/editorjs)
[![Join the chat at https://gitter.im/codex-team/editor.js](https://badges.gitter.im/codex-team/editor.js.svg)](https://gitter.im/codex-team/editor.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari-ios/safari-ios_48x48.png" alt="iOS Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>iOS Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| | | | | | |
| --------- | --------- | --------- | --------- | --------- | --------- |
| Edge 12+ | Firefox 18+ | Chrome 49+ | Safari 10+ | Safari 10+ | Opera 36+
| <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="16px" height="16px" /> Edge 12+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="16px" height="16px" /> Firefox 18+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="16px" height="16px" /> Chrome 49+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="16px" height="16px" /> Safari 10+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari-ios/safari-ios_48x48.png" alt="iOS Safari" width="16px" height="16px" /> iOS Safari 10+ | <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="16px" height="16px" /> Opera 36+
## Roadmap
<img align="right" width="342" src="https://user-images.githubusercontent.com/3684889/152149321-3f9c585d-7d87-4dad-ab14-def0d526e71c.png" hspace="50">
- Collaborative editing
- [ ] Implement Inline Tools JSON format `In progress` [#1801](https://github.com/codex-team/editor.js/pull/1801)
- [ ] Implement Operations creation and transformations
- [ ] Implement Tools API changes
- [ ] Implement Server and communication
- [ ] Update basic tools to fit the new API
- Unified Toolbox
- [x] Block Tunes moved left [#1815](https://github.com/codex-team/editor.js/pull/1815)
- [x] Toolbox become vertical [#2014](https://github.com/codex-team/editor.js/pull/2014)
- [ ] Ability to display several Toolbox buttons by the single Tool `In progress`
- [ ] Conversion Toolbar uses Unified Toolbox
- [ ] Block Tunes become vertical
- [ ] Conversion Toolbar added to the Block Tunes
- Ecosystem improvements
<a href="https://opencollective.com/editorjs/donate" target="_blank">
<img width="432" alt="image" src="https://user-images.githubusercontent.com/3684889/166146666-e6f5fcc3-bccf-43b0-b48c-e8f1b25b2896.png">
</a>
##
## If you like a project 💗💗💗
@ -27,7 +56,6 @@ Support us by becoming a sponsor. Your logo will show up here with a link to you
<a href="https://slid.cc/" target="_blank"><img src="https://images.opencollective.com/slid_team/ff564d7/logo/256.png" width="64"></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/editorjs/contribute/backer-8632/checkout)]

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,5 +1,35 @@
# Changelog
### 2.24.2
- `Fix` — Scrolling issue when opening toolbox on mobile fixed
- `Fix` — Typo in toolbox empty placeholder fixed
- `Fix` — The issue with scroll jumping on block hovering have fixed [2036](https://github.com/codex-team/editor.js/issues/2036)
- `Improvement`*Dev Example Page* - Add popup example page
- `Improvement`*UI* - The Toolbox will restore the internal scroll on every opening
### 2.24.1
`Fix` — The I18n of Tools` titles at the Toolbox now works correctly [#2030](https://github.com/codex-team/editor.js/issues/2030)
### 2.24.0
- `New`*UI* — The Toolbox became vertical 🥳
- `Improvement`*UI* — the Plus button will always be shown (previously, it appears only for empty blocks)
- `Improvement`*Dev Example Page* - Server added to allow opening example page on other devices in network.
- `Fix``UI` — the Toolbar won't move on hover at mobile viewports. Resolves [#1972](https://github.com/codex-team/editor.js/issues/1972)
- `Fix``OnChange` event invocation after block insertion. [#1997](https://github.com/codex-team/editor.js/issues/1997)
- `Fix``ReadOnly` — the `readonly.isEnabled` API getter now works correctly after `readonly.toggle()` calling. Resolves [#1822](https://github.com/codex-team/editor.js/issues/1822)
- `Fix``Paste` — the inline HTML tags now will be preserved on pasting. [#1686](https://github.com/codex-team/editor.js/pull/1686)
### 2.23.2
`Fix` — Crash on initialization in the read-only mode [#1968](https://github.com/codex-team/editor.js/issues/1968)
### 2.23.1
`Fix` — Incorrect release tag fixed
### 2.23.0
- `Improvement`*EditorConfig* — The `onChange` callback now accepts two arguments: EditorJS API and the CustomEvent with `type` and `detail` allowing to determine what happened with a Block
@ -21,6 +51,7 @@
- `Refactoring` — Toolbox became a standalone class instead of a Module. It can be accessed only through the Toolbar module.
- `Refactoring` — CI flow optimized.
- `Fix` - Recognize async `onPaste` handlers in tools [#1803](https://github.com/codex-team/editor.js/issues/1803).
- `Fix` — Fire onChange event for native inputs [#1750](https://github.com/codex-team/editor.js/issues/1750)
### 2.22.3

View file

@ -270,6 +270,56 @@ body {
padding: 30px;
}
/**
* Styles for the popup example page
*/
.ce-example--popup {
height: 100vh;
display: flex;
flex-direction: column;
}
.ce-example--popup .ce-example__content {
flex-grow: 2;
}
.ce-example-popup__overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #00000085;
}
.ce-example-popup__popup {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
width: 800px;
max-width: 100%;
max-height: 90vh;
background: white;
padding: 20px;
border-radius: 8px;
overflow: auto;
box-sizing: border-box;
}
@media all and (max-width: 730px){
.ce-example-popup__popup {
top: 10px;
left: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
transform: none;
max-height: none;
}
}
.show-block-boundaries .ce-block {
box-shadow: inset 0 0 0 1px #eff2f5;
}

View file

@ -98,7 +98,7 @@
<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/bundle.js"></script><!-- Table -->
<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 -->

View file

@ -193,7 +193,9 @@
},
"toolbar": {
"toolbox": {
"Add": "Добавить"
"Add": "Добавить",
"Filter": "Поиск",
"Nothing found": "Ничего не найдено"
}
}
},

131
example/example-popup.html Normal file
View file

@ -0,0 +1,131 @@
<!--
Use this page for debugging purposes.
This page can be used for testing editor nested in a popup.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Editor.js 🤩🧦🤨 example: Popup</title>
<link href="assets/demo.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
<div class="ce-example ce-example--popup">
<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 ce-example__content--with-bg _ce-example__content--small">
<div id="hint-core" style="text-align: center; padding-top: 20px">
No core bundle file found. Run <code class="inline-code">yarn build</code>
</div>
<div class="stub">
<h1>Base concepts</h1>
<p>
Editor.js is a block-style editor for rich media stories. It outputs clean data in JSON instead of heavy HTML markup. And more important thing is that Editor.js is designed to be API extendable and pluggable.
</p>
<p>
So there are a few key features:
</p>
<ul>
<li>Clean data output</li>
<li>API pluggable</li>
<li>Open source</li>
</ul>
<h2>
What does it mean block-styled
</h2>
<p>
In other editors, the workspace is provided by single contenteditable element in where you can create different HTML markup. All of us saw permanent bugs with moving text fragments or scaling images, while page parts are jumping and twitches. Or highlighting big parts of the text in the case when you just want to make few words to be a heading or bold.
</p>
<p>
The 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.
</p>
<p>
At the same time, most useful features as arrow-navigation, copy & paste, cross block selection, and others works almost as in the familiar editors.
</p>
<h2>
What is clean data
</h2>
<p>
But the more interesting thing is, as mentioned above, that Editor.js returns clean data instead of HTML-markup. Take a look at the example.
</p>
<p>
If our entry consists of few paragraphs and a heading, in popular Medium editor after saving we will have something like this:
</p>
<p>
As you can see, there are only data we need: a list of structural Blocks with their content description.
</p>
<p>
You can use this data to easily render in Web, native mobile/desktop application, pass to Audio Readers, create templates for Facebook Instant Articles, AMP, RSS, create chat-bots, and many others.
</p>
<p>
Also, the clean data can be useful for backend processing: sanitizing, validation, injecting an advertising or other stuff, extracting Headings, make covers for social networks from Image Blocks, and other.
</p>
<h2>
API pluggable?
</h2>
<p>
A key value of the Editor is the API. All main functional units of the editor — Blocks, Inline Formatting Tools, Block Tunes — are provided by external plugins that use Editor's API.
</p>
<p>
We decide to extract all these Tools to separate scripts to make Editor's Core more abstract and make API more powerful. Any challenges and tasks you are facing can be implemented by your own plugins using the API.
</p>
<p>
At the same time, API is created to be easy-to-understand and simple-to-use.
</p>
<h2>
Open Source, so?
</h2>
<p>
Editor.js is more than just an editor. It is a big open-source community of developers and contributors. Anyone can suggest an improvement or a bug fix. Anyone can create new cool API features and plugins.
</p>
<p>
We will support each developer of Editor.js plugins: the best solutions will be collected to the Awesome List and promoted to the community. Together we can create a big suite of different Blocks, Inline Tools, Block Tunes that can hit a wide specter of tasks.
</p>
<p>
Thanks for your interest. Hope you enjoy Editor.js.
</p>
</div>
<div class="ce-example-popup">
<div class="ce-example-popup__overlay"></div>
<div class="ce-example-popup__popup">
<div id="editorjs"></div>
</div>
</div>
</div>
<div class="ce-example__output">
<div class="ce-example__output-footer">
<a href="https://codex.so" style="font-weight: bold;">Made by CodeX</a>
</div>
</div>
</div>
<!-- Load Editor.js's Core -->
<script src="../dist/editor.js" onload="document.getElementById('hint-core').hidden = true"></script>
<script src="./tools/header/dist/bundle.js"></script><!-- Header -->
<!-- Initialization -->
<script>
var editor1 = new EditorJS({
holder: 'editorjs',
tools: {
header: {
class: Header,
shortcut: 'CMD+SHIFT+H'
}
}
});
</script>
</body>
</html>

View file

@ -61,7 +61,7 @@
<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/bundle.js"></script><!-- Table -->
<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 -->

@ -1 +1 @@
Subproject commit b3c87948d5a5926f2557c2029d64aa819ca61920
Subproject commit 35742f01ae5875d442b145121d3c9b71b23aea56

@ -1 +1 @@
Subproject commit d452b46106766b5fc4c5747e99888f613a3764fd
Subproject commit 0fc365ef256decb8f765fb72b060d5bef9254aa3

@ -1 +1 @@
Subproject commit 004a06458d3881e8d922506381c21847900e3c62
Subproject commit 58b5dc7072ad92a048ebe9488d0ca8e7bfa069d2

@ -1 +1 @@
Subproject commit 8375ae17756fa2677d57e716e12096437d01e8f8
Subproject commit 9add95389afca0711c05260a92283fae8eb209eb

@ -1 +1 @@
Subproject commit 84cc4f393db0939c6246c9a579377f2540dac289
Subproject commit 4f8010dd640d847778220d9b1947fa8235b17f87

@ -1 +1 @@
Subproject commit ddbc1147e127a727cfac4dbe0326b78e02dceb9e
Subproject commit ad0d9012d149e3ca4b41a5ce096b31767cc8c1fd

@ -0,0 +1 @@
Subproject commit 02538b1daea722c854cc61b6fbca01b746c21717

View file

@ -1,6 +1,6 @@
{
"name": "@editorjs/editorjs",
"version": "2.23.0-rc.1",
"version": "2.24.2",
"description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js",
"types": "./types/index.d.ts",
@ -24,14 +24,15 @@
"svg": "svg-sprite-generate -d src/assets/ -o dist/sprite.svg",
"ci:pull_paragraph": "git submodule update --init ./src/tools/paragraph",
"pull_tools": "git submodule update --init --recursive",
"_tools:checkout": "git submodule foreach 'git checkout master || git checkout main'",
"_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 && cypress run",
"test:e2e:open": "yarn build && cypress open"
"test:e2e:open": "yarn build && cypress open",
"devserver:start": "yarn build && node ./devserver.js"
},
"author": "CodeX",
"license": "Apache-2.0",
@ -50,6 +51,8 @@
"@codexteam/shortcuts": "^1.1.1",
"@cypress/code-coverage": "^3.9.2",
"@cypress/webpack-preprocessor": "^5.6.0",
"@editorjs/code": "^2.7.0",
"@editorjs/delimiter": "^1.2.0",
"@editorjs/header": "^2.6.1",
"@editorjs/simple-image": "^1.4.1",
"@types/node": "^14.14.35",
@ -96,7 +99,7 @@
},
"dependencies": {
"codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.4",
"codex-tooltip": "^1.0.5",
"nanoid": "^3.1.22"
}
}

3
src/assets/search.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M12.711 2.18a7.448 7.448 0 0 1 .79 9.603l2.143 2.144a1.214 1.214 0 1 1-1.717 1.717L11.783 13.5a7.446 7.446 0 1 1 .928-11.32ZM11.39 3.61a5.5 5.5 0 1 0-7.778 7.78 5.5 5.5 0 0 0 7.778-7.78Z" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -52,7 +52,7 @@ interface BlockConstructorOptions {
/**
* Tunes data for current Block
*/
tunesData: {[name: string]: BlockTuneData};
tunesData: { [name: string]: BlockTuneData };
}
/**
@ -98,7 +98,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
*
* @returns {{wrapper: string, content: string}}
*/
public static get CSS(): {[name: string]: string} {
public static get CSS(): { [name: string]: string } {
return {
wrapper: 'ce-block',
wrapperStretched: 'ce-block--stretched',
@ -170,7 +170,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
* If there is saved data for Tune which is not available at the moment,
* we will store it here and provide back on save so data is not lost
*/
private unavailableTunesData: {[name: string]: BlockTuneData} = {};
private unavailableTunesData: { [name: string]: BlockTuneData } = {};
/**
* Editor`s API module
@ -201,11 +201,15 @@ export default class Block extends EventsDispatcher<BlockEvents> {
/**
* Is fired when DOM mutation has been happened
*/
private didMutated = _.debounce((mutations: MutationRecord[] = []): void => {
const shouldFireUpdate = !mutations.some(({ addedNodes = [], removedNodes }) => {
return [...Array.from(addedNodes), ...Array.from(removedNodes)]
.some(node => $.isElement(node) && (node as HTMLElement).dataset.mutationFree === 'true');
});
private didMutated = _.debounce((mutationsOrInputEvent: MutationRecord[] | InputEvent = []): void => {
const shouldFireUpdate = mutationsOrInputEvent instanceof InputEvent ||
!mutationsOrInputEvent.some(({
addedNodes = [],
removedNodes,
}) => {
return [...Array.from(addedNodes), ...Array.from(removedNodes)]
.some(node => $.isElement(node) && (node as HTMLElement).dataset.mutationFree === 'true');
});
/**
* In case some mutation free elements are added or removed, do not trigger didMutated event
@ -575,9 +579,9 @@ export default class Block extends EventsDispatcher<BlockEvents> {
*
* @returns {object}
*/
public async save(): Promise<void|SavedData> {
public async save(): Promise<void | SavedData> {
const extractedBlock = await this.toolInstance.save(this.pluginsContent as HTMLElement);
const tunesData: {[name: string]: BlockTuneData} = this.unavailableTunesData;
const tunesData: { [name: string]: BlockTuneData } = this.unavailableTunesData;
[
...this.tunesInstances.entries(),
@ -706,7 +710,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
* Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback
* Can be useful for block changes invisible for editor core.
*/
public dispatchChange(): void{
public dispatchChange(): void {
this.didMutated();
}
@ -775,7 +779,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
* @param tunesData - current Block tunes data
* @private
*/
private composeTunes(tunesData: {[name: string]: BlockTuneData}): void {
private composeTunes(tunesData: { [name: string]: BlockTuneData }): void {
Array.from(this.tunes.values()).forEach((tune) => {
const collection = tune.isInternal ? this.defaultTunesInstances : this.tunesInstances;

View file

@ -17,21 +17,23 @@ export interface FlipperOptions {
*/
items?: HTMLElement[];
/**
* Defines arrows usage. By default Flipper leafs items also via RIGHT/LEFT.
*
* true by default
*
* Pass 'false' if you don't need this behaviour
* (for example, Inline Toolbar should be closed by arrows,
* because it means caret moving with selection clearing)
*/
allowArrows?: boolean;
/**
* Optional callback for button click
*/
activateCallback?: (item: HTMLElement) => void;
/**
* List of keys allowed for handling.
* Can include codes of the following keys:
* - Tab
* - Enter
* - Arrow up
* - Arrow down
* - Arrow right
* - Arrow left
* If not specified all keys are enabled
*/
allowedKeys?: number[];
}
/**
@ -53,11 +55,9 @@ export default class Flipper {
private activated = false;
/**
* Flag that allows arrows usage to flip items
*
* @type {boolean}
* List codes of the keys allowed for handling
*/
private readonly allowArrows: boolean = true;
private readonly allowedKeys: number[];
/**
* Call back for button click/enter
@ -68,9 +68,9 @@ export default class Flipper {
* @param {FlipperOptions} options - different constructing settings
*/
constructor(options: FlipperOptions) {
this.allowArrows = _.isBoolean(options.allowArrows) ? options.allowArrows : true;
this.iterator = new DomIterator(options.items, options.focusedItemClass);
this.activateCallback = options.activateCallback;
this.allowedKeys = options.allowedKeys || Flipper.usedKeys;
}
/**
@ -120,15 +120,6 @@ export default class Flipper {
document.removeEventListener('keydown', this.onKeyDown);
}
/**
* Return current focused button
*
* @returns {HTMLElement|null}
*/
public get currentItem(): HTMLElement|null {
return this.iterator.currentItem;
}
/**
* Focus first item
*/
@ -142,6 +133,7 @@ export default class Flipper {
*/
public flipLeft(): void {
this.iterator.previous();
this.flipCallback();
}
/**
@ -149,6 +141,14 @@ export default class Flipper {
*/
public flipRight(): void {
this.iterator.next();
this.flipCallback();
}
/**
* Return true if some button is focused
*/
public hasFocus(): boolean {
return !!this.iterator.currentItem;
}
/**
@ -206,23 +206,7 @@ export default class Flipper {
* @returns {boolean}
*/
private isEventReadyForHandling(event: KeyboardEvent): boolean {
const handlingKeyCodeList = [
_.keyCodes.TAB,
_.keyCodes.ENTER,
];
const isCurrentItemIsFocusedInput = this.iterator.currentItem == document.activeElement;
if (this.allowArrows && !isCurrentItemIsFocusedInput) {
handlingKeyCodeList.push(
_.keyCodes.LEFT,
_.keyCodes.RIGHT,
_.keyCodes.UP,
_.keyCodes.DOWN
);
}
return this.activated && handlingKeyCodeList.indexOf(event.keyCode) !== -1;
return this.activated && this.allowedKeys.includes(event.keyCode);
}
/**
@ -266,4 +250,13 @@ export default class Flipper {
event.preventDefault();
event.stopPropagation();
}
/**
* Fired after flipping in any direction
*/
private flipCallback(): void {
if (this.iterator.currentItem) {
this.iterator.currentItem.scrollIntoViewIfNeeded();
}
}
}

View file

@ -13,7 +13,9 @@
},
"toolbar": {
"toolbox": {
"Add": ""
"Add": "",
"Filter": "",
"Nothing found": ""
}
}
},

View file

@ -10,9 +10,14 @@ export default class ReadOnlyAPI extends Module {
* Available methods
*/
public get methods(): ReadOnly {
const getIsEnabled = (): boolean => this.isEnabled;
// eslint-disable-next-line @typescript-eslint/no-this-alias
return {
toggle: (state): Promise<boolean> => this.toggle(state),
isEnabled: this.isEnabled,
get isEnabled(): boolean {
return getIsEnabled();
},
};
}

View file

@ -662,7 +662,7 @@ export default class Paste extends Module {
/** If there is no pattern substitute - insert string as it is */
if (BlockManager.currentBlock && BlockManager.currentBlock.currentInput) {
const currentToolSanitizeConfig = BlockManager.currentBlock.tool.sanitizeConfig;
const currentToolSanitizeConfig = BlockManager.currentBlock.tool.baseSanitizeConfig;
document.execCommand(
'insertHTML',

View file

@ -376,7 +376,6 @@ export default class RectangleSelection extends Module {
this.inverseSelection();
SelectionUtils.get().removeAllRanges();
event.preventDefault();
}
/**

View file

@ -29,16 +29,11 @@ export default class Saver extends Module {
* @returns {OutputData}
*/
public async save(): Promise<OutputData> {
const { BlockManager, Tools, ModificationsObserver } = this.Editor;
const { BlockManager, Tools } = this.Editor;
const blocks = BlockManager.blocks,
chainData = [];
try {
/**
* Disable onChange callback on save to not to spam those events
*/
ModificationsObserver.disable();
blocks.forEach((block: Block) => {
chainData.push(this.getSavedData(block));
});
@ -65,8 +60,6 @@ export default class Saver extends Module {
return this.makeOutput(withFragments);
} catch (e) {
_.logLabeled(`Saving failed due to the Error %o`, 'error', e);
} finally {
ModificationsObserver.enable();
}
}

View file

@ -13,6 +13,10 @@ import Toolbox, { ToolboxEvent } from '../../ui/toolbox';
* @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set)
* - make Block Settings a standalone module
*
* @todo - Keyboard-only mode bug:
* press Tab, flip to the Checkbox. press Enter (block will be added), Press Tab
* (Block Tunes will be opened with Move up focused), press Enter, press Tab both Block Tunes and Toolbox will be opened
*
* @todo TESTCASE - show toggler after opening and closing the Inline Toolbar
* @todo TESTCASE - Click outside Editor holder should close Toolbar and Clear Focused blocks
* @todo TESTCASE - Click inside Editor holder should close Toolbar and Clear Focused blocks
@ -33,11 +37,7 @@ interface ToolbarNodes {
content: HTMLElement;
actions: HTMLElement;
// Content Zone
plusButton: HTMLElement;
// Actions Zone
blockActionsButtons: HTMLElement;
settingsToggler: HTMLElement;
}
/**
@ -137,14 +137,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
toolbarOpened: 'ce-toolbar--opened',
openedToolboxHolderModifier: 'codex-editor--toolbox-opened',
// Content Zone
plusButton: 'ce-toolbar__plus',
plusButtonShortcut: 'ce-toolbar__plus-shortcut',
plusButtonHidden: 'ce-toolbar__plus--hidden',
// Actions Zone
blockActionsButtons: 'ce-toolbar__actions-buttons',
settingsToggler: 'ce-toolbar__settings-btn',
settingsTogglerHidden: 'ce-toolbar__settings-btn--hidden',
};
}
@ -157,23 +153,6 @@ export default class Toolbar extends Module<ToolbarNodes> {
return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);
}
/**
* Plus Button public methods
*
* @returns {{hide: function(): void, show: function(): void}}
*/
public get plusButton(): { hide: () => void; show: () => void } {
return {
hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
show: (): void => {
if (this.toolboxInstance.isEmpty) {
return;
}
this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden);
},
};
}
/**
* Public interface for accessing the Toolbox
*/
@ -182,11 +161,14 @@ export default class Toolbar extends Module<ToolbarNodes> {
close: () => void;
open: () => void;
toggle: () => void;
flipperHasFocus: boolean;
hasFocus: () => boolean;
} {
return {
opened: this.toolboxInstance.opened,
close: (): void => this.toolboxInstance.close(),
close: (): void => {
this.toolboxInstance.close();
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
},
open: (): void => {
/**
* Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block.
@ -196,14 +178,12 @@ export default class Toolbar extends Module<ToolbarNodes> {
this.toolboxInstance.open();
},
toggle: (): void => this.toolboxInstance.toggle(),
flipperHasFocus: this.toolboxInstance.flipperHasFocus,
hasFocus: (): boolean => this.toolboxInstance.hasFocus(),
};
}
/**
* Block actions appearance manipulations
*
* @returns {{hide: function(): void, show: function(): void}}
*/
private get blockActions(): { hide: () => void; show: () => void } {
return {
@ -216,6 +196,16 @@ export default class Toolbar extends Module<ToolbarNodes> {
};
}
/**
* Methods for working with Block Tunes toggler
*/
private get blockTunesToggler(): { hide: () => void; show: () => void } {
return {
hide: (): void => this.nodes.settingsToggler.classList.add(this.CSS.settingsTogglerHidden),
show: (): void => this.nodes.settingsToggler.classList.remove(this.CSS.settingsTogglerHidden),
};
}
/**
* Toggles read-only mode
*
@ -227,7 +217,6 @@ export default class Toolbar extends Module<ToolbarNodes> {
this.enableModuleBindings();
} else {
this.destroy();
this.toolboxInstance.destroy();
this.Editor.BlockSettings.destroy();
this.disableModuleBindings();
}
@ -277,15 +266,15 @@ export default class Toolbar extends Module<ToolbarNodes> {
/**
* Move Toolbar to the Top coordinate of Block
*/
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`;
this.nodes.wrapper.style.top = `${Math.floor(toolbarY)}px`;
/**
* Plus Button should be shown only for __empty__ __default__ block
* Do not show Block Tunes Toggler near single and empty block
*/
if (block.tool.isDefault && block.isEmpty) {
this.plusButton.show();
if (this.Editor.BlockManager.blocks.length === 1 && block.isEmpty) {
this.blockTunesToggler.hide();
} else {
this.plusButton.hide();
this.blockTunesToggler.show();
}
this.open();
@ -295,6 +284,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
* Close the Toolbar
*/
public close(): void {
if (this.Editor.ReadOnly.isEnabled) {
return;
}
this.nodes.wrapper.classList.remove(this.CSS.toolbarOpened);
/** Close components */
@ -378,13 +371,11 @@ export default class Toolbar extends Module<ToolbarNodes> {
* - Remove Block Button
* - Settings Panel
*/
this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons);
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
const settingsIcon = $.svg('dots', 16, 16);
$.append(this.nodes.settingsToggler, settingsIcon);
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
$.append(this.nodes.actions, this.nodes.settingsToggler);
this.tooltip.onHover(
this.nodes.settingsToggler,
@ -397,7 +388,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
/**
* Appending Toolbar components to itself
*/
$.append(this.nodes.content, this.makeToolbox());
$.append(this.nodes.actions, this.makeToolbox());
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
/**
@ -416,6 +407,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
this.toolboxInstance = new Toolbox({
api: this.Editor.API.methods,
tools: this.Editor.Tools.blockTools,
i18nLabels: {
filter: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Filter'),
nothingFound: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Nothing found'),
},
});
this.toolboxInstance.on(ToolboxEvent.Opened, () => {
@ -484,18 +479,25 @@ export default class Toolbar extends Module<ToolbarNodes> {
}, true);
/**
* Subscribe to the 'block-hovered' event
* Subscribe to the 'block-hovered' event if currenct view is not mobile
*
* @see https://github.com/codex-team/editor.js/issues/1972
*/
this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => {
if (!_.isMobileScreen()) {
/**
* Do not move toolbar if Block Settings or Toolbox opened
* Subscribe to the 'block-hovered' event
*/
if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) {
return;
}
this.eventsDispatcher.on(this.Editor.UI.events.blockHovered, (data: {block: Block}) => {
/**
* Do not move toolbar if Block Settings or Toolbox opened
*/
if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) {
return;
}
this.moveAndOpen(data.block);
});
this.moveAndOpen(data.block);
});
}
}
/**
@ -551,7 +553,9 @@ export default class Toolbar extends Module<ToolbarNodes> {
*/
private destroy(): void {
this.removeAllNodes();
this.toolboxInstance.destroy();
if (this.toolboxInstance) {
this.toolboxInstance.destroy();
}
this.tooltip.destroy();
}
}

View file

@ -698,7 +698,10 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
private enableFlipper(): void {
this.flipper = new Flipper({
focusedItemClass: this.CSS.focusedButton,
allowArrows: false,
allowedKeys: [
_.keyCodes.ENTER,
_.keyCodes.TAB,
],
});
}
}

View file

@ -231,7 +231,7 @@ export default class UI extends Module<UINodes> {
* Toolbar has internal module (Toolbox) that has own Flipper,
* so we check it manually
*/
if (this.Editor.Toolbar.toolbox.flipperHasFocus) {
if (this.Editor.Toolbar.toolbox.hasFocus()) {
return true;
}
@ -239,7 +239,7 @@ export default class UI extends Module<UINodes> {
return moduleClass.flipper instanceof Flipper;
})
.some(([moduleName, moduleClass]) => {
return moduleClass.flipper.currentItem;
return moduleClass.flipper.hasFocus();
});
}
@ -385,7 +385,7 @@ export default class UI extends Module<UINodes> {
*/
private watchBlockHoveredEvents(): void {
/**
* Used to not to emit the same block multiple times to the 'block-hovered' event on every mousemove
* Used to not emit the same block multiple times to the 'block-hovered' event on every mousemove
*/
let blockHoveredEmitted;

View file

@ -96,3 +96,46 @@ if (!Element.prototype.prepend) {
this.insertBefore(docFrag, this.firstChild);
};
}
interface Element {
/**
* Scrolls the current element into the visible area of the browser window
*
* @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor.
*/
scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
}
/**
* ScrollIntoViewIfNeeded polyfill by KilianSSL (forked from hsablonniere)
*
* @see {@link https://gist.github.com/KilianSSL/774297b76378566588f02538631c3137}
* @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor.
*/
if (!Element.prototype.scrollIntoViewIfNeeded) {
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded): void {
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
const parent = this.parentNode,
parentComputedStyle = window.getComputedStyle(parent, null),
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
alignWithTop = overTop && !overBottom;
if ((overTop || overBottom) && centerIfNeeded) {
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
}
if ((overLeft || overRight) && centerIfNeeded) {
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
}
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
this.scrollIntoView(alignWithTop);
}
};
}

View file

@ -1,15 +1,17 @@
import $ from '../dom';
import * as _ from '../utils';
import Flipper from '../flipper';
import { BlockToolAPI } from '../block';
import I18n from '../i18n';
import { I18nInternalNS } from '../i18n/namespace-internal';
import Shortcuts from '../utils/shortcuts';
import Tooltip from '../utils/tooltip';
import BlockTool from '../tools/block';
import ToolsCollection from '../tools/collection';
import { API } from '../../../types';
import EventsDispatcher from '../utils/events';
import Popover, { PopoverEvent } from '../utils/popover';
import I18n from '../i18n';
import { I18nInternalNS } from '../i18n/namespace-internal';
/**
* @todo the first Tab on the Block focus Plus Button, the second focus Block Tunes Toggler, the third focus next Block
*/
/**
* Event that can be triggered by the Toolbox
@ -31,6 +33,11 @@ export enum ToolboxEvent {
BlockAdded = 'toolbox-block-added',
}
/**
* Available i18n dict keys that should be passed to the constructor
*/
type toolboxTextLabelsKeys = 'filter' | 'nothingFound';
/**
* Toolbox
* This UI element contains list of Block Tools available to be inserted
@ -45,7 +52,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
* @returns {boolean}
*/
public get isEmpty(): boolean {
return this.displayedToolsCount === 0;
return this.toolsToBeDisplayed.length === 0;
}
/**
@ -60,21 +67,29 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
*/
private api: API;
/**
* Popover instance. There is a util for vertical lists.
*/
private popover: Popover;
/**
* List of Tools available. Some of them will be shown in the Toolbox
*/
private tools: ToolsCollection<BlockTool>;
/**
* Text labels used in the Toolbox. Should be passed from the i18n module
*/
private i18nLabels: Record<toolboxTextLabelsKeys, string>;
/**
* Current module HTML Elements
*/
private nodes: {
toolbox: HTMLElement;
buttons: HTMLElement[];
} = {
toolbox: null,
buttons: [],
}
};
/**
* CSS styles
@ -84,34 +99,10 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
private static get CSS(): { [name: string]: string } {
return {
toolbox: 'ce-toolbox',
toolboxButton: 'ce-toolbox__button',
toolboxButtonActive: 'ce-toolbox__button--active',
toolboxOpened: 'ce-toolbox--opened',
buttonTooltip: 'ce-toolbox-button-tooltip',
buttonShortcut: 'ce-toolbox-button-tooltip__shortcut',
toolboxOpenedTop: 'ce-toolbox--opened-top',
};
}
/**
* How many tools displayed in Toolbox
*
* @type {number}
*/
private displayedToolsCount = 0;
/**
* Instance of class that responses for leafing buttons by arrows/tab
*
* @type {Flipper|null}
*/
private flipper: Flipper = null;
/**
* Tooltip utility Instance
*/
private tooltip: Tooltip;
/**
* Id of listener added used to remove it on destroy()
*/
@ -124,67 +115,78 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
* @param options.api - Editor API methods
* @param options.tools - Tools available to check whether some of them should be displayed at the Toolbox or not
*/
constructor({ api, tools }) {
constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection<BlockTool>; i18nLabels: Record<toolboxTextLabelsKeys, string>}) {
super();
this.api = api;
this.tools = tools;
this.tooltip = new Tooltip();
}
/**
* Returns true if the Toolbox has the Flipper activated and the Flipper has selected button
*/
public get flipperHasFocus(): boolean {
return this.flipper && this.flipper.currentItem !== null;
this.i18nLabels = i18nLabels;
}
/**
* Makes the Toolbox
*/
public make(): Element {
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
this.popover = new Popover({
className: Toolbox.CSS.toolbox,
searchable: true,
filterLabel: this.i18nLabels.filter,
nothingFoundLabel: this.i18nLabels.nothingFound,
items: this.toolsToBeDisplayed.map(tool => {
return {
icon: tool.toolbox.icon,
label: I18n.t(I18nInternalNS.toolNames, tool.toolbox.title || _.capitalize(tool.name)),
name: tool.name,
onClick: (item): void => {
this.toolButtonActivated(tool.name);
},
secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '',
};
}),
});
this.addTools();
this.enableFlipper();
this.popover.on(PopoverEvent.OverlayClicked, this.onOverlayClicked);
/**
* Enable tools shortcuts
*/
this.enableShortcuts();
this.nodes.toolbox = this.popover.getElement();
return this.nodes.toolbox;
}
/**
* Returns true if the Toolbox has the Flipper activated and the Flipper has selected button
*/
public hasFocus(): boolean {
return this.popover.hasFocus();
}
/**
* Destroy Module
*/
public destroy(): void {
super.destroy();
/**
* Sometimes (in read-only mode) there is no Flipper
*/
if (this.flipper) {
this.flipper.deactivate();
this.flipper = null;
}
if (this.nodes && this.nodes.toolbox) {
this.nodes.toolbox.remove();
this.nodes.toolbox = null;
this.nodes.buttons = [];
}
this.api.listeners.offById(this.clickListenerId);
this.removeAllShortcuts();
this.tooltip.destroy();
this.popover.off(PopoverEvent.OverlayClicked, this.onOverlayClicked);
}
/**
* Toolbox Tool's button click handler
*
* @param {MouseEvent|KeyboardEvent} event - event that activates toolbox button
* @param {string} toolName - button to activate
* @param toolName - tool type to be activated
*/
public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void {
public toolButtonActivated(toolName: string): void {
this.insertNewBlock(toolName);
}
@ -196,24 +198,28 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
return;
}
this.emit(ToolboxEvent.Opened);
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened);
/**
* Open the popover above the button
* if there is not enough available space below it
*/
if (!this.shouldOpenPopoverBottom) {
this.nodes.toolbox.style.setProperty('--popover-height', this.popover.calculateHeight() + 'px');
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpenedTop);
}
this.popover.show();
this.opened = true;
this.flipper.activate();
this.emit(ToolboxEvent.Opened);
}
/**
* Close Toolbox
*/
public close(): void {
this.emit(ToolboxEvent.Closed);
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened);
this.popover.hide();
this.opened = false;
this.flipper.deactivate();
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpenedTop);
this.emit(ToolboxEvent.Closed);
}
/**
@ -228,106 +234,65 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
}
/**
* Iterates available tools and appends them to the Toolbox
* Checks if there popover should be opened downwards.
* It happens in case there is enough space below or not enough space above
*/
private addTools(): void {
Array
private get shouldOpenPopoverBottom(): boolean {
const toolboxRect = this.nodes.toolbox.getBoundingClientRect();
const editorElementRect = this.api.ui.nodes.redactor.getBoundingClientRect();
const popoverHeight = this.popover.calculateHeight();
const popoverPotentialBottomEdge = toolboxRect.top + popoverHeight;
const popoverPotentialTopEdge = toolboxRect.top - popoverHeight;
const bottomEdgeForComparison = Math.min(window.innerHeight, editorElementRect.bottom);
return popoverPotentialTopEdge < editorElementRect.top || popoverPotentialBottomEdge <= bottomEdgeForComparison;
}
/**
* Handles overlay click
*/
private onOverlayClicked = (): void => {
this.close();
}
/**
* Returns list of tools that enables the Toolbox (by specifying the 'toolbox' getter)
*/
@_.cacheable
private get toolsToBeDisplayed(): BlockTool[] {
return Array
.from(this.tools.values())
.forEach((tool) => this.addTool(tool));
.filter(tool => {
const toolToolboxSettings = tool.toolbox;
/**
* Skip tools that don't pass 'toolbox' property
*/
if (!toolToolboxSettings) {
return false;
}
if (toolToolboxSettings && !toolToolboxSettings.icon) {
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name);
return false;
}
return true;
});
}
/**
* Append Tool to the Toolbox
*
* @param {BlockToolConstructable} tool - BlockTool object
* Iterate all tools and enable theirs shortcuts if specified
*/
private addTool(tool: BlockTool): void {
const toolToolboxSettings = tool.toolbox;
private enableShortcuts(): void {
this.toolsToBeDisplayed.forEach((tool: BlockTool) => {
const shortcut = tool.shortcut;
/**
* Skip tools that don't pass 'toolbox' property
*/
if (!toolToolboxSettings) {
return;
}
if (toolToolboxSettings && !toolToolboxSettings.icon) {
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', tool.name);
return;
}
/**
* @todo Add checkup for the render method
*/
// if (typeof tool.render !== 'function') {
// _.log('render method missed. Tool %o skipped', 'warn', tool);
// return;
// }
const button = $.make('li', [ Toolbox.CSS.toolboxButton ]);
button.dataset.tool = tool.name;
button.innerHTML = toolToolboxSettings.icon;
$.append(this.nodes.toolbox, button);
this.nodes.toolbox.appendChild(button);
this.nodes.buttons.push(button);
/**
* Add click listener
*/
this.clickListenerId = this.api.listeners.on(button, 'click', (event: KeyboardEvent|MouseEvent) => {
this.toolButtonActivate(event, tool.name);
if (shortcut) {
this.enableShortcutForTool(tool.name, shortcut);
}
});
/**
* Add listeners to show/hide toolbox tooltip
*/
const tooltipContent = this.drawTooltip(tool);
this.tooltip.onHover(button, tooltipContent, {
placement: 'bottom',
hidingDelay: 200,
});
const shortcut = tool.shortcut;
if (shortcut) {
this.enableShortcut(tool.name, shortcut);
}
/** Increment Tools count */
this.displayedToolsCount++;
}
/**
* Draw tooltip for toolbox tools
*
* @param tool - BlockTool object
* @returns {HTMLElement}
*/
private drawTooltip(tool: BlockTool): HTMLElement {
const toolboxSettings = tool.toolbox || {};
const name = I18n.t(I18nInternalNS.toolNames, toolboxSettings.title || tool.name);
let shortcut = tool.shortcut;
const tooltip = $.make('div', Toolbox.CSS.buttonTooltip);
const hint = document.createTextNode(_.capitalize(name));
tooltip.appendChild(hint);
if (shortcut) {
shortcut = _.beautifyShortcut(shortcut);
tooltip.appendChild($.make('div', Toolbox.CSS.buttonShortcut, {
textContent: shortcut,
}));
}
return tooltip;
}
/**
@ -336,7 +301,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
* @param {string} toolName - Tool name
* @param {string} shortcut - shortcut according to the ShortcutData Module format
*/
private enableShortcut(toolName: string, shortcut: string): void {
private enableShortcutForTool(toolName: string, shortcut: string): void {
Shortcuts.add({
name: shortcut,
on: this.api.ui.nodes.redactor,
@ -352,26 +317,12 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
* Fired when the Read-Only mode is activated
*/
private removeAllShortcuts(): void {
Array
.from(this.tools.values())
.forEach((tool) => {
const shortcut = tool.shortcut;
this.toolsToBeDisplayed.forEach((tool: BlockTool) => {
const shortcut = tool.shortcut;
if (shortcut) {
Shortcuts.remove(this.api.ui.nodes.redactor, shortcut);
}
});
}
/**
* Creates Flipper instance to be able to leaf tools
*/
private enableFlipper(): void {
const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[];
this.flipper = new Flipper({
items: tools,
focusedItemClass: Toolbox.CSS.toolboxButtonActive,
if (shortcut) {
Shortcuts.remove(this.api.ui.nodes.redactor, shortcut);
}
});
}

View file

@ -762,3 +762,20 @@ export function cacheable<Target, Value, Arguments extends unknown[] = unknown[]
return descriptor;
};
/**
* True if screen has mobile size
*/
export function isMobileScreen(): boolean {
return window.matchMedia('(max-width: 650px)').matches;
}
/**
* True if current device runs iOS
*/
export const isIosDevice =
typeof window !== 'undefined' &&
window.navigator &&
window.navigator.platform &&
(/iP(ad|hone|od)/.test(window.navigator.platform) ||
(window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1));

View file

@ -0,0 +1,417 @@
import Dom from '../dom';
import Listeners from './listeners';
import Flipper from '../flipper';
import SearchInput from './search-input';
import EventsDispatcher from './events';
import { isMobileScreen, keyCodes, cacheable } from '../utils';
import ScrollLocker from './scroll-locker';
/**
* Describe parameters for rendering the single item of Popover
*/
export interface PopoverItem {
/**
* Item icon to be appeared near a title
*/
icon: string;
/**
* Displayed text
*/
label: string;
/**
* Item name
* Used in data attributes needed for cypress tests
*/
name?: string;
/**
* Additional displayed text
*/
secondaryLabel?: string;
/**
* Itm click handler
*
* @param item - clicked item
*/
onClick: (item: PopoverItem) => void;
}
/**
* Event that can be triggered by the Popover
*/
export enum PopoverEvent {
/**
* When popover overlay is clicked
*/
OverlayClicked = 'overlay-clicked',
}
/**
* Popover is the UI element for displaying vertical lists
*/
export default class Popover extends EventsDispatcher<PopoverEvent> {
/**
* Items list to be displayed
*/
private readonly items: PopoverItem[];
/**
* Stores the visibility state.
*/
private isShown = false;
/**
* Created nodes
*/
private nodes: {
wrapper: HTMLElement;
popover: HTMLElement;
items: HTMLElement;
nothingFound: HTMLElement;
overlay: HTMLElement;
} = {
wrapper: null,
popover: null,
items: null,
nothingFound: null,
overlay: null,
}
/**
* Additional wrapper's class name
*/
private readonly className: string;
/**
* Listeners util instance
*/
private listeners: Listeners;
/**
* Flipper - module for keyboard iteration between elements
*/
private flipper: Flipper;
/**
* Pass true to enable local search field
*/
private readonly searchable: boolean;
/**
* Instance of the Search Input
*/
private search: SearchInput;
/**
* Label for the 'Filter' placeholder
*/
private readonly filterLabel: string;
/**
* Label for the 'Nothing found' message
*/
private readonly nothingFoundLabel: string;
/**
* Style classes
*/
private static get CSS(): {
popover: string;
popoverOpened: string;
itemsWrapper: string;
item: string;
itemHidden: string;
itemFocused: string;
itemLabel: string;
itemIcon: string;
itemSecondaryLabel: string;
noFoundMessage: string;
noFoundMessageShown: string;
popoverOverlay: string;
popoverOverlayHidden: string;
} {
return {
popover: 'ce-popover',
popoverOpened: 'ce-popover--opened',
itemsWrapper: 'ce-popover__items',
item: 'ce-popover__item',
itemHidden: 'ce-popover__item--hidden',
itemFocused: 'ce-popover__item--focused',
itemLabel: 'ce-popover__item-label',
itemIcon: 'ce-popover__item-icon',
itemSecondaryLabel: 'ce-popover__item-secondary-label',
noFoundMessage: 'ce-popover__no-found',
noFoundMessageShown: 'ce-popover__no-found--shown',
popoverOverlay: 'ce-popover__overlay',
popoverOverlayHidden: 'ce-popover__overlay--hidden',
};
}
/**
* ScrollLocker instance
*/
private scrollLocker = new ScrollLocker()
/**
* Creates the Popover
*
* @param options - config
* @param options.items - config for items to be displayed
* @param options.className - additional class name to be added to the popover wrapper
* @param options.filterLabel - label for the search Field
* @param options.nothingFoundLabel - label of the 'nothing found' message
*/
constructor({ items, className, searchable, filterLabel, nothingFoundLabel }: {
items: PopoverItem[];
className?: string;
searchable?: boolean;
filterLabel: string;
nothingFoundLabel: string;
}) {
super();
this.items = items;
this.className = className || '';
this.searchable = searchable;
this.listeners = new Listeners();
this.filterLabel = filterLabel;
this.nothingFoundLabel = nothingFoundLabel;
this.render();
this.enableFlipper();
}
/**
* Returns rendered wrapper
*/
public getElement(): HTMLElement {
return this.nodes.wrapper;
}
/**
* Shows the Popover
*/
public show(): void {
/**
* Clear search and items scrolling
*/
this.search.clear();
this.nodes.items.scrollTop = 0;
this.nodes.popover.classList.add(Popover.CSS.popoverOpened);
this.nodes.overlay.classList.remove(Popover.CSS.popoverOverlayHidden);
this.flipper.activate();
if (this.searchable) {
window.requestAnimationFrame(() => {
this.search.focus();
});
}
if (isMobileScreen()) {
this.scrollLocker.lock();
}
this.isShown = true;
}
/**
* Hides the Popover
*/
public hide(): void {
/**
* If it's already hidden, do nothing
* to prevent extra DOM operations
*/
if (!this.isShown) {
return;
}
this.nodes.popover.classList.remove(Popover.CSS.popoverOpened);
this.nodes.overlay.classList.add(Popover.CSS.popoverOverlayHidden);
this.flipper.deactivate();
if (isMobileScreen()) {
this.scrollLocker.unlock();
}
this.isShown = false;
}
/**
* Clears memory
*/
public destroy(): void {
this.listeners.removeAll();
}
/**
* Returns true if some item is focused
*/
public hasFocus(): boolean {
return this.flipper.hasFocus();
}
/**
* Helps to calculate height of popover while it is not displayed on screen.
* Renders invisible clone of popover to get actual height.
*/
@cacheable
public calculateHeight(): number {
let height = 0;
const popoverClone = this.nodes.popover.cloneNode(true) as HTMLElement;
popoverClone.style.visibility = 'hidden';
popoverClone.style.position = 'absolute';
popoverClone.style.top = '-1000px';
popoverClone.classList.add(Popover.CSS.popoverOpened);
document.body.appendChild(popoverClone);
height = popoverClone.offsetHeight;
popoverClone.remove();
return height;
}
/**
* Makes the UI
*/
private render(): void {
this.nodes.wrapper = Dom.make('div', this.className);
this.nodes.popover = Dom.make('div', Popover.CSS.popover);
this.nodes.wrapper.appendChild(this.nodes.popover);
this.nodes.overlay = Dom.make('div', [Popover.CSS.popoverOverlay, Popover.CSS.popoverOverlayHidden]);
this.nodes.wrapper.appendChild(this.nodes.overlay);
if (this.searchable) {
this.addSearch(this.nodes.popover);
}
this.nodes.items = Dom.make('div', Popover.CSS.itemsWrapper);
this.items.forEach(item => {
this.nodes.items.appendChild(this.createItem(item));
});
this.nodes.popover.appendChild(this.nodes.items);
this.nodes.nothingFound = Dom.make('div', [ Popover.CSS.noFoundMessage ], {
textContent: this.nothingFoundLabel,
});
this.nodes.popover.appendChild(this.nodes.nothingFound);
this.listeners.on(this.nodes.popover, 'click', (event: KeyboardEvent|MouseEvent) => {
const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement;
if (clickedItem) {
this.itemClicked(clickedItem);
}
});
this.listeners.on(this.nodes.overlay, 'click', () => {
this.emit(PopoverEvent.OverlayClicked);
});
}
/**
* Adds the s4arch field to passed element
*
* @param holder - where to append search input
*/
private addSearch(holder: HTMLElement): void {
this.search = new SearchInput({
items: this.items,
placeholder: this.filterLabel,
onSearch: (filteredItems): void => {
const itemsVisible = [];
this.items.forEach((item, index) => {
const itemElement = this.nodes.items.children[index];
if (filteredItems.includes(item)) {
itemsVisible.push(itemElement);
itemElement.classList.remove(Popover.CSS.itemHidden);
} else {
itemElement.classList.add(Popover.CSS.itemHidden);
}
});
this.nodes.nothingFound.classList.toggle(Popover.CSS.noFoundMessageShown, itemsVisible.length === 0);
/**
* Update flipper items with only visible
*/
this.flipper.deactivate();
this.flipper.activate(itemsVisible);
this.flipper.focusFirst();
},
});
const searchField = this.search.getElement();
holder.appendChild(searchField);
}
/**
* Renders the single item
*
* @param item - item data to be rendered
*/
private createItem(item: PopoverItem): HTMLElement {
const el = Dom.make('div', Popover.CSS.item);
el.dataset.itemName = item.name;
const label = Dom.make('div', Popover.CSS.itemLabel, {
innerHTML: item.label,
});
if (item.icon) {
el.appendChild(Dom.make('div', Popover.CSS.itemIcon, {
innerHTML: item.icon,
}));
}
el.appendChild(label);
if (item.secondaryLabel) {
el.appendChild(Dom.make('div', Popover.CSS.itemSecondaryLabel, {
textContent: item.secondaryLabel,
}));
}
return el;
}
/**
* Item click handler
*
* @param itemEl - clicked item
*/
private itemClicked(itemEl: HTMLElement): void {
const allItems = this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`);
const itemIndex = Array.from(allItems).indexOf(itemEl);
const clickedItem = this.items[itemIndex];
clickedItem.onClick(clickedItem);
}
/**
* Creates Flipper instance to be able to leaf tools
*/
private enableFlipper(): void {
const tools = Array.from(this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`)) as HTMLElement[];
this.flipper = new Flipper({
items: tools,
focusedItemClass: Popover.CSS.itemFocused,
allowedKeys: [
keyCodes.TAB,
keyCodes.UP,
keyCodes.DOWN,
keyCodes.ENTER,
],
});
}
}

View file

@ -0,0 +1,64 @@
import { isIosDevice } from '../utils';
/**
* Utility allowing to lock body scroll on demand
*/
export default class ScrollLocker {
/**
* Style classes
*/
private static CSS = {
scrollLocked: 'ce-scroll-locked',
scrollLockedHard: 'ce-scroll-locked--hard',
}
/**
* Stores scroll position, used for hard scroll lock
*/
private scrollPosition: null|number
/**
* Locks body element scroll
*/
public lock(): void {
if (isIosDevice) {
this.lockHard();
} else {
document.body.classList.add(ScrollLocker.CSS.scrollLocked);
}
}
/**
* Unlocks body element scroll
*/
public unlock(): void {
if (isIosDevice) {
this.unlockHard();
} else {
document.body.classList.remove(ScrollLocker.CSS.scrollLocked);
}
}
/**
* Locks scroll in a hard way (via setting fixed position to body element)
*/
private lockHard(): void {
this.scrollPosition = window.pageYOffset;
document.documentElement.style.setProperty(
'--window-scroll-offset',
`${this.scrollPosition}px`
);
document.body.classList.add(ScrollLocker.CSS.scrollLockedHard);
}
/**
* Unlocks hard scroll lock
*/
private unlockHard(): void {
document.body.classList.remove(ScrollLocker.CSS.scrollLockedHard);
if (this.scrollPosition !== null) {
window.scrollTo(0, this.scrollPosition);
}
this.scrollPosition = null;
}
}

View file

@ -0,0 +1,152 @@
import Dom from '../dom';
import Listeners from './listeners';
/**
* Item that could be searched
*/
interface SearchableItem {
label: string;
}
/**
* Provides search input element and search logic
*/
export default class SearchInput {
/**
* Input wrapper element
*/
private wrapper: HTMLElement;
/**
* Editable input itself
*/
private input: HTMLInputElement;
/**
* The instance of the Listeners util
*/
private listeners: Listeners;
/**
* Items for local search
*/
private items: SearchableItem[];
/**
* Current search query
*/
private searchQuery: string;
/**
* Externally passed callback for the search
*/
private readonly onSearch: (items: SearchableItem[]) => void;
/**
* Styles
*/
private static get CSS(): {
input: string;
icon: string;
wrapper: string;
} {
return {
wrapper: 'cdx-search-field',
icon: 'cdx-search-field__icon',
input: 'cdx-search-field__input',
};
}
/**
* @param options - available config
* @param options.items - searchable items list
* @param options.onSearch - search callback
* @param options.placeholder - input placeholder
*/
constructor({ items, onSearch, placeholder }: {
items: SearchableItem[];
onSearch: (items: SearchableItem[]) => void;
placeholder: string;
}) {
this.listeners = new Listeners();
this.items = items;
this.onSearch = onSearch;
this.render(placeholder);
}
/**
* Returns search field element
*/
public getElement(): HTMLElement {
return this.wrapper;
}
/**
* Sets focus to the input
*/
public focus(): void {
this.input.focus();
}
/**
* Clears search query and results
*/
public clear(): void {
this.input.value = '';
this.searchQuery = '';
this.onSearch(this.foundItems);
}
/**
* Clears memory
*/
public destroy(): void {
this.listeners.removeAll();
}
/**
* Creates the search field
*
* @param placeholder - input placeholder
*/
private render(placeholder: string): void {
this.wrapper = Dom.make('div', SearchInput.CSS.wrapper);
const iconWrapper = Dom.make('div', SearchInput.CSS.icon);
const icon = Dom.svg('search', 16, 16);
this.input = Dom.make('input', SearchInput.CSS.input, {
placeholder,
}) as HTMLInputElement;
iconWrapper.appendChild(icon);
this.wrapper.appendChild(iconWrapper);
this.wrapper.appendChild(this.input);
this.listeners.on(this.input, 'input', () => {
this.searchQuery = this.input.value;
this.onSearch(this.foundItems);
});
}
/**
* Returns list of found items for the current search query
*/
private get foundItems(): SearchableItem[] {
return this.items.filter(item => this.checkItem(item));
}
/**
* Contains logic for checking whether passed item conforms the search query
*
* @param item - item to be checked
*/
private checkItem(item: SearchableItem): boolean {
const text = item.label.toLowerCase();
const query = this.searchQuery.toLowerCase();
return text.includes(query);
}
}

View file

@ -117,3 +117,20 @@
transform: translateY(0);
}
}
@keyframes panelShowingMobile {
from {
opacity: 0;
transform: translateY(14px) scale(0.98);
}
70% {
opacity: 1;
transform: translateY(-4px);
}
to {
transform: translateY(0);
}
}

View file

@ -1,4 +1,17 @@
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.ce-block {
animation: fade-in 300ms ease;
animation-fill-mode: initial;
&:first-of-type {
margin-top: 0;
}

46
src/styles/input.css Normal file
View file

@ -0,0 +1,46 @@
.cdx-search-field {
--icon-margin-right: 10px;
background: rgba(232,232,235,0.49);
border: 1px solid rgba(226,226,229,0.20);
border-radius: 6px;
padding: 2px;
display: grid;
grid-template-columns: auto auto 1fr;
grid-template-rows: auto;
&__icon {
width: var(--toolbox-buttons-size);
height: var(--toolbox-buttons-size);
display: flex;
align-items: center;
justify-content: center;
margin-right: var(--icon-margin-right);
.icon {
width: 14px;
height: 14px;
color: var(--grayText);
flex-shrink: 0;
}
}
&__input {
font-size: 14px;
outline: none;
font-weight: 500;
font-family: inherit;
border: 0;
background: transparent;
margin: 0;
padding: 0;
line-height: 22px;
min-width: calc(100% - var(--toolbox-buttons-size) - var(--icon-margin-right));
&::placeholder {
color: var(--grayText);
font-weight: 500;
}
}
}

View file

@ -10,3 +10,5 @@
@import './export.css';
@import './stub.css';
@import './rtl.css';
@import './popover.css';
@import './input.css';

142
src/styles/popover.css Normal file
View file

@ -0,0 +1,142 @@
.ce-popover {
position: absolute;
opacity: 0;
will-change: opacity, transform;
display: flex;
flex-direction: column;
padding: 6px;
min-width: 200px;
overflow: hidden;
box-sizing: border-box;
flex-shrink: 0;
max-height: 0;
@apply --overlay-pane;
z-index: 4;
flex-wrap: nowrap;
&--opened {
opacity: 1;
max-height: 270px;
animation: panelShowing 100ms ease;
@media (--mobile) {
animation: panelShowingMobile 250ms ease;
}
}
&::-webkit-scrollbar {
width: 7px;
}
&::-webkit-scrollbar-thumb {
box-sizing: border-box;
box-shadow: inset 0 0 2px 2px var(--bg-light);
border: 3px solid transparent;
border-left-width: 0px;
border-top-width: 4px;
border-bottom-width: 4px;
}
@media (--mobile) {
position: fixed;
max-width: none;
min-width: auto;
left: 5px;
right: 5px;
bottom: calc(5px + env(safe-area-inset-bottom));
top: auto;
border-radius: 10px;
}
&__items {
overflow-y: auto;
overscroll-behavior: contain;
@media (--not-mobile) {
margin-top: 5px;
}
}
&__item {
@apply --popover-button;
&--focused {
@apply --button-focused;
}
&--hidden {
display: none;
}
&-icon {
@apply --tool-icon;
}
&-label {
&::after {
content: '';
width: 25px;
display: inline-block;
}
}
&-secondary-label {
color: var(--grayText);
font-size: 12px;
margin-left: auto;
white-space: nowrap;
letter-spacing: -0.1em;
padding-right: 5px;
margin-bottom: -2px;
opacity: 0.6;
@media (--mobile){
display: none;
}
}
}
&__no-found {
@apply --popover-button;
color: var(--grayText);
display: none;
cursor: default;
&--shown {
display: block;
}
&:hover {
background-color: transparent;
}
}
@media (--mobile) {
&__overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: var(--color-dark);
opacity: 0.5;
z-index: 3;
transition: opacity 0.12s ease-in;
will-change: opacity;
visibility: visible;
}
.cdx-search-field {
display: none;
}
}
&__overlay--hidden {
z-index: 0;
opacity: 0;
visibility: hidden;
}
}

View file

@ -1,7 +1,7 @@
.ce-settings {
@apply --overlay-pane;
right: -1px;
top: 30px;
top: var(--toolbar-buttons-size);
left: 0;
min-width: 114px;
box-sizing: content-box;

View file

@ -4,34 +4,18 @@
right: 0;
top: 0;
transition: opacity 100ms ease;
will-change: opacity, transform;
display: none;
will-change: opacity, top;
@media (--mobile) {
@apply --overlay-pane;
padding: 3px;
margin-top: 5px;
}
display: none;
&--opened {
display: block;
@media (--mobile){
display: flex;
}
}
&__content {
max-width: var(--content-width);
margin: 0 auto;
position: relative;
@media (--mobile){
display: flex;
align-content: center;
margin: 0;
max-width: 100%;
}
}
&__plus {
@ -44,14 +28,9 @@
margin-top: 5px;
}
&--hidden {
display: none;
}
@media (--mobile){
display: inline-flex !important;
@apply --overlay-pane;
position: static;
transform: none !important;
}
}
@ -64,37 +43,37 @@
right: 100%;
opacity: 0;
display: flex;
@media (--mobile){
position: absolute;
right: auto;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
}
padding-right: 5px;
&--opened {
opacity: 1;
}
&-buttons {
text-align: right;
@media (--mobile){
right: auto;
}
}
&__settings-btn {
@apply --toolbox-button;
width: 18px;
margin: 0 5px;
margin-left: 5px;
cursor: pointer;
user-select: none;
}
}
.codex-editor--toolbox-opened .ce-toolbar__actions {
display: none;
@media (--not-mobile){
width: 18px;
}
&--hidden {
display: none;
}
@media (--mobile){
@apply --overlay-pane;
position: static;
}
}
}
/**

View file

@ -1,44 +1,24 @@
.ce-toolbox {
position: absolute;
visibility: hidden;
transition: opacity 100ms ease;
will-change: opacity;
display: flex;
flex-direction: row;
--gap: 8px;
@media (--mobile){
position: static;
transform: none !important;
align-items: center;
overflow-x: auto;
}
@media (--not-mobile){
position: absolute;
top: calc(var(--toolbox-buttons-size) + var(--gap));
left: 0;
&--opened {
opacity: 1;
visibility: visible;
}
&__button {
@apply --toolbox-button;
flex-shrink: 0;
margin-left: 5px;
&--opened-top {
top: calc(-1 * (var(--gap) + var(--popover-height)));
}
}
}
.ce-toolbox-button-tooltip {
&__shortcut {
opacity: 0.6;
word-spacing: -3px;
margin-top: 3px;
}
}
/**
* Styles for Narrow mode
*/
.codex-editor--narrow .ce-toolbox {
@media (--not-mobile) {
background: #fff;
z-index: 2;
@media (--not-mobile){
left: auto;
right: 0;
.ce-popover {
right: 0;
}
}
}

View file

@ -127,3 +127,14 @@
transform: rotate(360deg);
}
}
.ce-scroll-locked {
overflow: hidden;
}
.ce-scroll-locked--hard {
overflow: hidden;
top: calc(-1 * var(--window-scroll-offset));
position: fixed;
width: 100%;
}

View file

@ -1,5 +1,9 @@
/**
* Updating values in media queries should also include changes in utils.ts@isMobile
*/
@custom-media --mobile (width <= 650px);
@custom-media --not-mobile (width >= 651px);
@custom-media --can-hover (hover: hover);
:root {
/**
@ -21,7 +25,7 @@
/**
* Gray icons hover
*/
--color-dark: #1D202B;
--color-dark: #1D202B;
/**
* Blue icons
@ -95,6 +99,11 @@
}
};
--button-focused: {
box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08);
background: rgba(34, 186, 255, 0.08) !important;
};
/**
* Styles for Toolbox Buttons and Plus Button
*/
@ -103,22 +112,25 @@
cursor: pointer;
width: var(--toolbox-buttons-size);
height: var(--toolbox-buttons-size);
border-radius: 3px;
border-radius: 7px;
display: inline-flex;
justify-content: center;
align-items: center;
user-select: none;
@media (--mobile){
width: var(--toolbox-buttons-size--mobile);
height: var(--toolbox-buttons-size--mobile);
}
&:hover,
&--active {
background-color: var(--bg-light);
@media (--can-hover) {
&:hover {
background-color: var(--bg-light);
}
}
&--active{
&--active {
background-color: var(--bg-light);
animation: bounceIn 0.75s 1;
animation-fill-mode: forwards;
}
@ -132,9 +144,9 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
line-height: 34px;
width: var(--toolbar-buttons-size);
height: var(--toolbar-buttons-size);
line-height: var(--toolbar-buttons-size);
padding: 0 !important;
text-align: center;
border-radius: 3px;
@ -155,8 +167,7 @@
}
&--focused {
box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08);
background: rgba(34, 186, 255, 0.08) !important;
@apply --button-focused;
&-animated {
animation-name: buttonClicked;
@ -164,5 +175,66 @@
}
}
};
/**
* Element of the Toolbox. Has icon and label
*/
--popover-button: {
display: grid;
grid-template-columns: auto auto 1fr;
grid-template-rows: auto;
justify-content: start;
white-space: nowrap;
padding: 3px;
font-size: 14px;
line-height: 20px;
font-weight: 500;
cursor: pointer;
align-items: center;
border-radius: 6px;
&:not(:last-of-type){
margin-bottom: 1px;
}
@media (--can-hover) {
&:hover {
background-color: var(--bg-light);
}
}
@media (--mobile) {
font-size: 16px;
padding: 4px;
}
};
/**
* Tool icon with border
*/
--tool-icon: {
display: inline-flex;
width: var(--toolbox-buttons-size);
height: var(--toolbox-buttons-size);
border: 1px solid var(--color-gray-border);
border-radius: 5px;
align-items: center;
justify-content: center;
background: #fff;
box-sizing: border-box;
flex-shrink: 0;
margin-right: 10px;
@media (--mobile) {
width: var(--toolbox-buttons-size--mobile);
height: var(--toolbox-buttons-size--mobile);
border-radius: 8px;
}
svg {
width: 12px;
height: 12px;
}
}
}

View file

@ -31,7 +31,7 @@ describe.only('Block ids', () => {
.click();
cy.get('[data-cy=editorjs]')
.get('li.ce-toolbox__button[data-tool=header]')
.get('div.ce-popover__item[data-item-name=header]')
.click();
cy.get('[data-cy=editorjs]')

View file

@ -0,0 +1,84 @@
import Header from '@editorjs/header';
import { ToolboxConfig } from '../../../types';
/**
* Tool class allowing to test case when capitalized tool name is used as translation key if toolbox title is missing
*/
class TestTool {
/**
* Returns toolbox config without title
*/
public static get toolbox(): ToolboxConfig {
return {
title: '',
icon: '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"/></svg>',
};
}
}
describe('Editor i18n', () => {
context('Toolbox', () => {
it('should translate tool title in a toolbox', () => {
if (this && this.editorInstance) {
this.editorInstance.destroy();
}
const toolNamesDictionary = {
Heading: 'Заголовок',
};
cy.createEditor({
tools: {
header: Header,
},
i18n: {
messages: {
toolNames: toolNamesDictionary,
},
},
}).as('editorInstance');
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click();
cy.get('[data-cy=editorjs]')
.get('div.ce-toolbar__plus')
.click();
cy.get('[data-cy=editorjs]')
.get('div.ce-popover__item[data-item-name=header]')
.should('contain.text', toolNamesDictionary.Heading);
});
it('should use capitalized tool name as translation key if toolbox title is missing', () => {
if (this && this.editorInstance) {
this.editorInstance.destroy();
}
const toolNamesDictionary = {
TestTool: 'ТестТул',
};
cy.createEditor({
tools: {
testTool: TestTool,
},
i18n: {
messages: {
toolNames: toolNamesDictionary,
},
},
}).as('editorInstance');
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click();
cy.get('[data-cy=editorjs]')
.get('div.ce-toolbar__plus')
.click();
cy.get('[data-cy=editorjs]')
.get('div.ce-popover__item[data-item-name=testTool]')
.should('contain.text', toolNamesDictionary.TestTool);
});
});
});

View file

@ -25,4 +25,26 @@ describe('Editor basic initialization', () => {
.should('be.visible');
});
});
describe('Configuration', () => {
describe('readOnly', () => {
beforeEach(() => {
if (this && this.editorInstance) {
this.editorInstance.destroy();
}
});
it('should create editor without editing ability when true passed', () => {
cy.createEditor({
readOnly: true,
}).as('editorInstance');
cy.get('[data-cy=editorjs]')
.get('div.codex-editor')
.get('div.ce-paragraph')
.invoke('attr', 'contenteditable')
.should('eq', 'false');
});
});
});
});

View file

@ -1,4 +1,6 @@
import Header from '@editorjs/header';
import Code from '@editorjs/code';
import Delimiter from '@editorjs/delimiter';
import { BlockMutationType } from '../../../types/events/block/mutation-type';
/**
@ -16,6 +18,7 @@ describe('onChange callback', () => {
const config = {
tools: {
header: Header,
code: Code,
},
onChange: (api, event): void => {
console.log('something changed', api, event);
@ -30,6 +33,32 @@ describe('onChange callback', () => {
cy.createEditor(config).as('editorInstance');
}
/**
* Creates Editor instance with save inside the onChange event.
*
* @param blocks - list of blocks to prefill the editor
*/
function createEditorWithSave(blocks = null): void {
const config = {
tools: {
header: Header,
code: Code,
delimiter: Delimiter,
},
onChange: (api, event): void => {
console.log('something changed', api, event);
api.saver.save();
},
data: blocks ? {
blocks,
} : null,
};
cy.spy(config, 'onChange').as('onChange');
cy.createEditor(config).as('editorInstance');
}
/**
* EditorJS API is passed as the first parameter of the onChange callback
*/
@ -47,7 +76,7 @@ describe('onChange callback', () => {
type: BlockMutationType.Added,
detail: {
target: {
name: 'paragraph'
name: 'paragraph',
},
index: 0,
},
@ -67,7 +96,7 @@ describe('onChange callback', () => {
type: BlockMutationType.Added,
detail: {
target: {
name: 'paragraph'
name: 'paragraph',
},
index: 1,
},
@ -85,7 +114,54 @@ describe('onChange callback', () => {
cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({
type: BlockMutationType.Changed,
detail: {
index: 0
index: 0,
},
}));
});
it('should fire onChange callback on block insertion with save inside onChange', () => {
createEditorWithSave();
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click();
cy.get('[data-cy=editorjs]')
.get('div.ce-toolbar__plus')
.click();
cy.get('[data-cy=editorjs]')
.get('div.ce-popover__item[data-item-name=delimiter]')
.click();
cy.get('@onChange').should('be.calledThrice');
cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({
type: BlockMutationType.Removed,
detail: {
index: 0,
target: {
name: 'paragraph',
},
},
}));
cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({
type: BlockMutationType.Added,
detail: {
index: 0,
target: {
name: 'delimiter',
},
},
}));
cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({
type: BlockMutationType.Added,
detail: {
index: 1,
target: {
name: 'paragraph',
},
},
}));
});
@ -102,7 +178,7 @@ describe('onChange callback', () => {
.click();
cy.get('[data-cy=editorjs]')
.get('li.ce-toolbox__button[data-tool=header]')
.get('div.ce-popover__item[data-item-name=header]')
.click();
cy.get('@onChange').should('be.calledTwice');
@ -169,6 +245,14 @@ describe('onChange callback', () => {
it('should fire onChange callback when block is removed', () => {
createEditor();
/**
* The only block does not have Tune menu, so need to create at least 2 blocks to test deleting
*/
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click()
.type('some text');
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click();
@ -185,7 +269,7 @@ describe('onChange callback', () => {
cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({
type: BlockMutationType.Removed,
detail: {
index: 0
index: 0,
},
}));
});
@ -219,4 +303,24 @@ describe('onChange callback', () => {
},
}));
});
it('should fire onChange if something changed inside native input', () => {
createEditor([ {
type: 'code',
data: {
code: '',
},
} ]);
cy.get('[data-cy=editorjs')
.get('textarea')
.type('Some input to the textarea');
cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({
type: BlockMutationType.Changed,
detail: {
index: 0,
},
}));
});
});

View file

@ -0,0 +1,50 @@
import EditorJS, { EditorConfig } from '../../../types';
describe('ReadOnly API spec', () => {
function createEditor(config?: EditorConfig): void {
const editorConfig = Object.assign({}, config || {});
cy.createEditor(editorConfig).as('editorInstance');
}
it('should return correct value for readOnly.isEnabled when editor initialized in normal mode', () => {
createEditor();
cy
.get<EditorJS>('@editorInstance')
.then(editor => {
expect(editor.readOnly.isEnabled).to.be.false;
});
});
it('should return correct value for readOnly.isEnabled when editor initialized in read-only mode', () => {
createEditor({
readOnly: true,
});
cy
.get<EditorJS>('@editorInstance')
.then(editor => {
expect(editor.readOnly.isEnabled).to.be.true;
});
});
it('should return correct value for readOnly.isEnabled when read-only mode toggled', () => {
createEditor();
cy
.get<EditorJS>('@editorInstance')
.then(async editor => {
expect(editor.readOnly.isEnabled).to.be.false;
editor.readOnly.toggle()
.then(() => {
expect(editor.readOnly.isEnabled).to.be.true;
})
.then(() => editor.readOnly.toggle())
.then(() => {
expect(editor.readOnly.isEnabled).to.be.false;
});
});
});
});

117
yarn.lock
View file

@ -1423,6 +1423,16 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@editorjs/code@^2.7.0":
version "2.7.0"
resolved "https://registry.yarnpkg.com/@editorjs/code/-/code-2.7.0.tgz#0a21de9ac15e4533605ffcc80969513ab2142ac5"
integrity sha512-gXtTce915fHp3H9i4IqhTxEDbbkT2heFfYiW/bhFHsCmZDpyGzfZxi94kmrEqDmbxXjV49ZZ6GZbR26If13KJw==
"@editorjs/delimiter@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@editorjs/delimiter/-/delimiter-1.2.0.tgz#5075f1a3e68765cfb6aec8694b316d81e2b41607"
integrity sha512-GKsCFPk85vH5FuCuVQ48NTLc9hk0T3DsBH9zABaicTYIJayFcUa8N4/Y+L3i4tduzDqqyvoxkv+5n43GmC5gEA==
"@editorjs/header@^2.6.1":
version "2.6.1"
resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.6.1.tgz#454a46e4dbb32ae3aa1db4d22b0ddf2cc36c3134"
@ -2385,24 +2395,16 @@ browserify@^16.1.0:
vm-browserify "^1.0.0"
xtend "^4.0.0"
browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.6.4, browserslist@^4.8.5, browserslist@^4.9.1:
version "4.12.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d"
browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.14.5, browserslist@^4.16.3, browserslist@^4.6.0, browserslist@^4.6.4, browserslist@^4.8.5, browserslist@^4.9.1:
version "4.20.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf"
integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==
dependencies:
caniuse-lite "^1.0.30001043"
electron-to-chromium "^1.3.413"
node-releases "^1.1.53"
pkg-up "^2.0.0"
browserslist@^4.14.5, browserslist@^4.16.3, browserslist@^4.6.0:
version "4.16.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717"
dependencies:
caniuse-lite "^1.0.30001181"
colorette "^1.2.1"
electron-to-chromium "^1.3.649"
caniuse-lite "^1.0.30001332"
electron-to-chromium "^1.4.118"
escalade "^3.1.1"
node-releases "^1.1.70"
node-releases "^2.0.3"
picocolors "^1.0.0"
buffer-crc32@~0.2.3:
version "0.2.13"
@ -2504,8 +2506,9 @@ cache-base@^1.0.1:
unset-value "^1.0.0"
cached-path-relative@^1.0.0, cached-path-relative@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db"
version "1.1.0"
resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.1.0.tgz#865576dfef39c0d6a7defde794d078f5308e3ef3"
integrity sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==
cachedir@^2.3.0:
version "2.3.0"
@ -2565,13 +2568,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001039, caniuse-lite@^1.0.30001043:
version "1.0.30001048"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz#4bb4f1bc2eb304e5e1154da80b93dee3f1cf447e"
caniuse-lite@^1.0.30001181:
version "1.0.30001202"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001202.tgz#4cb3bd5e8a808e8cd89e4e66c549989bc8137201"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001039, caniuse-lite@^1.0.30001332:
version "1.0.30001332"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd"
integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==
caseless@~0.12.0:
version "0.12.0"
@ -2808,9 +2808,10 @@ codex-notifier@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895"
codex-tooltip@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.4.tgz#bb8c6e0fe7accc68ce79cdcb7c71bf7b4bf1317a"
codex-tooltip@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.5.tgz#ba25fd5b3a58ba2f73fd667c2b46987ffd1edef2"
integrity sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag==
coffeeify@3.0.1:
version "3.0.1"
@ -2855,8 +2856,9 @@ color-name@^1.0.0, color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
color-string@^1.5.2:
version "1.5.3"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
@ -2868,10 +2870,6 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.2"
colorette@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
colors@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
@ -3558,13 +3556,10 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
electron-to-chromium@^1.3.413:
version "1.3.418"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.418.tgz#840021191f466b803a873e154113620c9f53cec6"
electron-to-chromium@^1.3.649:
version "1.3.690"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.690.tgz#54df63ec42fba6b8e9e05fe4be52caeeedb6e634"
electron-to-chromium@^1.4.118:
version "1.4.122"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.122.tgz#56e518e8c4433876b01d4460eac0f653841ed510"
integrity sha512-VuLNxTIt8sBWIT2sd186xPd18Y8KcK8myLd9nMdSJOYZwFUxxbLVmX/T1VX+qqaytRlrYYQv39myxJdXtu7Ysw==
elegant-spinner@^1.0.1:
version "1.0.1"
@ -3679,6 +3674,7 @@ es6-promise@^2.3.0:
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-string-regexp@4.0.0:
version "4.0.0"
@ -4884,6 +4880,7 @@ is-arrayish@^0.2.1:
is-arrayish@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
is-binary-path@^1.0.0:
version "1.0.1"
@ -5805,8 +5802,9 @@ minimist-options@^4.0.1:
is-plain-obj "^1.1.0"
minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
minipass-collect@^1.0.2:
version "1.0.2"
@ -5919,8 +5917,9 @@ module-deps@^6.0.0, module-deps@^6.2.3:
xtend "^4.0.0"
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
version "2.29.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
move-concurrently@^1.0.1:
version "1.0.1"
@ -5958,8 +5957,9 @@ nanoid@3.1.20:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
nanoid@^3.1.22:
version "3.1.22"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
version "3.1.31"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.31.tgz#f5b58a1ce1b7604da5f0605757840598d8974dc6"
integrity sha512-ZivnJm0o9bb13p2Ot5CpgC2rQdzB9Uxm/mFZweqm5eMViqOJe3PV6LU2E30SiLgheesmcPrjquqraoolONSA0A==
nanomatch@^1.2.9:
version "1.2.13"
@ -6027,13 +6027,10 @@ node-preload@^0.2.1:
dependencies:
process-on-spawn "^1.0.0"
node-releases@^1.1.53:
version "1.1.53"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4"
node-releases@^1.1.70:
version "1.1.71"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb"
node-releases@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96"
integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw==
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
version "2.5.0"
@ -6469,6 +6466,11 @@ performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
@ -6505,12 +6507,6 @@ pkg-dir@^4.1.0:
dependencies:
find-up "^4.0.0"
pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
dependencies:
find-up "^2.1.0"
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -7818,6 +7814,7 @@ simple-concat@^1.0.0:
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
dependencies:
is-arrayish "^0.3.1"