Compare commits
25 Commits
a513e1079a
...
a5bea55d35
Author | SHA1 | Date |
---|---|---|
Ilya Maroz | a5bea55d35 | |
github-actions[bot] | 320a30afcc | |
Peter Savchenko | 509023e307 | |
Peter Savchenko | d56c07c3dc | |
Peter Savchenko | 1a72d2153b | |
github-actions[bot] | 04118b2d6e | |
Peter Savchenko | 73c9bdf40b | |
github-actions[bot] | a8026e6829 | |
Peter Savchenko | 0bf5007f6c | |
github-actions[bot] | 6088aaead6 | |
dependabot[bot] | cdaf10a58f | |
Peter Savchenko | e048c4d46b | |
github-actions[bot] | fb86d71828 | |
Valerii Varankin | 811a41366b | |
Peter Savchenko | 0d0f9e8de2 | |
azibodusi osain | 9b7da504e4 | |
github-actions[bot] | a1071b7229 | |
Peter Savchenko | b1b582b150 | |
github-actions[bot] | f0f8f8fcb5 | |
Peter Savchenko | 3272efc3f7 | |
github-actions[bot] | 19f3790cfd | |
Umang G. Patel | f659015be6 | |
ilyamore88 | 665d5d1388 | |
github-actions[bot] | cd06bfc9c5 | |
Tatiana Fomina | 581289c03e |
22
.eslintrc
|
@ -1,27 +1,7 @@
|
|||
{
|
||||
"extends": [
|
||||
"codex"
|
||||
"codex/ts"
|
||||
],
|
||||
"rules": {
|
||||
/**
|
||||
* Temporary suppress some errors. We need to fix them partially in next patches
|
||||
*/
|
||||
"import/no-duplicates": ["warn"],
|
||||
"@typescript-eslint/triple-slash-reference": ["off"],
|
||||
"jsdoc/no-undefined-types": ["warn", {"definedTypes": [
|
||||
"ConstructorOptions",
|
||||
"API",
|
||||
"BlockToolConstructable",
|
||||
"EditorConfig",
|
||||
"Tool",
|
||||
"ToolSettings"
|
||||
]}]
|
||||
},
|
||||
"settings": {
|
||||
"jsdoc": {
|
||||
"mode": "typescript"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"Node": true,
|
||||
"Range": true,
|
||||
|
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
# Setup node environment
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15
|
||||
node-version: 14.17.0
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
# Prepare, build and publish project
|
||||
|
|
|
@ -4,7 +4,7 @@ jobs:
|
|||
firefox:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: cypress/browsers:node14.16.0-chrome89-ff86
|
||||
image: cypress/browsers:node14.17.0-chrome88-ff89
|
||||
options: --user 1001
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
# Setup node environment
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 15
|
||||
node-version: 14.17.0
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
# Prepare, build and publish project
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"autofocused",
|
||||
"Behaviour",
|
||||
"cacheable",
|
||||
"childs",
|
||||
"codexteam",
|
||||
"colspan",
|
||||
"contenteditable",
|
||||
"contentless",
|
||||
"cssnano",
|
||||
"cssnext",
|
||||
"Debouncer",
|
||||
"devserver",
|
||||
"editorjs",
|
||||
"entrypoints",
|
||||
"Flippable",
|
||||
"GRAMMARLY",
|
||||
"hsablonniere",
|
||||
"intellij",
|
||||
"keydown",
|
||||
"keydowns",
|
||||
"Kilian",
|
||||
"mergeable",
|
||||
"movetostart",
|
||||
"nofollow",
|
||||
"opencollective",
|
||||
"preconfigured",
|
||||
"resetors",
|
||||
"rowspan",
|
||||
"selectall",
|
||||
"sometool",
|
||||
"stylelint",
|
||||
"textareas",
|
||||
"twitterwidget",
|
||||
"typeof",
|
||||
"viewports"
|
||||
]
|
||||
}
|
|
@ -1,5 +1,25 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
### 2.26.0
|
||||
|
||||
- `New` — *UI* — Block Tunes became vertical just like the Toolbox 🤩
|
||||
- `New` — *Block Tunes API* — Now `render()` method of a Block Tune can return config with just icon, label and callback instead of custom HTML. This improvement is a key to the new straightforward way of configuring tune's appearance in Block Tunes menu.
|
||||
- `New` — *Tools API* — As well as `render()` in `Tunes API`, Tool's `renderSettings()` now also supports new configuration format.
|
||||
- `New` — *UI* — Meet the new icons from [CodeX Icons](https://github.com/codex-team/icons) pack 🛍 💝
|
||||
- `New` — *BlocksAPI* — the `blocks.insert()` method now also have the optional `id` param. If passed, this id will be used instead of the generated one.
|
||||
- `Deprecated` — *Styles API* — CSS classes `.cdx-settings-button` and `.cdx-settings-button--active` are not recommended to use. Consider configuring your block settings with new JSON API instead.
|
||||
- `Fix` — Wrong element not highlighted anymore when popover opened.
|
||||
- `Fix` — When Tunes Menu open keydown events can not be handled inside plugins.
|
||||
- `Fix` — If a Tool specifies some tags to substitute on paste, all attributes of that tags will be removed before passing them to the tool. Possible XSS vulnerability fixed.
|
||||
- `Fix` — Pasting from Microsoft Word to Chrome (Mac OS) fixed. Now if there are no image-tools connected, regular text content will be pasted.
|
||||
- `Fix` — Workaround for the HTMLJanitor bug with Tables (https://github.com/guardian/html-janitor/issues/3) added
|
||||
- `Fix` — Toolbox shortcuts appearance and execution fixed [#2112](https://github.com/codex-team/editor.js/issues/2112)
|
||||
- `Fix` — Inline Tools click handling on mobile devices improved
|
||||
- `Improvement` — *Tools API* — `pasteConfig().tags` now support sanitizing configuration. It allows you to leave some explicitly specified attributes for pasted content.
|
||||
- `Improvement` — *CodeStyle* — [CodeX ESLint Config](https://github.com/codex-team/eslint-config) has bee updated. All ESLint/Spelling issues resolved
|
||||
- `Improvement` — *ToolsAPI* — The `icon` property of the `toolbox` getter became optional.
|
||||
|
||||
### 2.25.0
|
||||
|
||||
- `New` — *Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool! <br>
|
||||
|
@ -8,7 +28,7 @@ Due to that API changes: tool's `toolbox` getter now can return either a single
|
|||
|
||||
### 2.24.4
|
||||
|
||||
- `Fix` — Keyboard selection by word [2045](https://github.com/codex-team/editor.js/issues/2045)
|
||||
- `Fix` — Keyboard selection by word [#2045](https://github.com/codex-team/editor.js/issues/2045)
|
||||
|
||||
### 2.24.3
|
||||
|
||||
|
@ -102,7 +122,7 @@ Due to that API changes: tool's `toolbox` getter now can return either a single
|
|||
### 2.20.1
|
||||
|
||||
- `Fix` - Create a new block when clicked at the bottom [#1588](https://github.com/codex-team/editor.js/issues/1588).
|
||||
- `Fix` — Fix sanitisation problem with Inline Tools [#1631](https://github.com/codex-team/editor.js/issues/1631)
|
||||
- `Fix` — Fix sanitization problem with Inline Tools [#1631](https://github.com/codex-team/editor.js/issues/1631)
|
||||
- `Fix` — Fix copy in FireFox [1625](https://github.com/codex-team/editor.js/issues/1625)
|
||||
- `Refactoring` - The Sanitizer module is util now.
|
||||
- `Refactoring` - Tooltip module is util now.
|
||||
|
@ -157,7 +177,7 @@ Due to that API changes: tool's `toolbox` getter now can return either a single
|
|||
- `New` - Tool's `reset` static method added to the API to clean up any data added by Tool on initialization
|
||||
- `Improvements` - The `initialBlock` property of Editor config is deprecated. Use the `defaultBlock` instead. [#993](https://github.com/codex-team/editor.js/issues/993)
|
||||
- `Improvements` - BlockAPI `call()` method now returns the result of calling method, thus allowing it to expose arbitrary data as needed [#1205](https://github.com/codex-team/editor.js/pull/1205)
|
||||
- `Improvements` - Unuseful log about missed i18n section has been removed [#1269](https://github.com/codex-team/editor.js/issues/1269)
|
||||
- `Improvements` - Useless log about missed i18n section has been removed [#1269](https://github.com/codex-team/editor.js/issues/1269)
|
||||
- `Improvements` - Allowed to set `false` as `toolbox` config in order to hide Toolbox button [#1221](https://github.com/codex-team/editor.js/issues/1221)
|
||||
- `Fix` — Fix problem with types usage [#1183](https://github.com/codex-team/editor.js/issues/1183)
|
||||
- `Fix` - Fixed issue with Spam clicking the "Click to tune" button duplicates the icons on FireFox. [#1273](https://github.com/codex-team/editor.js/issues/1273)
|
||||
|
@ -168,7 +188,7 @@ Due to that API changes: tool's `toolbox` getter now can return either a single
|
|||
- `Fix` - Fixed issue with enter key in inputs and textareas [#920](https://github.com/codex-team/editor.js/issues/920)
|
||||
- `Fix` - blocks.getBlockByIndex() API method now returns void for indexes out of range [#1270](https://github.com/codex-team/editor.js/issues/1270)
|
||||
- `Fix` - Fixed the `Tab` key behavior when the caret is not set inside contenteditable element, but the block is selected [#1302](https://github.com/codex-team/editor.js/issues/1302).
|
||||
- `Fix` - Fixed the `onChange` callback issue. This method didn't be called for native inputs before some contentedtable element changed [#843](https://github.com/codex-team/editor.js/issues/843)
|
||||
- `Fix` - Fixed the `onChange` callback issue. This method didn't be called for native inputs before some contenteditable element changed [#843](https://github.com/codex-team/editor.js/issues/843)
|
||||
- `Fix` - Fixed the `onChange` callback issue. This method didn't be called after the callback throws an exception [#1339](https://github.com/codex-team/editor.js/issues/1339)
|
||||
- `Fix` - The internal `shortcut` getter of Tools classes will work now.
|
||||
- `Deprecated` — The Inline Tool `clear()` method is deprecated because the new instance of Inline Tools will be created on every showing of the Inline Toolbar
|
||||
|
@ -217,7 +237,7 @@ Due to that API changes: tool's `toolbox` getter now can return either a single
|
|||
|
||||
- `Fix` — Fix Firefox bug with incorrect height and cursor position of empty content editable elements [#947](https://github.com/codex-team/editor.js/issues/947) [#876](https://github.com/codex-team/editor.js/issues/876) [#608](https://github.com/codex-team/editor.js/issues/608) [#876](https://github.com/codex-team/editor.js/issues/876)
|
||||
- `Fix` — Set initial hidden Inline Toolbar position [#979](https://github.com/codex-team/editor.js/issues/979)
|
||||
- `Fix` — Fix issue with CodeX.Toolips TypeScript definitions [#978](https://github.com/codex-team/editor.js/issues/978)
|
||||
- `Fix` — Fix issue with CodeX.Tooltips TypeScript definitions [#978](https://github.com/codex-team/editor.js/issues/978)
|
||||
- `Fix` — Fix some issues with Inline and Tunes toolbars.
|
||||
- `Fix` - Fix `minHeight` option with zero-value issue [#724](https://github.com/codex-team/editor.js/issues/724)
|
||||
- `Improvements` — Disable Conversion Toolbar if there are no Tools to convert [#984](https://github.com/codex-team/editor.js/issues/984)
|
||||
|
@ -330,7 +350,7 @@ Due to that API changes: tool's `toolbox` getter now can return either a single
|
|||
|
||||
### 2.11.5
|
||||
|
||||
- `Fix` *RectangeSelection* — Redesign of the scrolling zones
|
||||
- `Fix` *RectangleSelection* — Redesign of the scrolling zones
|
||||
|
||||
### 2.11.4
|
||||
|
||||
|
@ -346,7 +366,7 @@ Due to that API changes: tool's `toolbox` getter now can return either a single
|
|||
|
||||
### 2.11.1
|
||||
|
||||
- `Fix` *RectangeSelection* — Selection is available only for the main mouse button
|
||||
- `Fix` *RectangleSelection* — Selection is available only for the main mouse button
|
||||
|
||||
### 2.11.0
|
||||
|
||||
|
@ -378,7 +398,7 @@ Due to that API changes: tool's `toolbox` getter now can return either a single
|
|||
|
||||
### 2.9.0
|
||||
|
||||
- `New` *RectangeSelection* — Ability to select Block or several Blocks with mouse
|
||||
- `New` *RectangleSelection* — Ability to select Block or several Blocks with mouse
|
||||
|
||||
### 2.8.1
|
||||
|
||||
|
@ -386,7 +406,7 @@ Due to that API changes: tool's `toolbox` getter now can return either a single
|
|||
|
||||
### 2.8.0
|
||||
|
||||
- `Imporvements` *API* — Added [API methods](api.md#caretapi) to manage caret position
|
||||
- `Improvements` *API* — Added [API methods](api.md#caretapi) to manage caret position
|
||||
|
||||
### 2.7.32
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ To handle pasted HTML elements object returned from `pasteConfig` getter should
|
|||
|
||||
For correct work you MUST provide `onPaste` handler at least for `defaultBlock` Tool.
|
||||
|
||||
> Example
|
||||
#### Example
|
||||
|
||||
Header Tool can handle `H1`-`H6` tags using paste handling API
|
||||
|
||||
|
@ -163,7 +163,27 @@ static get pasteConfig() {
|
|||
}
|
||||
```
|
||||
|
||||
> Same tag can be handled by one (first specified) Tool only.
|
||||
**Note. Same tag can be handled by one (first specified) Tool only.**
|
||||
|
||||
**Note. All attributes of pasted tag will be removed. To leave some attribute, you should explicitly specify them. Se below**
|
||||
|
||||
Let's suppose you want to leave the 'src' attribute when handle pasting of the `img` tags. Your config should look like this:
|
||||
|
||||
```javascript
|
||||
static get pasteConfig() {
|
||||
return {
|
||||
tags: [
|
||||
{
|
||||
img: {
|
||||
src: true
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Read more](https://editorjs.io/sanitizer) about the sanitizing configuration.
|
||||
|
||||
### RegExp patterns handling
|
||||
|
||||
|
|
|
@ -194,9 +194,11 @@
|
|||
"toolbar": {
|
||||
"toolbox": {
|
||||
"Add": "Добавить",
|
||||
"Filter": "Поиск",
|
||||
"Nothing found": "Ничего не найдено"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"Filter": "Поиск",
|
||||
"Nothing found": "Ничего не найдено"
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 35742f01ae5875d442b145121d3c9b71b23aea56
|
||||
Subproject commit 23de06be69bb9e636a2278b0d54f8c2d85d7ae13
|
|
@ -1 +1 @@
|
|||
Subproject commit 585bca271f7696cd17533fa5877d1f72b3a03d2e
|
||||
Subproject commit 056ff5e52677d239dfe73b9ddc6e074474a54a63
|
|
@ -1 +1 @@
|
|||
Subproject commit 0fc365ef256decb8f765fb72b060d5bef9254aa3
|
||||
Subproject commit 13372270afdee5dfb0f1509b491888db7f06a8e4
|
|
@ -1 +1 @@
|
|||
Subproject commit 9add95389afca0711c05260a92283fae8eb209eb
|
||||
Subproject commit 6da4d45354b8b05b384ea175d7685c733c80a9c8
|
|
@ -1 +1 @@
|
|||
Subproject commit ad0d9012d149e3ca4b41a5ce096b31767cc8c1fd
|
||||
Subproject commit a45d7329f877552bfa6c3c4e10c9bdd13f3e9e31
|
20
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@editorjs/editorjs",
|
||||
"version": "2.25.0",
|
||||
"version": "2.26.0",
|
||||
"description": "Editor.js — Native JS, based on API and Open Source",
|
||||
"main": "dist/editor.js",
|
||||
"types": "./types/index.d.ts",
|
||||
|
@ -13,15 +13,14 @@
|
|||
],
|
||||
"scripts": {
|
||||
"clear": "rimraf dist && mkdirp dist",
|
||||
"build": "yarn clear && yarn svg && yarn build:webpack:prod",
|
||||
"build:dev": "yarn clear && yarn svg && yarn build:webpack:dev",
|
||||
"build": "yarn clear && yarn build:webpack:prod",
|
||||
"build:dev": "yarn clear && yarn build:webpack:dev",
|
||||
"build:webpack:dev": "webpack --mode development --progress --display-error-details --display-entrypoints --watch",
|
||||
"build:webpack:prod": "webpack --mode production",
|
||||
"lint": "eslint src/ --ext .ts && yarn lint:tests",
|
||||
"lint:errors": "eslint src/ --ext .ts --quiet",
|
||||
"lint:fix": "eslint src/ --ext .ts --fix",
|
||||
"lint:tests": "eslint test/ --ext .ts",
|
||||
"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\"",
|
||||
|
@ -67,13 +66,12 @@
|
|||
"cssnano": "^4.1.10",
|
||||
"cypress": "^6.8.0",
|
||||
"cypress-intellij-reporter": "^0.0.6",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-codex": "^1.3.3",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-codex": "^1.7.1",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-chai-friendly": "^0.6.0",
|
||||
"eslint-plugin-cypress": "^2.11.2",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"html-janitor": "^2.0.4",
|
||||
"license-webpack-plugin": "^2.1.4",
|
||||
"mkdirp": "^1.0.4",
|
||||
"postcss-apply": "^0.12.0",
|
||||
|
@ -82,10 +80,8 @@
|
|||
"postcss-nested": "^4.1.2",
|
||||
"postcss-nested-ancestors": "^2.0.0",
|
||||
"postcss-preset-env": "^6.6.0",
|
||||
"raw-loader": "^4.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"stylelint": "^13.3.3",
|
||||
"svg-sprite-generator": "^0.0.7",
|
||||
"terser-webpack-plugin": "^2.3.6",
|
||||
"ts-loader": "^7.0.1",
|
||||
"tslint": "^6.1.1",
|
||||
|
@ -98,8 +94,10 @@
|
|||
"url": "https://opencollective.com/editorjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codexteam/icons": "^0.0.4",
|
||||
"codex-notifier": "^1.1.2",
|
||||
"codex-tooltip": "^1.0.5",
|
||||
"html-janitor": "^2.0.4",
|
||||
"nanoid": "^3.1.22"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path transform="matrix(1 0 0 -1 0 14)" d="M8.024 4.1v8.6a1.125 1.125 0 0 1-2.25 0V4.1L2.18 7.695A1.125 1.125 0 1 1 .59 6.104L6.103.588c.44-.439 1.151-.439 1.59 0l5.516 5.516a1.125 1.125 0 0 1-1.59 1.59L8.023 4.1z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 353 B |
|
@ -1,3 +0,0 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M8.024 4.1v8.6a1.125 1.125 0 0 1-2.25 0V4.1L2.18 7.695A1.125 1.125 0 1 1 .59 6.104L6.103.588c.44-.439 1.151-.439 1.59 0l5.516 5.516a1.125 1.125 0 0 1-1.59 1.59L8.023 4.1z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 321 B |
|
@ -1 +0,0 @@
|
|||
<svg width="12" height="14" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M5.997 14H1.72c-.618 0-1.058-.138-1.323-.415C.132 13.308 0 12.867 0 12.262V1.738C0 1.121.135.676.406.406.676.136 1.114 0 1.719 0h4.536c.669 0 1.248.041 1.738.124.49.083.93.242 1.318.478a3.458 3.458 0 0 1 1.461 1.752c.134.366.2.753.2 1.16 0 1.401-.7 2.426-2.1 3.075 1.84.586 2.76 1.726 2.76 3.42 0 .782-.2 1.487-.602 2.114a3.61 3.61 0 0 1-1.623 1.39 5.772 5.772 0 0 1-1.471.377c-.554.073-1.2.11-1.939.11zm-.21-6.217h-2.95v4.087h3.046c1.916 0 2.874-.69 2.874-2.072 0-.707-.248-1.22-.745-1.537-.496-.319-1.238-.478-2.225-.478zM2.837 2.13v3.619h2.597c.707 0 1.252-.067 1.638-.2.385-.134.68-.389.883-.765.16-.267.239-.566.239-.897 0-.707-.252-1.176-.755-1.409-.503-.232-1.27-.348-2.301-.348H2.836z"/></svg>
|
Before Width: | Height: | Size: 794 B |
|
@ -1,3 +0,0 @@
|
|||
<svg width="237" height="237" viewBox="0 0 237 237" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path transform="rotate(45 280.675 51.325)" d="M191 191V73c0-5.523 4.477-10 10-10h25c5.523 0 10 4.477 10 10v118h118c5.523 0 10 4.477 10 10v25c0 5.523-4.477 10-10 10H236v118c0 5.523-4.477 10-10 10h-25c-5.523 0-10-4.477-10-10V236H73c-5.523 0-10-4.477-10-10v-25c0-5.523 4.477-10 10-10h118z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 430 B |
|
@ -1,10 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<g transform="translate(4 1.5)" fill-rule="evenodd">
|
||||
<circle cx="1.3" cy="1.3" r="1.3"/>
|
||||
<circle cx="6.5" cy="1.3" r="1.3"/>
|
||||
<circle cx="6.5" cy="6.5" r="1.3"/>
|
||||
<circle cx="1.3" cy="6.5" r="1.3"/>
|
||||
<circle cx="6.5" cy="11.7" r="1.3"/>
|
||||
<circle cx="1.3" cy="11.7" r="1.3"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 372 B |
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 11">
|
||||
<path d="M3.289 4.17L2.164 9.713c-.078.384-.238.674-.48.87-.243.198-.52.296-.831.296-.312 0-.545-.1-.699-.302-.153-.202-.192-.49-.116-.864L1.15 4.225c.077-.38.232-.665.466-.857a1.25 1.25 0 01.818-.288c.312 0 .55.096.713.288.163.192.21.46.141.801zm-.667-2.09c-.295 0-.53-.09-.706-.273-.176-.181-.233-.439-.173-.77.055-.302.207-.55.457-.745C2.45.097 2.716 0 3 0c.273 0 .5.088.68.265.179.176.238.434.177.771-.06.327-.21.583-.45.767-.24.185-.502.277-.785.277z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 530 B |
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 10">
|
||||
<path d="M6 0v2H5a3 3 0 000 6h1v2H5A5 5 0 115 0h1zm2 0h1a5 5 0 110 10H8V8h1a3 3 0 000-6H8V0zM5 4h4a1 1 0 110 2H5a1 1 0 110-2z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 199 B |
|
@ -1,6 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<g transform="translate(1 1.5)" fill-rule="evenodd">
|
||||
<rect x="6" width="2" height="13" rx="1"/>
|
||||
<rect x=".5" y="5.5" width="13" height="2" rx="1"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 233 B |
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52">
|
||||
<path fill="#D76B6B" fill-rule="nonzero" d="M26 52C11.64 52 0 40.36 0 26S11.64 0 26 0s26 11.64 26 26-11.64 26-26 26zm0-3.25c12.564 0 22.75-10.186 22.75-22.75S38.564 3.25 26 3.25 3.25 13.436 3.25 26 13.436 48.75 26 48.75zM15.708 33.042a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm23.834 0a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm-15.875 5.452a1.083 1.083 0 1 1-1.834-1.155c1.331-2.114 3.49-3.179 6.334-3.179 2.844 0 5.002 1.065 6.333 3.18a1.083 1.083 0 1 1-1.833 1.154c-.913-1.45-2.366-2.167-4.5-2.167s-3.587.717-4.5 2.167z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 643 B |
|
@ -1,3 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 289 B |
|
@ -1,3 +0,0 @@
|
|||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 9.294a.792.792 0 01-.562-.232L2.233 5.356a.794.794 0 011.123-1.123L6.5 7.377l3.144-3.144a.794.794 0 011.123 1.123L7.062 9.062a.792.792 0 01-.562.232z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 240 B |
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 11">
|
||||
<path d="M13.073 2.099l-1.448 1.448A3 3 0 009 2H8V0h1c1.68 0 3.166.828 4.073 2.099zM6.929 4l-.879.879L7.172 6H5a1 1 0 110-2h1.929zM6 0v2H5a3 3 0 100 6h1v2H5A5 5 0 115 0h1zm6.414 7l2.122 2.121-1.415 1.415L11 8.414l-2.121 2.122L7.464 9.12 9.586 7 7.464 4.879 8.88 3.464 11 5.586l2.121-2.122 1.415 1.415L12.414 7z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 384 B |
|
@ -19,7 +19,6 @@ declare const VERSION: string;
|
|||
* Short Description (눈_눈;)
|
||||
*
|
||||
* @version 2.18.0
|
||||
*
|
||||
* @license Apache-2.0
|
||||
* @author CodeX-Team <https://ifmo.su>
|
||||
*/
|
||||
|
|
|
@ -14,12 +14,11 @@ export type ModuleNodes = object;
|
|||
* @abstract
|
||||
* @class Module
|
||||
* @classdesc All modules inherits from this class.
|
||||
*
|
||||
* @typedef {Module} Module
|
||||
* @property {object} config - Editor user settings
|
||||
* @property {EditorModules} Editor - List of Editor modules
|
||||
*/
|
||||
export default class Module<T extends ModuleNodes = {}> {
|
||||
export default class Module<T extends ModuleNodes = Record<string, HTMLElement>> {
|
||||
/**
|
||||
* Each module can provide some UI elements that will be stored in this property
|
||||
*/
|
||||
|
@ -92,8 +91,9 @@ export default class Module<T extends ModuleNodes = {}> {
|
|||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {ModuleConfig} - Module config
|
||||
* @param options - Module options
|
||||
* @param options.config - Module config
|
||||
* @param options.eventsDispatcher - Common event bus
|
||||
*/
|
||||
constructor({ config, eventsDispatcher }: ModuleConfig) {
|
||||
if (new.target === Module) {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
/**
|
||||
* @class DeleteTune
|
||||
* @classdesc Editor's default tune that moves up selected block
|
||||
*
|
||||
* @copyright <CodeX Team> 2018
|
||||
*/
|
||||
import { API, BlockTune } from '../../../types';
|
||||
import $ from '../dom';
|
||||
import { API, BlockTune, PopoverItem } from '../../../types';
|
||||
import { IconCross } from '@codexteam/icons';
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -23,32 +22,6 @@ export default class DeleteTune implements BlockTune {
|
|||
*/
|
||||
private readonly api: API;
|
||||
|
||||
/**
|
||||
* Styles
|
||||
*/
|
||||
private CSS = {
|
||||
button: 'ce-settings__button',
|
||||
buttonDelete: 'ce-settings__button--delete',
|
||||
buttonConfirm: 'ce-settings__button--confirm',
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete confirmation
|
||||
*/
|
||||
private needConfirmation: boolean;
|
||||
|
||||
/**
|
||||
* set false confirmation state
|
||||
*/
|
||||
private readonly resetConfirmation: () => void;
|
||||
|
||||
/**
|
||||
* Tune nodes
|
||||
*/
|
||||
private nodes: {button: HTMLElement} = {
|
||||
button: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* DeleteTune constructor
|
||||
*
|
||||
|
@ -56,75 +29,27 @@ export default class DeleteTune implements BlockTune {
|
|||
*/
|
||||
constructor({ api }) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
this.resetConfirmation = (): void => {
|
||||
this.setConfirmation(false);
|
||||
/**
|
||||
* Tune's appearance in block settings menu
|
||||
*/
|
||||
public render(): PopoverItem {
|
||||
return {
|
||||
icon: IconCross,
|
||||
label: this.api.i18n.t('Delete'),
|
||||
name: 'delete',
|
||||
confirmation: {
|
||||
label: this.api.i18n.t('Click to delete'),
|
||||
onActivate: (): void => this.handleClick(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create "Delete" button and add click event listener
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
public render(): HTMLElement {
|
||||
this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {});
|
||||
this.nodes.button.appendChild($.svg('cross', 12, 12));
|
||||
this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false);
|
||||
|
||||
/**
|
||||
* Enable tooltip module
|
||||
*/
|
||||
this.api.tooltip.onHover(this.nodes.button, this.api.i18n.t('Delete'), {
|
||||
hidingDelay: 300,
|
||||
});
|
||||
|
||||
return this.nodes.button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete block conditions passed
|
||||
*
|
||||
* @param {MouseEvent} event - click event
|
||||
*/
|
||||
public handleClick(event: MouseEvent): void {
|
||||
/**
|
||||
* if block is not waiting the confirmation, subscribe on block-settings-closing event to reset
|
||||
* otherwise delete block
|
||||
*/
|
||||
if (!this.needConfirmation) {
|
||||
this.setConfirmation(true);
|
||||
|
||||
/**
|
||||
* Subscribe on event.
|
||||
* When toolbar block settings is closed but block deletion is not confirmed,
|
||||
* then reset confirmation state
|
||||
*/
|
||||
this.api.events.on('block-settings-closed', this.resetConfirmation);
|
||||
} else {
|
||||
/**
|
||||
* Unsubscribe from block-settings closing event
|
||||
*/
|
||||
this.api.events.off('block-settings-closed', this.resetConfirmation);
|
||||
|
||||
this.api.blocks.delete();
|
||||
this.api.toolbar.close();
|
||||
this.api.tooltip.hide();
|
||||
|
||||
/**
|
||||
* Prevent firing ui~documentClicked that can drop currentBlock pointer
|
||||
*/
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* change tune state
|
||||
*
|
||||
* @param {boolean} state - delete confirmation state
|
||||
*/
|
||||
private setConfirmation(state: boolean): void {
|
||||
this.needConfirmation = state;
|
||||
this.nodes.button.classList.add(this.CSS.buttonConfirm);
|
||||
public handleClick(): void {
|
||||
this.api.blocks.delete();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
/**
|
||||
* @class MoveDownTune
|
||||
* @classdesc Editor's default tune - Moves down highlighted block
|
||||
*
|
||||
* @copyright <CodeX Team> 2018
|
||||
*/
|
||||
|
||||
import $ from '../dom';
|
||||
import { API, BlockTune } from '../../../types';
|
||||
import { API, BlockTune, PopoverItem } from '../../../types';
|
||||
import Popover from '../utils/popover';
|
||||
import { IconChevronDown } from '@codexteam/icons';
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -26,12 +27,8 @@ export default class MoveDownTune implements BlockTune {
|
|||
|
||||
/**
|
||||
* Styles
|
||||
*
|
||||
* @type {{wrapper: string}}
|
||||
*/
|
||||
private CSS = {
|
||||
button: 'ce-settings__button',
|
||||
wrapper: 'ce-tune-move-down',
|
||||
animation: 'wobble',
|
||||
};
|
||||
|
||||
|
@ -45,47 +42,37 @@ export default class MoveDownTune implements BlockTune {
|
|||
}
|
||||
|
||||
/**
|
||||
* Return 'move down' button
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
* Tune's appearance in block settings menu
|
||||
*/
|
||||
public render(): HTMLElement {
|
||||
const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
|
||||
|
||||
moveDownButton.appendChild($.svg('arrow-down', 14, 14));
|
||||
this.api.listeners.on(
|
||||
moveDownButton,
|
||||
'click',
|
||||
(event) => this.handleClick(event as MouseEvent, moveDownButton),
|
||||
false
|
||||
);
|
||||
|
||||
/**
|
||||
* Enable tooltip module on button
|
||||
*/
|
||||
this.api.tooltip.onHover(moveDownButton, this.api.i18n.t('Move down'), {
|
||||
hidingDelay: 300,
|
||||
});
|
||||
|
||||
return moveDownButton;
|
||||
public render(): PopoverItem {
|
||||
return {
|
||||
icon: IconChevronDown,
|
||||
label: this.api.i18n.t('Move down'),
|
||||
onActivate: (item, event): void => this.handleClick(event),
|
||||
name: 'move-down',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clicks on 'move down' button
|
||||
*
|
||||
* @param {MouseEvent} event - click event
|
||||
* @param {HTMLElement} button - clicked button
|
||||
* @param event - click event
|
||||
*/
|
||||
public handleClick(event: MouseEvent, button: HTMLElement): void {
|
||||
public handleClick(event: MouseEvent): void {
|
||||
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
||||
const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
|
||||
|
||||
// If Block is last do nothing
|
||||
if (!nextBlock) {
|
||||
const button = (event.target as HTMLElement)
|
||||
.closest('.' + Popover.CSS.item)
|
||||
.querySelector('.' + Popover.CSS.itemIcon);
|
||||
|
||||
button.classList.add(this.CSS.animation);
|
||||
|
||||
window.setTimeout(() => {
|
||||
button.classList.remove(this.CSS.animation);
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 500);
|
||||
|
||||
return;
|
||||
|
@ -110,8 +97,5 @@ export default class MoveDownTune implements BlockTune {
|
|||
this.api.blocks.move(currentBlockIndex + 1);
|
||||
|
||||
this.api.toolbar.toggleBlockSettings(true);
|
||||
|
||||
/** Hide the Tooltip */
|
||||
this.api.tooltip.hide();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* @class MoveUpTune
|
||||
* @classdesc Editor's default tune that moves up selected block
|
||||
*
|
||||
* @copyright <CodeX Team> 2018
|
||||
*/
|
||||
import $ from '../dom';
|
||||
import { API, BlockTune } from '../../../types';
|
||||
import { API, BlockTune, PopoverItem } from '../../../types';
|
||||
import Popover from '../../components/utils/popover';
|
||||
import { IconChevronUp } from '@codexteam/icons';
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -25,12 +25,8 @@ export default class MoveUpTune implements BlockTune {
|
|||
|
||||
/**
|
||||
* Styles
|
||||
*
|
||||
* @type {{wrapper: string}}
|
||||
*/
|
||||
private CSS = {
|
||||
button: 'ce-settings__button',
|
||||
wrapper: 'ce-tune-move-up',
|
||||
animation: 'wobble',
|
||||
};
|
||||
|
||||
|
@ -44,47 +40,37 @@ export default class MoveUpTune implements BlockTune {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create "MoveUp" button and add click event listener
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
* Tune's appearance in block settings menu
|
||||
*/
|
||||
public render(): HTMLElement {
|
||||
const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
|
||||
|
||||
moveUpButton.appendChild($.svg('arrow-up', 14, 14));
|
||||
this.api.listeners.on(
|
||||
moveUpButton,
|
||||
'click',
|
||||
(event) => this.handleClick(event as MouseEvent, moveUpButton),
|
||||
false
|
||||
);
|
||||
|
||||
/**
|
||||
* Enable tooltip module on button
|
||||
*/
|
||||
this.api.tooltip.onHover(moveUpButton, this.api.i18n.t('Move up'), {
|
||||
hidingDelay: 300,
|
||||
});
|
||||
|
||||
return moveUpButton;
|
||||
public render(): PopoverItem {
|
||||
return {
|
||||
icon: IconChevronUp,
|
||||
label: this.api.i18n.t('Move up'),
|
||||
onActivate: (item, e): void => this.handleClick(e),
|
||||
name: 'move-up',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Move current block up
|
||||
*
|
||||
* @param {MouseEvent} event - click event
|
||||
* @param {HTMLElement} button - clicked button
|
||||
*/
|
||||
public handleClick(event: MouseEvent, button: HTMLElement): void {
|
||||
public handleClick(event: MouseEvent): void {
|
||||
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
|
||||
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
|
||||
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
|
||||
|
||||
if (currentBlockIndex === 0 || !currentBlock || !previousBlock) {
|
||||
const button = (event.target as HTMLElement)
|
||||
.closest('.' + Popover.CSS.item)
|
||||
.querySelector('.' + Popover.CSS.itemIcon);
|
||||
|
||||
button.classList.add(this.CSS.animation);
|
||||
|
||||
window.setTimeout(() => {
|
||||
button.classList.remove(this.CSS.animation);
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 500);
|
||||
|
||||
return;
|
||||
|
@ -118,8 +104,5 @@ export default class MoveUpTune implements BlockTune {
|
|||
this.api.blocks.move(currentBlockIndex - 1);
|
||||
|
||||
this.api.toolbar.toggleBlockSettings(true);
|
||||
|
||||
/** Hide the Tooltip */
|
||||
this.api.tooltip.hide();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { BlockAPI as BlockAPIInterface } from '../../../types/api';
|
|||
* Constructs new BlockAPI object
|
||||
*
|
||||
* @class
|
||||
*
|
||||
* @param {Block} block - Block to expose
|
||||
*/
|
||||
function BlockAPI(
|
||||
|
@ -90,7 +89,6 @@ function BlockAPI(
|
|||
*
|
||||
* @param {string} methodName - method to call
|
||||
* @param {object} param - object with parameters
|
||||
*
|
||||
* @returns {unknown}
|
||||
*/
|
||||
call(methodName: string, param?: object): unknown {
|
||||
|
@ -110,7 +108,6 @@ function BlockAPI(
|
|||
* Validate Block data
|
||||
*
|
||||
* @param {BlockToolData} data - data to validate
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
validate(data: BlockToolData): Promise<boolean> {
|
||||
|
|
|
@ -5,7 +5,8 @@ import {
|
|||
BlockTune as IBlockTune,
|
||||
SanitizerConfig,
|
||||
ToolConfig,
|
||||
ToolboxConfigEntry
|
||||
ToolboxConfigEntry,
|
||||
PopoverItem
|
||||
} from '../../../types';
|
||||
|
||||
import { SavedData } from '../../../types/data-formats';
|
||||
|
@ -59,10 +60,8 @@ interface BlockConstructorOptions {
|
|||
/**
|
||||
* @class Block
|
||||
* @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool
|
||||
*
|
||||
* @property {BlockTool} tool — current block tool (Paragraph, for example)
|
||||
* @property {object} CSS — block`s css classes
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -73,11 +72,13 @@ export enum BlockToolAPI {
|
|||
* @todo remove method in 3.0.0
|
||||
* @deprecated — use 'rendered' hook instead
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
APPEND_CALLBACK = 'appendCallback',
|
||||
RENDERED = 'rendered',
|
||||
MOVED = 'moved',
|
||||
UPDATED = 'updated',
|
||||
REMOVED = 'removed',
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ON_PASTE = 'onPaste',
|
||||
}
|
||||
|
||||
|
@ -88,7 +89,6 @@ type BlockEvents = 'didMutated';
|
|||
|
||||
/**
|
||||
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
|
||||
*
|
||||
* @property {BlockTool} tool - Tool instance
|
||||
* @property {HTMLElement} holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class
|
||||
* @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function
|
||||
|
@ -243,7 +243,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
* @param {object} options - block constructor options
|
||||
* @param {string} [options.id] - block's id. Will be generated if omitted.
|
||||
* @param {BlockToolData} options.data - Tool's initial data
|
||||
* @param {BlockToolConstructable} options.tool — block's tool
|
||||
* @param {BlockTool} options.tool — block's tool
|
||||
* @param options.api - Editor API module for pass it to the Block Tunes
|
||||
* @param {boolean} options.readOnly - Read-Only flag
|
||||
*/
|
||||
|
@ -280,7 +280,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Find and return all editable elements (contenteditables and native inputs) in the Tool HTML
|
||||
* Find and return all editable elements (contenteditable and native inputs) in the Tool HTML
|
||||
*
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
|
@ -395,7 +395,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
|
||||
/**
|
||||
* is block mergeable
|
||||
* We plugin have merge function then we call it mergable
|
||||
* We plugin have merge function then we call it mergeable
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
@ -416,7 +416,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if block has a media content such as images, iframes and other
|
||||
* Check if block has a media content such as images, iframe and other
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
@ -486,7 +486,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
/**
|
||||
* Set stretched state
|
||||
*
|
||||
* @param {boolean} state - 'true' to enable, 'false' to disable stretched statte
|
||||
* @param {boolean} state - 'true' to enable, 'false' to disable stretched state
|
||||
*/
|
||||
public set stretched(state: boolean) {
|
||||
this.holder.classList.toggle(Block.CSS.wrapperStretched, state);
|
||||
|
@ -618,7 +618,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
_.log(`Saving proccess for ${this.name} tool failed due to the ${error}`, 'log', 'red');
|
||||
_.log(`Saving process for ${this.name} tool failed due to the ${error}`, 'log', 'red');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -627,7 +627,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
* Tool's validation method is optional
|
||||
*
|
||||
* @description Method returns true|false whether data passed the validation or not
|
||||
*
|
||||
* @param {BlockToolData} data - data to validate
|
||||
* @returns {Promise<boolean>} valid
|
||||
*/
|
||||
|
@ -642,22 +641,33 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Enumerates initialized tunes and returns fragment that can be appended to the toolbars area
|
||||
*
|
||||
* @returns {DocumentFragment[]}
|
||||
* Returns data to render in tunes menu.
|
||||
* Splits block tunes settings into 2 groups: popover items and custom html.
|
||||
*/
|
||||
public renderTunes(): [DocumentFragment, DocumentFragment] {
|
||||
const tunesElement = document.createDocumentFragment();
|
||||
const defaultTunesElement = document.createDocumentFragment();
|
||||
public getTunes(): [PopoverItem[], HTMLElement] {
|
||||
const customHtmlTunesContainer = document.createElement('div');
|
||||
const tunesItems: PopoverItem[] = [];
|
||||
|
||||
this.tunesInstances.forEach((tune) => {
|
||||
$.append(tunesElement, tune.render());
|
||||
});
|
||||
this.defaultTunesInstances.forEach((tune) => {
|
||||
$.append(defaultTunesElement, tune.render());
|
||||
/** Tool's tunes: may be defined as return value of optional renderSettings method */
|
||||
const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : [];
|
||||
|
||||
/** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */
|
||||
const commonTunes = [
|
||||
...this.defaultTunesInstances.values(),
|
||||
...this.tunesInstances.values(),
|
||||
].map(tuneInstance => tuneInstance.render());
|
||||
|
||||
[tunesDefinedInTool, commonTunes].flat().forEach(rendered => {
|
||||
if ($.isElement(rendered)) {
|
||||
customHtmlTunesContainer.appendChild(rendered);
|
||||
} else if (Array.isArray(rendered)) {
|
||||
tunesItems.push(...rendered);
|
||||
} else {
|
||||
tunesItems.push(rendered);
|
||||
}
|
||||
});
|
||||
|
||||
return [tunesElement, defaultTunesElement];
|
||||
return [tunesItems, customHtmlTunesContainer];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -726,15 +736,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Tool instance renderSettings method
|
||||
*/
|
||||
public renderSettings(): HTMLElement | undefined {
|
||||
if (_.isFunction(this.toolInstance.renderSettings)) {
|
||||
return this.toolInstance.renderSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool could specify several entries to be displayed at the Toolbox (for example, "Heading 1", "Heading 2", "Heading 3")
|
||||
* This method returns the entry that is related to the Block (depended on the Block data)
|
||||
|
@ -852,10 +853,10 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
* Update current input
|
||||
*/
|
||||
this.updateCurrentInput();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds focus event listeners to all inputs and contentEditables
|
||||
* Adds focus event listeners to all inputs and contenteditable
|
||||
*/
|
||||
private addInputEvents(): void {
|
||||
this.inputs.forEach(input => {
|
||||
|
@ -871,7 +872,7 @@ export default class Block extends EventsDispatcher<BlockEvents> {
|
|||
}
|
||||
|
||||
/**
|
||||
* removes focus event listeners from all inputs and contentEditables
|
||||
* removes focus event listeners from all inputs and contenteditable
|
||||
*/
|
||||
private removeInputEvents(): void {
|
||||
this.inputs.forEach(input => {
|
||||
|
|
|
@ -6,11 +6,8 @@ import { MoveEvent } from '../../types/tools';
|
|||
/**
|
||||
* @class Blocks
|
||||
* @classdesc Class to work with Block instances array
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @property {HTMLElement} workingArea — editor`s working node
|
||||
*
|
||||
*/
|
||||
export default class Blocks {
|
||||
/**
|
||||
|
@ -25,7 +22,6 @@ export default class Blocks {
|
|||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {HTMLElement} workingArea — editor`s working node
|
||||
*/
|
||||
constructor(workingArea: HTMLElement) {
|
||||
|
@ -65,7 +61,6 @@ export default class Blocks {
|
|||
*
|
||||
* @example
|
||||
* blocks[0] = new Block(...)
|
||||
*
|
||||
* @param {Blocks} instance — Blocks instance
|
||||
* @param {PropertyKey} property — block index or any Blocks class property key to set
|
||||
* @param {Block} value — value to set
|
||||
|
@ -257,7 +252,6 @@ export default class Blocks {
|
|||
* Insert Block after passed target
|
||||
*
|
||||
* @todo decide if this method is necessary
|
||||
*
|
||||
* @param {Block} targetBlock — target after which Block should be inserted
|
||||
* @param {Block} newBlock — Block to insert
|
||||
*/
|
||||
|
|
|
@ -30,12 +30,9 @@ contextRequire.keys().forEach((filename) => {
|
|||
|
||||
/**
|
||||
* @class Core
|
||||
*
|
||||
* @classdesc Editor.js core class
|
||||
*
|
||||
* @property {EditorConfig} config - all settings
|
||||
* @property {EditorModules} moduleInstances - constructed editor components
|
||||
*
|
||||
* @type {Core}
|
||||
*/
|
||||
export default class Core {
|
||||
|
@ -61,7 +58,6 @@ export default class Core {
|
|||
|
||||
/**
|
||||
* @param {EditorConfig} config - user configuration
|
||||
*
|
||||
*/
|
||||
constructor(config?: EditorConfig|string) {
|
||||
/**
|
||||
|
@ -103,6 +99,7 @@ export default class Core {
|
|||
* Resolve this.isReady promise
|
||||
*/
|
||||
onReady();
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 500);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -173,6 +170,7 @@ export default class Core {
|
|||
*
|
||||
* @type {number}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
this.config.minHeight = this.config.minHeight !== undefined ? this.config.minHeight : 300;
|
||||
|
||||
/**
|
||||
|
|
|
@ -45,15 +45,14 @@ export default class Dom {
|
|||
}
|
||||
|
||||
/**
|
||||
* Helper for making Elements with classname and attributes
|
||||
* Helper for making Elements with class name and attributes
|
||||
*
|
||||
* @param {string} tagName - new Element tag name
|
||||
* @param {string[]|string} [classNames] - list or name of CSS classname(s)
|
||||
* @param {string[]|string} [classNames] - list or name of CSS class name(s)
|
||||
* @param {object} [attributes] - any attributes
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
public static make(tagName: string, classNames: string|string[] = null, attributes: object = {}): HTMLElement {
|
||||
public static make(tagName: string, classNames: string | string[] = null, attributes: object = {}): HTMLElement {
|
||||
const el = document.createElement(tagName);
|
||||
|
||||
if (Array.isArray(classNames)) {
|
||||
|
@ -75,33 +74,12 @@ export default class Dom {
|
|||
* Creates Text Node with the passed content
|
||||
*
|
||||
* @param {string} content - text content
|
||||
*
|
||||
* @returns {Text}
|
||||
*/
|
||||
public static text(content: string): Text {
|
||||
return document.createTextNode(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates SVG icon linked to the sprite
|
||||
*
|
||||
* @param {string} name - name (id) of icon from sprite
|
||||
* @param {number} [width] - icon width
|
||||
* @param {number} [height] - icon height
|
||||
*
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
public static svg(name: string, width = 14, height = 14): SVGElement {
|
||||
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
|
||||
icon.classList.add('icon', 'icon--' + name);
|
||||
icon.setAttribute('width', width + 'px');
|
||||
icon.setAttribute('height', height + 'px');
|
||||
icon.innerHTML = `<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#${name}"></use>`;
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one or several elements to the parent
|
||||
*
|
||||
|
@ -109,8 +87,8 @@ export default class Dom {
|
|||
* @param {Element|Element[]|DocumentFragment|Text|Text[]} elements - element or elements list
|
||||
*/
|
||||
public static append(
|
||||
parent: Element|DocumentFragment,
|
||||
elements: Element|Element[]|DocumentFragment|Text|Text[]
|
||||
parent: Element | DocumentFragment,
|
||||
elements: Element | Element[] | DocumentFragment | Text | Text[]
|
||||
): void {
|
||||
if (Array.isArray(elements)) {
|
||||
elements.forEach((el) => parent.appendChild(el));
|
||||
|
@ -125,7 +103,7 @@ export default class Dom {
|
|||
* @param {Element} parent - where to append
|
||||
* @param {Element|Element[]} elements - element or elements list
|
||||
*/
|
||||
public static prepend(parent: Element, elements: Element|Element[]): void {
|
||||
public static prepend(parent: Element, elements: Element | Element[]): void {
|
||||
if (Array.isArray(elements)) {
|
||||
elements = elements.reverse();
|
||||
elements.forEach((el) => parent.prepend(el));
|
||||
|
@ -165,10 +143,9 @@ export default class Dom {
|
|||
*
|
||||
* @param {Element} el - element we searching inside. Default - DOM Document
|
||||
* @param {string} selector - searching string
|
||||
*
|
||||
* @returns {Element}
|
||||
*/
|
||||
public static find(el: Element|Document = document, selector: string): Element {
|
||||
public static find(el: Element | Document = document, selector: string): Element {
|
||||
return el.querySelector(selector);
|
||||
}
|
||||
|
||||
|
@ -189,10 +166,9 @@ export default class Dom {
|
|||
*
|
||||
* @param {Element|Document} el - element we searching inside. Default - DOM Document
|
||||
* @param {string} selector - searching string
|
||||
*
|
||||
* @returns {NodeList}
|
||||
*/
|
||||
public static findAll(el: Element|Document = document, selector: string): NodeList {
|
||||
public static findAll(el: Element | Document = document, selector: string): NodeList {
|
||||
return el.querySelectorAll(selector);
|
||||
}
|
||||
|
||||
|
@ -207,7 +183,7 @@ export default class Dom {
|
|||
}
|
||||
|
||||
/**
|
||||
* Find all contendeditable, textarea and editable input elements passed holder contains
|
||||
* Find all contenteditable, textarea and editable input elements passed holder contains
|
||||
*
|
||||
* @param holder - element where to find inputs
|
||||
*/
|
||||
|
@ -230,11 +206,9 @@ export default class Dom {
|
|||
* Leaf is the vertex that doesn't have any child nodes
|
||||
*
|
||||
* @description Method recursively goes throw the all Node until it finds the Leaf
|
||||
*
|
||||
* @param {Node} node - root Node. From this vertex we start Deep-first search
|
||||
* {@link https://en.wikipedia.org/wiki/Depth-first_search}
|
||||
* @param {boolean} [atLast] - find last text node
|
||||
*
|
||||
* @returns {Node} - it can be text Node or Element Node, so that caret will able to work with it
|
||||
*/
|
||||
public static getDeepestNode(node: Node, atLast = false): Node {
|
||||
|
@ -287,7 +261,6 @@ export default class Dom {
|
|||
* Check if object is DOM node
|
||||
*
|
||||
* @param {*} node - object to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -318,7 +291,6 @@ export default class Dom {
|
|||
* Check if passed element is contenteditable
|
||||
*
|
||||
* @param {HTMLElement} element - html element to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public static isContentEditable(element: HTMLElement): boolean {
|
||||
|
@ -329,7 +301,6 @@ export default class Dom {
|
|||
* Checks target if it is native input
|
||||
*
|
||||
* @param {*} target - HTML element or string
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -346,7 +317,6 @@ export default class Dom {
|
|||
* Checks if we can set caret
|
||||
*
|
||||
* @param {HTMLElement} target - target to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public static canSetCaret(target: HTMLElement): boolean {
|
||||
|
@ -377,9 +347,7 @@ export default class Dom {
|
|||
*
|
||||
* @description Method checks simple Node without any childs for emptiness
|
||||
* If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method
|
||||
*
|
||||
* @param {Node} node - node to check
|
||||
*
|
||||
* @returns {boolean} true if it is empty
|
||||
*/
|
||||
public static isNodeEmpty(node: Node): boolean {
|
||||
|
@ -402,7 +370,6 @@ export default class Dom {
|
|||
* checks node if it is doesn't have any child nodes
|
||||
*
|
||||
* @param {Node} node - node to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public static isLeaf(node: Node): boolean {
|
||||
|
@ -418,7 +385,6 @@ export default class Dom {
|
|||
* {@link https://en.wikipedia.org/wiki/Breadth-first_search}
|
||||
*
|
||||
* @description Pushes to stack all DOM leafs and checks for emptiness
|
||||
*
|
||||
* @param {Node} node - node to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
@ -453,7 +419,6 @@ export default class Dom {
|
|||
* Check if string contains html elements
|
||||
*
|
||||
* @param {string} str - string to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public static isHTMLString(str: string): boolean {
|
||||
|
@ -468,7 +433,6 @@ export default class Dom {
|
|||
* Return length of node`s text content
|
||||
*
|
||||
* @param {Node} node - node with content
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
public static getContentLength(node: Node): number {
|
||||
|
@ -523,6 +487,8 @@ export default class Dom {
|
|||
'ruby',
|
||||
'section',
|
||||
'table',
|
||||
'tbody',
|
||||
'thead',
|
||||
'tr',
|
||||
'tfoot',
|
||||
'ul',
|
||||
|
@ -534,7 +500,6 @@ export default class Dom {
|
|||
* Check if passed content includes only inline elements
|
||||
*
|
||||
* @param {string|HTMLElement} data - element or html string
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public static containsOnlyInlineElements(data: string | HTMLElement): boolean {
|
||||
|
@ -559,7 +524,6 @@ export default class Dom {
|
|||
* Find and return all block elements in the passed parent (including subtree)
|
||||
*
|
||||
* @param {HTMLElement} parent - root element
|
||||
*
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
public static getDeepestBlockElements(parent: HTMLElement): HTMLElement[] {
|
||||
|
@ -576,7 +540,6 @@ export default class Dom {
|
|||
* Helper for get holder from {string} or return HTMLElement
|
||||
*
|
||||
* @param {string | HTMLElement} element - holder's id or holder's HTML Element
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
public static getHolder(element: string | HTMLElement): HTMLElement {
|
||||
|
@ -591,7 +554,6 @@ export default class Dom {
|
|||
* Method checks passed Node if it is some extension Node
|
||||
*
|
||||
* @param {Node} node - any node
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public static isExtensionNode(node: Node): boolean {
|
||||
|
@ -606,7 +568,6 @@ export default class Dom {
|
|||
* Returns true if element is anchor (is A tag)
|
||||
*
|
||||
* @param {Element} element - element to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public static isAnchor(element: Element): element is HTMLAnchorElement {
|
||||
|
@ -619,7 +580,7 @@ export default class Dom {
|
|||
* @todo handle case when editor initialized in scrollable popup
|
||||
* @param el - element to compute offset
|
||||
*/
|
||||
public static offset(el): {top: number; left: number; right: number; bottom: number} {
|
||||
public static offset(el): { top: number; left: number; right: number; bottom: number } {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
|
|
@ -60,6 +60,19 @@ export default class DomIterator {
|
|||
return this.items[this.cursor];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets cursor to specified position
|
||||
*
|
||||
* @param cursorPosition - new cursor position
|
||||
*/
|
||||
public setCursor(cursorPosition: number): void {
|
||||
if (cursorPosition < this.items.length && cursorPosition >= -1) {
|
||||
this.dropCursor();
|
||||
this.cursor = cursorPosition;
|
||||
this.items[this.cursor].classList.add(this.focusedCssClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets items. Can be used when iterable items changed dynamically
|
||||
*
|
||||
|
@ -162,6 +175,7 @@ export default class DomIterator {
|
|||
/**
|
||||
* Focus input with micro-delay to ensure DOM is updated
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
_.delay(() => SelectionUtils.setCursor(this.items[focusedButtonIndex]), 50)();
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,13 @@ export interface FlipperOptions {
|
|||
* Flipper is a component that iterates passed items array by TAB or Arrows and clicks it by ENTER
|
||||
*/
|
||||
export default class Flipper {
|
||||
/**
|
||||
* True if flipper is currently activated
|
||||
*/
|
||||
public get isActivated(): boolean {
|
||||
return this.activated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance of flipper iterator
|
||||
*
|
||||
|
@ -64,6 +71,11 @@ export default class Flipper {
|
|||
*/
|
||||
private readonly activateCallback: (item: HTMLElement) => void;
|
||||
|
||||
/**
|
||||
* Contains list of callbacks to be executed on each flip
|
||||
*/
|
||||
private flipCallbacks: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
* @param {FlipperOptions} options - different constructing settings
|
||||
*/
|
||||
|
@ -93,21 +105,30 @@ export default class Flipper {
|
|||
/**
|
||||
* Active tab/arrows handling by flipper
|
||||
*
|
||||
* @param {HTMLElement[]} items - Some modules (like, InlineToolbar, BlockSettings) might refresh buttons dynamically
|
||||
* @param items - Some modules (like, InlineToolbar, BlockSettings) might refresh buttons dynamically
|
||||
* @param cursorPosition - index of the item that should be focused once flipper is activated
|
||||
*/
|
||||
public activate(items?: HTMLElement[]): void {
|
||||
public activate(items?: HTMLElement[], cursorPosition?: number): void {
|
||||
this.activated = true;
|
||||
|
||||
if (items) {
|
||||
this.iterator.setItems(items);
|
||||
}
|
||||
|
||||
if (cursorPosition !== undefined) {
|
||||
this.iterator.setCursor(cursorPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listening all keydowns on document and react on TAB/Enter press
|
||||
* TAB will leaf iterator items
|
||||
* ENTER will click the focused item
|
||||
*
|
||||
* Note: the event should be handled in capturing mode on following reasons:
|
||||
* - prevents plugins inner keydown handlers from being called while keyboard navigation
|
||||
* - otherwise this handler will be called at the moment it is attached which causes false flipper firing (see https://techread.me/js-addeventlistener-fires-for-past-events/)
|
||||
*/
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
document.addEventListener('keydown', this.onKeyDown, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -151,6 +172,24 @@ export default class Flipper {
|
|||
return !!this.iterator.currentItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registeres function that should be executed on each navigation action
|
||||
*
|
||||
* @param cb - function to execute
|
||||
*/
|
||||
public onFlip(cb: () => void): void {
|
||||
this.flipCallbacks.push(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisteres function that is executed on each navigation action
|
||||
*
|
||||
* @param cb - function to stop executing
|
||||
*/
|
||||
public removeOnFlip(cb: () => void): void {
|
||||
this.flipCallbacks = this.flipCallbacks.filter(fn => fn !== cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops flipper's iterator cursor
|
||||
*
|
||||
|
@ -240,15 +279,17 @@ export default class Flipper {
|
|||
}
|
||||
|
||||
if (this.iterator.currentItem) {
|
||||
/**
|
||||
* Stop Enter propagation only if flipper is ready to select focused item
|
||||
*/
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.iterator.currentItem.click();
|
||||
}
|
||||
|
||||
if (_.isFunction(this.activateCallback)) {
|
||||
this.activateCallback(this.iterator.currentItem);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -258,5 +299,7 @@ export default class Flipper {
|
|||
if (this.iterator.currentItem) {
|
||||
this.iterator.currentItem.scrollIntoViewIfNeeded();
|
||||
}
|
||||
|
||||
this.flipCallbacks.forEach(cb => cb());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ export default class I18n {
|
|||
* Perform translation of the string by namespace and a key
|
||||
*
|
||||
* @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune')
|
||||
*
|
||||
* @param internalNamespace - path to translated string in dictionary
|
||||
* @param dictKey - dictionary key. Better to use default locale original text
|
||||
*/
|
||||
|
|
|
@ -13,10 +13,12 @@
|
|||
},
|
||||
"toolbar": {
|
||||
"toolbox": {
|
||||
"Add": "",
|
||||
"Filter": "",
|
||||
"Nothing found": ""
|
||||
"Add": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"Filter": "",
|
||||
"Nothing found": ""
|
||||
}
|
||||
},
|
||||
"toolNames": {
|
||||
|
@ -35,7 +37,8 @@
|
|||
},
|
||||
"blockTunes": {
|
||||
"delete": {
|
||||
"Delete": ""
|
||||
"Delete": "",
|
||||
"Click to delete": ""
|
||||
},
|
||||
"moveUp": {
|
||||
"Move up": ""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import $ from '../dom';
|
||||
import { InlineTool, SanitizerConfig } from '../../../types';
|
||||
import { IconBold } from '@codexteam/icons';
|
||||
|
||||
/**
|
||||
* Bold Tool
|
||||
|
@ -61,28 +61,24 @@ export default class BoldInlineTool implements InlineTool {
|
|||
this.nodes.button = document.createElement('button') as HTMLButtonElement;
|
||||
this.nodes.button.type = 'button';
|
||||
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
||||
this.nodes.button.appendChild($.svg('bold', 12, 14));
|
||||
this.nodes.button.innerHTML = IconBold;
|
||||
|
||||
return this.nodes.button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap range with <b> tag
|
||||
*
|
||||
* @param {Range} range - range to wrap
|
||||
*/
|
||||
public surround(range: Range): void {
|
||||
public surround(): void {
|
||||
document.execCommand(this.commandName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check selection and set activated state to button if there are <b> tag
|
||||
*
|
||||
* @param {Selection} selection - selection to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public checkState(selection: Selection): boolean {
|
||||
public checkState(): boolean {
|
||||
const isActive = document.queryCommandState(this.commandName);
|
||||
|
||||
this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import $ from '../dom';
|
||||
import { InlineTool, SanitizerConfig } from '../../../types';
|
||||
import { IconItalic } from '@codexteam/icons';
|
||||
|
||||
/**
|
||||
* Italic Tool
|
||||
|
@ -61,26 +61,22 @@ export default class ItalicInlineTool implements InlineTool {
|
|||
this.nodes.button = document.createElement('button') as HTMLButtonElement;
|
||||
this.nodes.button.type = 'button';
|
||||
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
||||
this.nodes.button.appendChild($.svg('italic', 4, 11));
|
||||
this.nodes.button.innerHTML = IconItalic;
|
||||
|
||||
return this.nodes.button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap range with <i> tag
|
||||
*
|
||||
* @param {Range} range - range to wrap
|
||||
*/
|
||||
public surround(range: Range): void {
|
||||
public surround(): void {
|
||||
document.execCommand(this.commandName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check selection and set activated state to button if there are <i> tag
|
||||
*
|
||||
* @param {Selection} selection - selection to check
|
||||
*/
|
||||
public checkState(selection: Selection): boolean {
|
||||
public checkState(): boolean {
|
||||
const isActive = document.queryCommandState(this.commandName);
|
||||
|
||||
this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import SelectionUtils from '../selection';
|
||||
|
||||
import $ from '../dom';
|
||||
import * as _ from '../utils';
|
||||
import { InlineTool, SanitizerConfig } from '../../../types';
|
||||
import { Notifier, Toolbar, I18n } from '../../../types/api';
|
||||
import { InlineTool, SanitizerConfig, API } from '../../../types';
|
||||
import { Notifier, Toolbar, I18n, InlineToolbar } from '../../../types/api';
|
||||
import { IconLink, IconUnlink } from '@codexteam/icons';
|
||||
|
||||
/**
|
||||
* Link Tool
|
||||
|
@ -71,9 +70,9 @@ export default class LinkInlineTool implements InlineTool {
|
|||
button: HTMLButtonElement;
|
||||
input: HTMLInputElement;
|
||||
} = {
|
||||
button: null,
|
||||
input: null,
|
||||
};
|
||||
button: null,
|
||||
input: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* SelectionUtils instance
|
||||
|
@ -93,7 +92,7 @@ export default class LinkInlineTool implements InlineTool {
|
|||
/**
|
||||
* Available inline toolbar methods (open/close)
|
||||
*/
|
||||
private inlineToolbar: Toolbar;
|
||||
private inlineToolbar: InlineToolbar;
|
||||
|
||||
/**
|
||||
* Notifier API methods
|
||||
|
@ -106,9 +105,9 @@ export default class LinkInlineTool implements InlineTool {
|
|||
private i18n: I18n;
|
||||
|
||||
/**
|
||||
* @param {API} api - Editor.js API
|
||||
* @param api - Editor.js API
|
||||
*/
|
||||
constructor({ api }) {
|
||||
constructor({ api }: { api: API }) {
|
||||
this.toolbar = api.toolbar;
|
||||
this.inlineToolbar = api.inlineToolbar;
|
||||
this.notifier = api.notifier;
|
||||
|
@ -123,8 +122,8 @@ export default class LinkInlineTool implements InlineTool {
|
|||
this.nodes.button = document.createElement('button') as HTMLButtonElement;
|
||||
this.nodes.button.type = 'button';
|
||||
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
||||
this.nodes.button.appendChild($.svg('link', 14, 10));
|
||||
this.nodes.button.appendChild($.svg('unlink', 15, 11));
|
||||
|
||||
this.nodes.button.innerHTML = IconLink;
|
||||
|
||||
return this.nodes.button;
|
||||
}
|
||||
|
@ -187,13 +186,12 @@ export default class LinkInlineTool implements InlineTool {
|
|||
|
||||
/**
|
||||
* Check selection and set activated state to button if there are <a> tag
|
||||
*
|
||||
* @param {Selection} selection - selection to check
|
||||
*/
|
||||
public checkState(selection?: Selection): boolean {
|
||||
public checkState(): boolean {
|
||||
const anchorTag = this.selection.findParentTag('A');
|
||||
|
||||
if (anchorTag) {
|
||||
this.nodes.button.innerHTML = IconUnlink;
|
||||
this.nodes.button.classList.add(this.CSS.buttonUnlink);
|
||||
this.nodes.button.classList.add(this.CSS.buttonActive);
|
||||
this.openActions();
|
||||
|
@ -207,6 +205,7 @@ export default class LinkInlineTool implements InlineTool {
|
|||
|
||||
this.selection.save();
|
||||
} else {
|
||||
this.nodes.button.innerHTML = IconLink;
|
||||
this.nodes.button.classList.remove(this.CSS.buttonUnlink);
|
||||
this.nodes.button.classList.remove(this.CSS.buttonActive);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export default class BlocksAPI extends Module {
|
|||
delete: (index?: number): void => this.delete(index),
|
||||
swap: (fromIndex: number, toIndex: number): void => this.swap(fromIndex, toIndex),
|
||||
move: (toIndex: number, fromIndex?: number): void => this.move(toIndex, fromIndex),
|
||||
getBlockByIndex: (index: number): BlockAPIInterface | void => this.getBlockByIndex(index),
|
||||
getBlockByIndex: (index: number): BlockAPIInterface | undefined => this.getBlockByIndex(index),
|
||||
getById: (id: string): BlockAPIInterface | null => this.getById(id),
|
||||
getCurrentBlockIndex: (): number => this.getCurrentBlockIndex(),
|
||||
getBlockIndex: (id: string): number => this.getBlockIndex(id),
|
||||
|
@ -58,7 +58,6 @@ export default class BlocksAPI extends Module {
|
|||
* Returns the index of Block by id;
|
||||
*
|
||||
* @param id - block id
|
||||
* @returns {number}
|
||||
*/
|
||||
public getBlockIndex(id: string): number | undefined {
|
||||
const block = this.Editor.BlockManager.getBlockById(id);
|
||||
|
@ -77,7 +76,7 @@ export default class BlocksAPI extends Module {
|
|||
*
|
||||
* @param {number} index - index to get
|
||||
*/
|
||||
public getBlockByIndex(index: number): BlockAPIInterface | void {
|
||||
public getBlockByIndex(index: number): BlockAPIInterface | undefined {
|
||||
const block = this.Editor.BlockManager.getBlockByIndex(index);
|
||||
|
||||
if (block === undefined) {
|
||||
|
@ -201,7 +200,6 @@ export default class BlocksAPI extends Module {
|
|||
*
|
||||
* @param {number} index - index of Block to stretch
|
||||
* @param {boolean} status - true to enable, false to disable
|
||||
*
|
||||
* @deprecated Use BlockAPI interface to stretch Blocks
|
||||
*/
|
||||
public stretchBlock(index: number, status = true): void {
|
||||
|
@ -229,16 +227,20 @@ export default class BlocksAPI extends Module {
|
|||
* @param {number?} index — index where to insert new Block
|
||||
* @param {boolean?} needToFocus - flag to focus inserted Block
|
||||
* @param replace - pass true to replace the Block existed under passed index
|
||||
* @param {string} id — An optional id for the new block. If omitted then the new id will be generated
|
||||
*/
|
||||
public insert = (
|
||||
type: string = this.config.defaultBlock,
|
||||
data: BlockToolData = {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
config: ToolConfig = {},
|
||||
index?: number,
|
||||
needToFocus?: boolean,
|
||||
replace?: boolean
|
||||
replace?: boolean,
|
||||
id?: string
|
||||
): BlockAPIInterface => {
|
||||
const insertedBlock = this.Editor.BlockManager.insert({
|
||||
id,
|
||||
tool: type,
|
||||
data,
|
||||
index,
|
||||
|
@ -247,7 +249,7 @@ export default class BlocksAPI extends Module {
|
|||
});
|
||||
|
||||
return new BlockAPI(insertedBlock);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates data of an empty block with a passed type.
|
||||
|
@ -265,14 +267,13 @@ export default class BlocksAPI extends Module {
|
|||
});
|
||||
|
||||
return block.data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert new Block
|
||||
* After set caret to this Block
|
||||
*
|
||||
* @todo remove in 3.0.0
|
||||
*
|
||||
* @deprecated with insert() method
|
||||
*/
|
||||
public insertNewBlock(): void {
|
||||
|
@ -307,5 +308,5 @@ export default class BlocksAPI extends Module {
|
|||
replace: true,
|
||||
tunes: block.tunes,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ export default class CaretAPI extends Module {
|
|||
*
|
||||
* @param {string} position - position where to set caret
|
||||
* @param {number} offset - caret offset
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private setToFirstBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset = 0): boolean => {
|
||||
|
@ -38,14 +37,13 @@ export default class CaretAPI extends Module {
|
|||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.firstBlock, position, offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets caret to the last Block
|
||||
*
|
||||
* @param {string} position - position where to set caret
|
||||
* @param {number} offset - caret offset
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private setToLastBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset = 0): boolean => {
|
||||
|
@ -56,14 +54,13 @@ export default class CaretAPI extends Module {
|
|||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.lastBlock, position, offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets caret to the previous Block
|
||||
*
|
||||
* @param {string} position - position where to set caret
|
||||
* @param {number} offset - caret offset
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private setToPreviousBlock = (
|
||||
|
@ -77,14 +74,13 @@ export default class CaretAPI extends Module {
|
|||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.previousBlock, position, offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets caret to the next Block
|
||||
*
|
||||
* @param {string} position - position where to set caret
|
||||
* @param {number} offset - caret offset
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private setToNextBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset = 0): boolean => {
|
||||
|
@ -95,7 +91,7 @@ export default class CaretAPI extends Module {
|
|||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.nextBlock, position, offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets caret to the Block by passed index
|
||||
|
@ -103,7 +99,6 @@ export default class CaretAPI extends Module {
|
|||
* @param {number} index - index of Block where to set caret
|
||||
* @param {string} position - position where to set caret
|
||||
* @param {number} offset - caret offset
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private setToBlock = (
|
||||
|
@ -118,13 +113,12 @@ export default class CaretAPI extends Module {
|
|||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.blocks[index], position, offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets caret to the Editor
|
||||
*
|
||||
* @param {boolean} atEnd - if true, set Caret to the end of the Editor
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private focus = (atEnd = false): boolean => {
|
||||
|
@ -133,5 +127,5 @@ export default class CaretAPI extends Module {
|
|||
}
|
||||
|
||||
return this.setToFirstBlock(this.Editor.Caret.positions.START);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import EventsDispatcher from '../../utils/events';
|
||||
import { Notifier as INotifier } from '../../../../types/api';
|
||||
import Notifier from '../../utils/notifier';
|
||||
import { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier';
|
||||
|
@ -15,10 +14,9 @@ export default class NotifierAPI extends Module {
|
|||
private notifier: Notifier;
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @param {object} moduleConfiguration - Module Configuration
|
||||
* @param {EditorConfig} moduleConfiguration.config - Editor's config
|
||||
* @param {EventsDispatcher} moduleConfiguration.eventsDispatcher - Editor's event dispatcher
|
||||
* @param moduleConfiguration - Module Configuration
|
||||
* @param moduleConfiguration.config - Editor's config
|
||||
* @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher
|
||||
*/
|
||||
constructor({ config, eventsDispatcher }: ModuleConfig) {
|
||||
super({
|
||||
|
|
|
@ -25,7 +25,6 @@ export default class ReadOnlyAPI extends Module {
|
|||
* Set or toggle read-only state
|
||||
*
|
||||
* @param {boolean|undefined} state - set or toggle state
|
||||
*
|
||||
* @returns {boolean} current value
|
||||
*/
|
||||
public toggle(state?: boolean): Promise<boolean> {
|
||||
|
|
|
@ -24,7 +24,6 @@ export default class SanitizerAPI extends Module {
|
|||
*
|
||||
* @param {string} taintString - what to sanitize
|
||||
* @param {SanitizerConfig} config - sanitizer config
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
public clean(taintString: string, config: SanitizerConfig): string {
|
||||
|
|
|
@ -24,7 +24,6 @@ export default class SelectionAPI extends Module {
|
|||
*
|
||||
* @param {string} tagName - tag to find
|
||||
* @param {string} className - tag's class name
|
||||
*
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
public findParentTag(tagName: string, className?: string): HTMLElement | null {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Contains keyboard and mouse events binded on each Block by Block Manager
|
||||
* Contains keyboard and mouse events bound on each Block by Block Manager
|
||||
*/
|
||||
import Module from '../__module';
|
||||
import * as _ from '../utils';
|
||||
|
@ -129,13 +129,14 @@ export default class BlockEvents extends Module {
|
|||
const canOpenToolbox = currentBlock.tool.isDefault && isEmptyBlock;
|
||||
const conversionToolbarOpened = !isEmptyBlock && ConversionToolbar.opened;
|
||||
const inlineToolbarOpened = !isEmptyBlock && !SelectionUtils.isCollapsed && InlineToolbar.opened;
|
||||
const canOpenBlockTunes = !conversionToolbarOpened && !inlineToolbarOpened;
|
||||
|
||||
/**
|
||||
* For empty Blocks we show Plus button via Toolbox only for default Blocks
|
||||
*/
|
||||
if (canOpenToolbox) {
|
||||
this.activateToolbox();
|
||||
} else if (!conversionToolbarOpened && !inlineToolbarOpened) {
|
||||
} else if (canOpenBlockTunes) {
|
||||
this.activateBlockSettings();
|
||||
}
|
||||
}
|
||||
|
@ -232,7 +233,7 @@ export default class BlockEvents extends Module {
|
|||
}
|
||||
|
||||
/**
|
||||
* Allow to create linebreaks by Shift+Enter
|
||||
* Allow to create line breaks by Shift+Enter
|
||||
*/
|
||||
if (event.shiftKey) {
|
||||
return;
|
||||
|
@ -423,6 +424,7 @@ export default class BlockEvents extends Module {
|
|||
if (this.Editor.BlockManager.currentBlock) {
|
||||
this.Editor.BlockManager.currentBlock.updateCurrentInput();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 20)();
|
||||
}
|
||||
|
||||
|
@ -481,6 +483,7 @@ export default class BlockEvents extends Module {
|
|||
if (this.Editor.BlockManager.currentBlock) {
|
||||
this.Editor.BlockManager.currentBlock.updateCurrentInput();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 20)();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
/**
|
||||
* @class BlockManager
|
||||
* @classdesc Manage editor`s blocks storage and appearance
|
||||
*
|
||||
* @module BlockManager
|
||||
*
|
||||
* @version 2.0.0
|
||||
*/
|
||||
import Block, { BlockToolAPI } from '../block';
|
||||
|
@ -184,9 +182,7 @@ export default class BlockManager extends Module {
|
|||
* this._blocks[0] = new Block(...);
|
||||
*
|
||||
* block = this._blocks[0];
|
||||
*
|
||||
* @todo proxy the enumerate method
|
||||
*
|
||||
* @type {Proxy}
|
||||
* @private
|
||||
*/
|
||||
|
@ -229,7 +225,6 @@ export default class BlockManager extends Module {
|
|||
* @param {string} options.tool - tools passed in editor config {@link EditorConfig#tools}
|
||||
* @param {string} [options.id] - unique id for this block
|
||||
* @param {BlockToolData} [options.data] - constructor params
|
||||
*
|
||||
* @returns {Block}
|
||||
*/
|
||||
public composeBlock({
|
||||
|
@ -266,7 +261,6 @@ export default class BlockManager extends Module {
|
|||
* @param {number} [options.index] - index where to insert new Block
|
||||
* @param {boolean} [options.needToFocus] - flag shows if needed to update current Block index
|
||||
* @param {boolean} [options.replace] - flag shows if block by passed index should be replaced with inserted one
|
||||
*
|
||||
* @returns {Block}
|
||||
*/
|
||||
public insert({
|
||||
|
@ -333,7 +327,6 @@ export default class BlockManager extends Module {
|
|||
* @param {object} options - replace options
|
||||
* @param {string} options.tool — plugin name
|
||||
* @param {BlockToolData} options.data — plugin data
|
||||
*
|
||||
* @returns {Block}
|
||||
*/
|
||||
public replace({
|
||||
|
@ -381,7 +374,6 @@ export default class BlockManager extends Module {
|
|||
* @param {boolean} needToFocus - if true, updates current Block index
|
||||
*
|
||||
* TODO: Remove method and use insert() with index instead (?)
|
||||
*
|
||||
* @returns {Block} inserted Block
|
||||
*/
|
||||
public insertDefaultBlockAtIndex(index: number, needToFocus = false): Block {
|
||||
|
@ -427,7 +419,6 @@ export default class BlockManager extends Module {
|
|||
*
|
||||
* @param {Block} targetBlock - previous block will be append to this block
|
||||
* @param {Block} blockToMerge - block that will be merged with target block
|
||||
*
|
||||
* @returns {Promise} - the sequence that can be continued
|
||||
*/
|
||||
public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise<void> {
|
||||
|
@ -559,7 +550,6 @@ export default class BlockManager extends Module {
|
|||
* Returns Block by passed index
|
||||
*
|
||||
* @param {number} index - index to get. -1 to get last
|
||||
*
|
||||
* @returns {Block}
|
||||
*/
|
||||
public getBlockByIndex(index): Block {
|
||||
|
@ -583,7 +573,6 @@ export default class BlockManager extends Module {
|
|||
* Returns the Block by passed id
|
||||
*
|
||||
* @param id - id of block to get
|
||||
*
|
||||
* @returns {Block}
|
||||
*/
|
||||
public getBlockById(id): Block | undefined {
|
||||
|
@ -594,8 +583,6 @@ export default class BlockManager extends Module {
|
|||
* Get Block instance by html element
|
||||
*
|
||||
* @param {Node} element - html element to get Block by
|
||||
*
|
||||
* @returns {Block}
|
||||
*/
|
||||
public getBlock(element: HTMLElement): Block {
|
||||
if (!$.isElement(element) as boolean) {
|
||||
|
@ -690,7 +677,6 @@ export default class BlockManager extends Module {
|
|||
* Return block which contents passed node
|
||||
*
|
||||
* @param {Node} childNode - node to get Block by
|
||||
*
|
||||
* @returns {Block}
|
||||
*/
|
||||
public getBlockByChildNode(childNode: Node): Block {
|
||||
|
@ -711,7 +697,6 @@ export default class BlockManager extends Module {
|
|||
*
|
||||
* @param {number} fromIndex - index of first block
|
||||
* @param {number} toIndex - index of second block
|
||||
*
|
||||
* @deprecated — use 'move' instead
|
||||
*/
|
||||
public swap(fromIndex, toIndex): void {
|
||||
|
@ -855,7 +840,6 @@ export default class BlockManager extends Module {
|
|||
* Validates that the given index is not lower than 0 or higher than the amount of blocks
|
||||
*
|
||||
* @param {number} index - index of blocks array to validate
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private validateIndex(index: number): boolean {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/**
|
||||
* @class BlockSelection
|
||||
* @classdesc Manages Block selection with shortcut CMD+A
|
||||
*
|
||||
* @module BlockSelection
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
@ -190,10 +189,8 @@ export default class BlockSelection extends Module {
|
|||
*
|
||||
* - Remove all ranges
|
||||
* - Unselect all Blocks
|
||||
*
|
||||
* @param {boolean} readOnlyEnabled - "read only" state
|
||||
*/
|
||||
public toggleReadOnly(readOnlyEnabled: boolean): void {
|
||||
public toggleReadOnly(): void {
|
||||
SelectionUtils.get()
|
||||
.removeAllRanges();
|
||||
|
||||
|
@ -250,12 +247,13 @@ export default class BlockSelection extends Module {
|
|||
const eventKey = (reason as KeyboardEvent).key;
|
||||
|
||||
/**
|
||||
* If event.key length >1 that means key is special (e.g. Enter or Dead or Unidentifier).
|
||||
* If event.key length >1 that means key is special (e.g. Enter or Dead or Unidentified).
|
||||
* So we use empty string
|
||||
*
|
||||
* @see https://developer.mozilla.org/ru/docs/Web/API/KeyboardEvent/key
|
||||
*/
|
||||
Caret.insertContentAtCaretPosition(eventKey.length > 1 ? '' : eventKey);
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 20)();
|
||||
}
|
||||
|
||||
|
@ -283,7 +281,6 @@ export default class BlockSelection extends Module {
|
|||
* Reduce each Block and copy its content
|
||||
*
|
||||
* @param {ClipboardEvent} e - copy/cut event
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public copySelectedBlocks(e: ClipboardEvent): Promise<void> {
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
* @classdesc Contains methods for working Caret
|
||||
*
|
||||
* Uses Range methods to manipulate with caret
|
||||
*
|
||||
* @module Caret
|
||||
*
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
|
@ -110,7 +108,7 @@ export default class Caret extends Module {
|
|||
* Workaround case when block starts with several <br>'s (created by SHIFT+ENTER)
|
||||
*
|
||||
* @see https://github.com/codex-team/editor.js/issues/726
|
||||
* We need to allow to delete such linebreaks, so in this case caret IS NOT AT START
|
||||
* We need to allow to delete such line breaks, so in this case caret IS NOT AT START
|
||||
*/
|
||||
const regularLineBreak = $.isLineBreakTag(node);
|
||||
/**
|
||||
|
@ -162,7 +160,7 @@ export default class Caret extends Module {
|
|||
* In this case, anchor node has ELEMENT_NODE node type.
|
||||
* Anchor offset shows amount of children between start of the element and caret position.
|
||||
*
|
||||
* So we use child with anchofocusOffset - 1 as new focusNode.
|
||||
* So we use child with focusOffset - 1 as new focusNode.
|
||||
*/
|
||||
let focusOffset = selection.focusOffset;
|
||||
|
||||
|
@ -262,6 +260,7 @@ export default class Caret extends Module {
|
|||
*/
|
||||
_.delay(() => {
|
||||
this.set(nodeToSet as HTMLElement, offset);
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 20)();
|
||||
|
||||
BlockManager.setCurrentBlockByChildNode(block.holder);
|
||||
|
@ -509,6 +508,7 @@ export default class Caret extends Module {
|
|||
|
||||
newRange.selectNode(shadowCaret);
|
||||
newRange.extractContents();
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 50);
|
||||
}
|
||||
|
||||
|
@ -549,7 +549,7 @@ export default class Caret extends Module {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get all first-level (first child of [contenteditabel]) siblings from passed node
|
||||
* Get all first-level (first child of [contenteditable]) siblings from passed node
|
||||
* Then you can check it for emptiness
|
||||
*
|
||||
* @example
|
||||
|
@ -562,10 +562,8 @@ export default class Caret extends Module {
|
|||
* <p></p> | right first-level siblings
|
||||
* <p></p> |
|
||||
* </div>
|
||||
*
|
||||
* @param {HTMLElement} from - element from which siblings should be searched
|
||||
* @param {'left' | 'right'} direction - direction of search
|
||||
*
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
private getHigherLevelSiblings(from: HTMLElement, direction?: 'left' | 'right'): HTMLElement[] {
|
||||
|
|
|
@ -176,7 +176,7 @@ export default class CrossBlockSelection extends Module {
|
|||
private onMouseUp = (): void => {
|
||||
this.listeners.off(document, 'mouseover', this.onMouseOver);
|
||||
this.listeners.off(document, 'mouseup', this.onMouseUp);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mouse over event handler
|
||||
|
@ -222,7 +222,7 @@ export default class CrossBlockSelection extends Module {
|
|||
|
||||
this.toggleBlocksSelectedState(relatedBlock, targetBlock);
|
||||
this.lastSelectedBlock = targetBlock;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Change blocks selection state between passed two blocks.
|
||||
|
|
|
@ -4,7 +4,9 @@ import * as _ from '../utils';
|
|||
import {
|
||||
BlockAPI,
|
||||
PasteEvent,
|
||||
PasteEventDetail
|
||||
PasteEventDetail,
|
||||
SanitizerConfig,
|
||||
SanitizerRule
|
||||
} from '../../../types';
|
||||
import Block from '../block';
|
||||
import { SavedData } from '../../../types/data-formats';
|
||||
|
@ -20,6 +22,12 @@ interface TagSubstitute {
|
|||
*
|
||||
*/
|
||||
tool: BlockTool;
|
||||
|
||||
/**
|
||||
* If a Tool specifies just a tag name, all the attributes will be sanitized.
|
||||
* But Tool can explicitly specify sanitizer configuration for supported tags
|
||||
*/
|
||||
sanitizationConfig?: SanitizerRule;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,9 +105,7 @@ interface PasteData {
|
|||
/**
|
||||
* @class Paste
|
||||
* @classdesc Contains methods to handle paste on editor
|
||||
*
|
||||
* @module Paste
|
||||
*
|
||||
* @version 2.0.0
|
||||
*/
|
||||
export default class Paste extends Module {
|
||||
|
@ -112,12 +118,12 @@ export default class Paste extends Module {
|
|||
/**
|
||||
* Tags` substitutions parameters
|
||||
*/
|
||||
private toolsTags: {[tag: string]: TagSubstitute} = {};
|
||||
private toolsTags: { [tag: string]: TagSubstitute } = {};
|
||||
|
||||
/**
|
||||
* Store tags to substitute by tool name
|
||||
*/
|
||||
private tagsByTool: {[tools: string]: string[]} = {};
|
||||
private tagsByTool: { [tools: string]: string[] } = {};
|
||||
|
||||
/** Patterns` substitutions parameters */
|
||||
private toolsPatterns: PatternSubstitute[] = [];
|
||||
|
@ -168,7 +174,7 @@ export default class Paste extends Module {
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const includesFiles = types.includes ? types.includes('Files') : (types as any).contains('Files');
|
||||
|
||||
if (includesFiles) {
|
||||
if (includesFiles && !_.isEmpty(this.toolsFiles)) {
|
||||
await this.processFiles(dataTransfer.files);
|
||||
|
||||
return;
|
||||
|
@ -186,7 +192,7 @@ export default class Paste extends Module {
|
|||
this.insertEditorJSData(JSON.parse(editorJSData));
|
||||
|
||||
return;
|
||||
} catch (e) {} // Do nothing and continue execution as usual if error appears
|
||||
} catch (e) { } // Do nothing and continue execution as usual if error appears
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -198,7 +204,11 @@ export default class Paste extends Module {
|
|||
|
||||
/** Add all tags that can be substituted to sanitizer configuration */
|
||||
const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {
|
||||
result[tag.toLowerCase()] = true;
|
||||
/**
|
||||
* If Tool explicitly specifies sanitizer configuration for the tag, use it.
|
||||
* Otherwise, remove all attributes
|
||||
*/
|
||||
result[tag.toLowerCase()] = this.toolsTags[tag].sanitizationConfig ?? {};
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
|
@ -304,6 +314,30 @@ export default class Paste extends Module {
|
|||
e
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get tags name list from either tag name or sanitization config.
|
||||
*
|
||||
* @param {string | object} tagOrSanitizeConfig - tag name or sanitize config object.
|
||||
* @returns {string[]} array of tags.
|
||||
*/
|
||||
private collectTagNames(tagOrSanitizeConfig: string | SanitizerConfig): string[] {
|
||||
/**
|
||||
* If string, then it is a tag name.
|
||||
*/
|
||||
if (_.isString(tagOrSanitizeConfig)) {
|
||||
return [ tagOrSanitizeConfig ];
|
||||
}
|
||||
/**
|
||||
* If object, then its keys are tags.
|
||||
*/
|
||||
if (_.isObject(tagOrSanitizeConfig)) {
|
||||
return Object.keys(tagOrSanitizeConfig);
|
||||
}
|
||||
|
||||
/** Return empty tag list */
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -312,25 +346,39 @@ export default class Paste extends Module {
|
|||
* @param tool - BlockTool object
|
||||
*/
|
||||
private getTagsConfig(tool: BlockTool): void {
|
||||
const tags = tool.pasteConfig.tags || [];
|
||||
const tagsOrSanitizeConfigs = tool.pasteConfig.tags || [];
|
||||
const toolTags = [];
|
||||
|
||||
tags.forEach((tag) => {
|
||||
if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
|
||||
_.log(
|
||||
`Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` +
|
||||
`because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`,
|
||||
'warn'
|
||||
);
|
||||
tagsOrSanitizeConfigs.forEach((tagOrSanitizeConfig) => {
|
||||
const tags = this.collectTagNames(tagOrSanitizeConfig);
|
||||
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Add tags to toolTags array
|
||||
*/
|
||||
toolTags.push(...tags);
|
||||
tags.forEach((tag) => {
|
||||
if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
|
||||
_.log(
|
||||
`Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` +
|
||||
`because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`,
|
||||
'warn'
|
||||
);
|
||||
|
||||
this.toolsTags[tag.toUpperCase()] = {
|
||||
tool,
|
||||
};
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Get sanitize config for tag.
|
||||
*/
|
||||
const sanitizationConfig = _.isObject(tagOrSanitizeConfig) ? tagOrSanitizeConfig[tag] : null;
|
||||
|
||||
this.toolsTags[tag.toUpperCase()] = {
|
||||
tool,
|
||||
sanitizationConfig,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
this.tagsByTool[tool.name] = tags.map((t) => t.toUpperCase());
|
||||
this.tagsByTool[tool.name] = toolTags.map((t) => t.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -405,7 +453,6 @@ export default class Paste extends Module {
|
|||
* Check if browser behavior suits better
|
||||
*
|
||||
* @param {EventTarget} element - element where content has been pasted
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isNativeBehaviour(element: EventTarget): boolean {
|
||||
|
@ -439,7 +486,7 @@ export default class Paste extends Module {
|
|||
|
||||
BlockManager.clearFocused();
|
||||
Toolbar.close();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get files from data transfer object and insert related Tools
|
||||
|
@ -449,7 +496,7 @@ export default class Paste extends Module {
|
|||
private async processFiles(items: FileList): Promise<void> {
|
||||
const { BlockManager } = this.Editor;
|
||||
|
||||
let dataToInsert: {type: string; event: PasteEvent}[];
|
||||
let dataToInsert: { type: string; event: PasteEvent }[];
|
||||
|
||||
dataToInsert = await Promise.all(
|
||||
Array
|
||||
|
@ -473,11 +520,12 @@ export default class Paste extends Module {
|
|||
*
|
||||
* @param {File} file - file to process
|
||||
*/
|
||||
private async processFile(file: File): Promise<{event: PasteEvent; type: string}> {
|
||||
private async processFile(file: File): Promise<{ event: PasteEvent; type: string }> {
|
||||
const extension = _.getFileExtension(file);
|
||||
|
||||
const foundConfig = Object
|
||||
.entries(this.toolsFiles)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
.find(([toolName, { mimeTypes, extensions } ]) => {
|
||||
const [fileType, fileSubtype] = file.type.split('/');
|
||||
|
||||
|
@ -510,11 +558,22 @@ export default class Paste extends Module {
|
|||
* Split HTML string to blocks and return it as array of Block data
|
||||
*
|
||||
* @param {string} innerHTML - html string to process
|
||||
*
|
||||
* @returns {PasteData[]}
|
||||
*/
|
||||
private processHTML(innerHTML: string): PasteData[] {
|
||||
const { Tools } = this.Editor;
|
||||
|
||||
/**
|
||||
* @todo Research, do we really need to always wrap innerHTML to a div:
|
||||
* - <img> tag could be processed separately, but for now it becomes div-wrapped
|
||||
* and then .getNodes() returns strange: [document-fragment, img]
|
||||
* (description of the method says that it should should return only block tags or fragments,
|
||||
* but there are inline-block element along with redundant empty fragment)
|
||||
* - probably this is a reason of bugs with unexpected new block creation instead of inline pasting:
|
||||
* - https://github.com/codex-team/editor.js/issues/1427
|
||||
* - https://github.com/codex-team/editor.js/issues/1244
|
||||
* - https://github.com/codex-team/editor.js/issues/740
|
||||
*/
|
||||
const wrapper = $.make('DIV');
|
||||
|
||||
wrapper.innerHTML = innerHTML;
|
||||
|
@ -543,16 +602,65 @@ export default class Paste extends Module {
|
|||
break;
|
||||
}
|
||||
|
||||
const { tags } = tool.pasteConfig;
|
||||
const { tags: tagsOrSanitizeConfigs } = tool.pasteConfig;
|
||||
|
||||
const toolTags = tags.reduce((result, tag) => {
|
||||
result[tag.toLowerCase()] = {};
|
||||
/**
|
||||
* Reduce the tags or sanitize configs to a single array of sanitize config.
|
||||
* For example:
|
||||
* If sanitize config is
|
||||
* [ 'tbody',
|
||||
* {
|
||||
* table: {
|
||||
* width: true,
|
||||
* height: true,
|
||||
* },
|
||||
* },
|
||||
* {
|
||||
* td: {
|
||||
* colspan: true,
|
||||
* rowspan: true,
|
||||
* },
|
||||
* tr: { // <-- the second tag
|
||||
* height: true,
|
||||
* },
|
||||
* },
|
||||
* ]
|
||||
* then sanitize config will be
|
||||
* [
|
||||
* 'table':{},
|
||||
* 'tbody':{width: true, height: true}
|
||||
* 'td':{colspan: true, rowspan: true},
|
||||
* 'tr':{height: true}
|
||||
* ]
|
||||
*/
|
||||
const toolTags = tagsOrSanitizeConfigs.reduce((result, tagOrSanitizeConfig) => {
|
||||
const tags = this.collectTagNames(tagOrSanitizeConfig);
|
||||
|
||||
tags.forEach((tag) => {
|
||||
const sanitizationConfig = _.isObject(tagOrSanitizeConfig) ? tagOrSanitizeConfig[tag] : null;
|
||||
|
||||
result[tag] = sanitizationConfig || {};
|
||||
});
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const customConfig = Object.assign({}, toolTags, tool.baseSanitizeConfig);
|
||||
|
||||
content.innerHTML = clean(content.innerHTML, customConfig);
|
||||
/**
|
||||
* A workaround for the HTMLJanitor bug with Tables (incorrect sanitizing of table.innerHTML)
|
||||
* https://github.com/guardian/html-janitor/issues/3
|
||||
*/
|
||||
if (content.tagName.toLowerCase() === 'table') {
|
||||
const cleanTableHTML = clean(content.outerHTML, customConfig);
|
||||
const tmpWrapper = $.make('div', undefined, {
|
||||
innerHTML: cleanTableHTML,
|
||||
});
|
||||
|
||||
content = tmpWrapper.firstChild;
|
||||
} else {
|
||||
content.innerHTML = clean(content.innerHTML, customConfig);
|
||||
}
|
||||
|
||||
const event = this.composePasteEvent('tag', {
|
||||
data: content,
|
||||
|
@ -565,18 +673,22 @@ export default class Paste extends Module {
|
|||
event,
|
||||
};
|
||||
})
|
||||
.filter((data) => !$.isNodeEmpty(data.content) || $.isSingleTag(data.content));
|
||||
.filter((data) => {
|
||||
const isEmpty = $.isEmpty(data.content);
|
||||
const isSingleTag = $.isSingleTag(data.content);
|
||||
|
||||
return !isEmpty || isSingleTag;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Split plain text by new line symbols and return it as array of Block data
|
||||
*
|
||||
* @param {string} plain - string to process
|
||||
*
|
||||
* @returns {PasteData[]}
|
||||
*/
|
||||
private processPlain(plain: string): PasteData[] {
|
||||
const { defaultBlock } = this.config as {defaultBlock: string};
|
||||
const { defaultBlock } = this.config as { defaultBlock: string };
|
||||
|
||||
if (!plain) {
|
||||
return [];
|
||||
|
@ -608,7 +720,7 @@ export default class Paste extends Module {
|
|||
/**
|
||||
* Process paste of single Block tool content
|
||||
*
|
||||
* @param {PasteData} dataToInsert - data of Block to inseret
|
||||
* @param {PasteData} dataToInsert - data of Block to insert
|
||||
*/
|
||||
private async processSingleBlock(dataToInsert: PasteData): Promise<void> {
|
||||
const { Caret, BlockManager } = this.Editor;
|
||||
|
@ -678,10 +790,9 @@ export default class Paste extends Module {
|
|||
* Get patterns` matches
|
||||
*
|
||||
* @param {string} text - text to process
|
||||
*
|
||||
* @returns {Promise<{event: PasteEvent, tool: string}>}
|
||||
*/
|
||||
private async processPattern(text: string): Promise<{event: PasteEvent; tool: string}> {
|
||||
private async processPattern(text: string): Promise<{ event: PasteEvent; tool: string }> {
|
||||
const pattern = this.toolsPatterns.find((substitute) => {
|
||||
const execResult = substitute.pattern.exec(text);
|
||||
|
||||
|
@ -712,7 +823,6 @@ export default class Paste extends Module {
|
|||
*
|
||||
* @param {PasteData} data - data to insert
|
||||
* @param {boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
private insertBlock(data: PasteData, canReplaceCurrentBlock = false): void {
|
||||
|
@ -736,7 +846,6 @@ export default class Paste extends Module {
|
|||
* Insert data passed as application/x-editor-js JSON
|
||||
*
|
||||
* @param {Array} blocks — Blocks' data to insert
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
private insertEditorJSData(blocks: Pick<SavedData, 'id' | 'data' | 'tool'>[]): void {
|
||||
|
@ -770,8 +879,6 @@ export default class Paste extends Module {
|
|||
* @param {Node} node - current node
|
||||
* @param {Node[]} nodes - processed nodes
|
||||
* @param {Node} destNode - destination node
|
||||
*
|
||||
* @returns {Node[]}
|
||||
*/
|
||||
private processElementNode(node: Node, nodes: Node[], destNode: Node): Node[] | void {
|
||||
const tags = Object.keys(this.toolsTags);
|
||||
|
@ -814,7 +921,6 @@ export default class Paste extends Module {
|
|||
* 2. Document Fragments contained text and markup tags like a, b, i etc.
|
||||
*
|
||||
* @param {Node} wrapper - wrapper of paster HTML content
|
||||
*
|
||||
* @returns {Node[]}
|
||||
*/
|
||||
private getNodes(wrapper: Node): Node[] {
|
||||
|
@ -878,3 +984,4 @@ export default class Paste extends Module {
|
|||
}) as PasteEvent;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,9 +6,7 @@ import { CriticalError } from '../errors/critical';
|
|||
*
|
||||
* Has one important method:
|
||||
* - {Function} toggleReadonly - Set read-only mode or toggle current state
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @typedef {ReadOnly} ReadOnly
|
||||
* @property {boolean} readOnlyEnabled - read-only state
|
||||
*/
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/**
|
||||
* @class RectangleSelection
|
||||
* @classdesc Manages Block selection with mouse
|
||||
*
|
||||
* @module RectangleSelection
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
@ -188,6 +187,7 @@ export default class RectangleSelection extends Module {
|
|||
|
||||
this.listeners.on(document.body, 'mousemove', _.throttle((mouseEvent: MouseEvent) => {
|
||||
this.processMouseMove(mouseEvent);
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 10), {
|
||||
passive: true,
|
||||
});
|
||||
|
@ -198,6 +198,7 @@ export default class RectangleSelection extends Module {
|
|||
|
||||
this.listeners.on(window, 'scroll', _.throttle((mouseEvent: MouseEvent) => {
|
||||
this.processScroll(mouseEvent);
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 10), {
|
||||
passive: true,
|
||||
});
|
||||
|
@ -290,7 +291,7 @@ export default class RectangleSelection extends Module {
|
|||
/**
|
||||
* Generates required HTML elements
|
||||
*
|
||||
* @returns {object<string, Element>}
|
||||
* @returns {Object<string, Element>}
|
||||
*/
|
||||
private genHTML(): {container: Element; overlay: Element} {
|
||||
const { UI } = this.Editor;
|
||||
|
|
|
@ -8,7 +8,6 @@ import BlockTool from '../tools/block';
|
|||
*
|
||||
* @module Renderer
|
||||
* @author CodeX Team
|
||||
*
|
||||
* @version 2.0.0
|
||||
*/
|
||||
export default class Renderer extends Module {
|
||||
|
@ -37,7 +36,6 @@ export default class Renderer extends Module {
|
|||
* }
|
||||
* },
|
||||
* ]
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -68,7 +66,6 @@ export default class Renderer extends Module {
|
|||
* Insert block to working zone
|
||||
*
|
||||
* @param {object} item - Block data to insert
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async insertBlock(item: OutputBlockData): Promise<void> {
|
||||
|
|
|
@ -16,7 +16,6 @@ declare const VERSION: string;
|
|||
|
||||
/**
|
||||
* @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data
|
||||
*
|
||||
* @typedef {Saver} Saver
|
||||
* @property {Element} html - Editor HTML content
|
||||
* @property {string} json - Editor JSON output
|
||||
|
|
|
@ -1,30 +1,22 @@
|
|||
import Module from '../../__module';
|
||||
import $ from '../../dom';
|
||||
import Flipper, { FlipperOptions } from '../../flipper';
|
||||
import * as _ from '../../utils';
|
||||
import SelectionUtils from '../../selection';
|
||||
import Block from '../../block';
|
||||
import Popover, { PopoverEvent } from '../../utils/popover';
|
||||
import I18n from '../../i18n';
|
||||
import { I18nInternalNS } from '../../i18n/namespace-internal';
|
||||
import Flipper from '../../flipper';
|
||||
|
||||
/**
|
||||
* HTML Elements that used for BlockSettings
|
||||
*/
|
||||
interface BlockSettingsNodes {
|
||||
wrapper: HTMLElement;
|
||||
toolSettings: HTMLElement;
|
||||
defaultSettings: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Block Settings
|
||||
*
|
||||
* ____ Settings Panel ____
|
||||
* | ...................... |
|
||||
* | . Tool Settings . |
|
||||
* | ...................... |
|
||||
* | . Default Settings . |
|
||||
* | ...................... |
|
||||
* |________________________|
|
||||
*
|
||||
* @todo Make Block Settings no-module but a standalone class, like Toolbox
|
||||
*/
|
||||
export default class BlockSettings extends Module<BlockSettingsNodes> {
|
||||
|
@ -42,82 +34,50 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
|||
|
||||
/**
|
||||
* Block Settings CSS
|
||||
*
|
||||
* @returns {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}}
|
||||
*/
|
||||
public get CSS(): { [name: string]: string } {
|
||||
return {
|
||||
// Settings Panel
|
||||
wrapper: 'ce-settings',
|
||||
wrapperOpened: 'ce-settings--opened',
|
||||
toolSettings: 'ce-settings__plugin-zone',
|
||||
defaultSettings: 'ce-settings__default-zone',
|
||||
|
||||
button: 'ce-settings__button',
|
||||
|
||||
focusedButton: 'ce-settings__button--focused',
|
||||
focusedButtonAnimated: 'ce-settings__button--focused-animated',
|
||||
settings: 'ce-settings',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Block Settings opened or not
|
||||
*
|
||||
* @returns {boolean}
|
||||
* Opened state
|
||||
*/
|
||||
public get opened(): boolean {
|
||||
return this.nodes.wrapper.classList.contains(this.CSS.wrapperOpened);
|
||||
public opened = false;
|
||||
|
||||
/**
|
||||
* Getter for inner popover's flipper instance
|
||||
*
|
||||
* @todo remove once BlockSettings becomes standalone non-module class
|
||||
*/
|
||||
public get flipper(): Flipper {
|
||||
return this.popover?.flipper;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of buttons
|
||||
*/
|
||||
private buttons: HTMLElement[] = [];
|
||||
|
||||
/**
|
||||
* Instance of class that responses for leafing buttons by arrows/tab
|
||||
*
|
||||
* @type {Flipper|null}
|
||||
*/
|
||||
private flipper: Flipper = null;
|
||||
|
||||
/**
|
||||
* Page selection utils
|
||||
*/
|
||||
private selection: SelectionUtils = new SelectionUtils();
|
||||
|
||||
/**
|
||||
* Popover instance. There is a util for vertical lists.
|
||||
*/
|
||||
private popover: Popover | undefined;
|
||||
|
||||
/**
|
||||
* Panel with block settings with 2 sections:
|
||||
* - Tool's Settings
|
||||
* - Default Settings [Move, Remove, etc]
|
||||
*/
|
||||
public make(): void {
|
||||
this.nodes.wrapper = $.make('div', this.CSS.wrapper);
|
||||
|
||||
this.nodes.toolSettings = $.make('div', this.CSS.toolSettings);
|
||||
this.nodes.defaultSettings = $.make('div', this.CSS.defaultSettings);
|
||||
|
||||
$.append(this.nodes.wrapper, [this.nodes.toolSettings, this.nodes.defaultSettings]);
|
||||
|
||||
/**
|
||||
* Active leafing by arrows/tab
|
||||
* Buttons will be filled on opening
|
||||
*/
|
||||
this.enableFlipper();
|
||||
this.nodes.wrapper = $.make('div');
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys module
|
||||
*/
|
||||
public destroy(): void {
|
||||
/**
|
||||
* Sometimes (in read-only mode) there is no Flipper
|
||||
*/
|
||||
if (this.flipper) {
|
||||
this.flipper.deactivate();
|
||||
this.flipper = null;
|
||||
}
|
||||
|
||||
this.removeAllNodes();
|
||||
}
|
||||
|
||||
|
@ -127,7 +87,7 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
|||
* @param targetBlock - near which Block we should open BlockSettings
|
||||
*/
|
||||
public open(targetBlock: Block = this.Editor.BlockManager.currentBlock): void {
|
||||
this.nodes.wrapper.classList.add(this.CSS.wrapperOpened);
|
||||
this.opened = true;
|
||||
|
||||
/**
|
||||
* If block settings contains any inputs, focus will be set there,
|
||||
|
@ -144,24 +104,41 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
|||
/**
|
||||
* Fill Tool's settings
|
||||
*/
|
||||
this.addToolSettings(targetBlock);
|
||||
|
||||
/**
|
||||
* Add default settings that presents for all Blocks
|
||||
*/
|
||||
this.addTunes(targetBlock);
|
||||
const [tunesItems, customHtmlTunesContainer] = targetBlock.getTunes();
|
||||
|
||||
/** Tell to subscribers that block settings is opened */
|
||||
this.eventsDispatcher.emit(this.events.opened);
|
||||
|
||||
this.flipper.activate(this.blockTunesButtons);
|
||||
this.popover = new Popover({
|
||||
className: this.CSS.settings,
|
||||
searchable: true,
|
||||
filterLabel: I18n.ui(I18nInternalNS.ui.popover, 'Filter'),
|
||||
nothingFoundLabel: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
|
||||
items: tunesItems,
|
||||
customContent: customHtmlTunesContainer,
|
||||
customContentFlippableItems: this.getControls(customHtmlTunesContainer),
|
||||
scopeElement: this.Editor.API.methods.ui.nodes.redactor,
|
||||
});
|
||||
this.popover.on(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
||||
this.popover.on(PopoverEvent.Close, () => this.close());
|
||||
|
||||
this.nodes.wrapper.append(this.popover.getElement());
|
||||
|
||||
this.popover.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns root block settings element
|
||||
*/
|
||||
public getElement(): HTMLElement {
|
||||
return this.nodes.wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Block Settings pane
|
||||
*/
|
||||
public close(): void {
|
||||
this.nodes.wrapper.classList.remove(this.CSS.wrapperOpened);
|
||||
this.opened = false;
|
||||
|
||||
/**
|
||||
* If selection is at editor on Block Settings closing,
|
||||
|
@ -183,106 +160,36 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
|
|||
this.Editor.BlockManager.currentBlock.selected = false;
|
||||
}
|
||||
|
||||
/** Clear settings */
|
||||
this.nodes.toolSettings.innerHTML = '';
|
||||
this.nodes.defaultSettings.innerHTML = '';
|
||||
|
||||
/** Tell to subscribers that block settings is closed */
|
||||
this.eventsDispatcher.emit(this.events.closed);
|
||||
|
||||
/** Clear cached buttons */
|
||||
this.buttons = [];
|
||||
|
||||
/** Clear focus on active button */
|
||||
this.flipper.deactivate();
|
||||
if (this.popover) {
|
||||
this.popover.off(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
||||
this.popover.destroy();
|
||||
this.popover.getElement().remove();
|
||||
this.popover = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Tools Settings and Default Settings
|
||||
* Returns list of buttons and inputs inside specified container
|
||||
*
|
||||
* @returns {HTMLElement[]}
|
||||
* @param container - container to query controls inside of
|
||||
*/
|
||||
public get blockTunesButtons(): HTMLElement[] {
|
||||
private getControls(container: HTMLElement): HTMLElement[] {
|
||||
const { StylesAPI } = this.Editor;
|
||||
|
||||
/**
|
||||
* Return from cache
|
||||
* if exists
|
||||
*/
|
||||
if (this.buttons.length !== 0) {
|
||||
return this.buttons;
|
||||
}
|
||||
|
||||
const toolSettings = this.nodes.toolSettings.querySelectorAll(
|
||||
// Select buttons and inputs
|
||||
/** Query buttons and inputs inside tunes html */
|
||||
const controls = container.querySelectorAll<HTMLElement>(
|
||||
`.${StylesAPI.classes.settingsButton}, ${$.allInputsSelector}`
|
||||
);
|
||||
const defaultSettings = this.nodes.defaultSettings.querySelectorAll(`.${this.CSS.button}`);
|
||||
|
||||
toolSettings.forEach((item) => {
|
||||
this.buttons.push((item as HTMLElement));
|
||||
});
|
||||
|
||||
defaultSettings.forEach((item) => {
|
||||
this.buttons.push((item as HTMLElement));
|
||||
});
|
||||
|
||||
return this.buttons;
|
||||
return Array.from(controls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Tool's settings
|
||||
*
|
||||
* @param targetBlock - Block to render settings
|
||||
* Handles overlay click
|
||||
*/
|
||||
private addToolSettings(targetBlock): void {
|
||||
const settingsElement = targetBlock.renderSettings();
|
||||
|
||||
if (settingsElement) {
|
||||
$.append(this.nodes.toolSettings, settingsElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tunes: provided by user and default ones
|
||||
*
|
||||
* @param targetBlock - Block to render its Tunes set
|
||||
*/
|
||||
private addTunes(targetBlock): void {
|
||||
const [toolTunes, defaultTunes] = targetBlock.renderTunes();
|
||||
|
||||
$.append(this.nodes.toolSettings, toolTunes);
|
||||
$.append(this.nodes.defaultSettings, defaultTunes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Active leafing by arrows/tab
|
||||
* Buttons will be filled on opening
|
||||
*/
|
||||
private enableFlipper(): void {
|
||||
this.flipper = new Flipper({
|
||||
focusedItemClass: this.CSS.focusedButton,
|
||||
/**
|
||||
* @param {HTMLElement} focusedItem - activated Tune
|
||||
*/
|
||||
activateCallback: (focusedItem) => {
|
||||
/**
|
||||
* If focused item is editable element, close block settings
|
||||
*/
|
||||
if (focusedItem && $.canSetCaret(focusedItem)) {
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restoring focus on current Block after settings clicked.
|
||||
* For example, when H3 changed to H2 — DOM Elements replaced, so we need to focus a new one
|
||||
*/
|
||||
_.delay(() => {
|
||||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
|
||||
}, 50)();
|
||||
},
|
||||
} as FlipperOptions);
|
||||
}
|
||||
private onOverlayClicked = (): void => {
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
/**
|
||||
* Available tools data
|
||||
*/
|
||||
private tools: {name: string; toolboxItem: ToolboxConfigEntry; button: HTMLElement}[] = []
|
||||
private tools: {name: string; toolboxItem: ToolboxConfigEntry; button: HTMLElement}[] = [];
|
||||
|
||||
/**
|
||||
* Instance of class that responses for leafing buttons by arrows/tab
|
||||
|
@ -184,8 +184,6 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
public async replaceWithBlock(replacingToolName: string, blockDataOverrides?: BlockToolData): Promise<void> {
|
||||
/**
|
||||
* At first, we get current Block data
|
||||
*
|
||||
* @type {BlockToolConstructable}
|
||||
*/
|
||||
const currentBlockTool = this.Editor.BlockManager.currentBlock.tool;
|
||||
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
|
||||
|
@ -193,8 +191,6 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
|
||||
/**
|
||||
* Getting a class of replacing Tool
|
||||
*
|
||||
* @type {BlockToolConstructable}
|
||||
*/
|
||||
const replacingTool = this.Editor.Tools.blockTools.get(replacingToolName);
|
||||
|
||||
|
@ -265,6 +261,7 @@ export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
|
|||
|
||||
_.delay(() => {
|
||||
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 10)();
|
||||
}
|
||||
|
||||
|
|
|
@ -8,25 +8,24 @@ import { ModuleConfig } from '../../../types-internal/module-config';
|
|||
import { BlockAPI } from '../../../../types';
|
||||
import Block from '../../block';
|
||||
import Toolbox, { ToolboxEvent } from '../../ui/toolbox';
|
||||
import { IconMenu, IconPlus } from '@codexteam/icons';
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @todo TESTCASE - Click inside Redactor zone when Block Settings are opened:
|
||||
* @todo TEST CASE - show toggler after opening and closing the Inline Toolbar
|
||||
* @todo TEST CASE - Click outside Editor holder should close Toolbar and Clear Focused blocks
|
||||
* @todo TEST CASE - Click inside Editor holder should close Toolbar and Clear Focused blocks
|
||||
* @todo TEST CASE - Click inside Redactor zone when Block Settings are opened:
|
||||
* - should close Block Settings
|
||||
* - should not close Toolbar
|
||||
* - should move Toolbar to the clicked Block
|
||||
* @todo TESTCASE - Toolbar should be closed on the Cross Block Selection
|
||||
* @todo TESTCASE - Toolbar should be closed on the Rectangle Selection
|
||||
* @todo TESTCASE - If Block Settings or Toolbox are opened, the Toolbar should not be moved by Bocks hovering
|
||||
* @todo TEST CASE - Toolbar should be closed on the Cross Block Selection
|
||||
* @todo TEST CASE - Toolbar should be closed on the Rectangle Selection
|
||||
* @todo TEST CASE - If Block Settings or Toolbox are opened, the Toolbar should not be moved by Bocks hovering
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -78,7 +77,6 @@ interface ToolbarNodes {
|
|||
*
|
||||
* @class
|
||||
* @classdesc Toolbar module
|
||||
*
|
||||
* @typedef {Toolbar} Toolbar
|
||||
* @property {object} nodes - Toolbar nodes
|
||||
* @property {Element} nodes.wrapper - Toolbar main element
|
||||
|
@ -300,12 +298,8 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
*
|
||||
* @param {boolean} withBlockActions - by default, Toolbar opens with Block Actions.
|
||||
* This flag allows to open Toolbar without Actions.
|
||||
* @param {boolean} needToCloseToolbox - by default, Toolbar will be moved with opening
|
||||
* (by click on Block, or by enter)
|
||||
* with closing Toolbox and Block Settings
|
||||
* This flag allows to open Toolbar with Toolbox
|
||||
*/
|
||||
private open(withBlockActions = true, needToCloseToolbox = true): void {
|
||||
private open(withBlockActions = true): void {
|
||||
_.delay(() => {
|
||||
this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);
|
||||
|
||||
|
@ -314,6 +308,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
} else {
|
||||
this.blockActions.hide();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 50)();
|
||||
}
|
||||
|
||||
|
@ -341,8 +336,9 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
* - Plus Button
|
||||
* - Toolbox
|
||||
*/
|
||||
this.nodes.plusButton = $.make('div', this.CSS.plusButton);
|
||||
$.append(this.nodes.plusButton, $.svg('plus', 16, 16));
|
||||
this.nodes.plusButton = $.make('div', this.CSS.plusButton, {
|
||||
innerHTML: IconPlus,
|
||||
});
|
||||
$.append(this.nodes.actions, this.nodes.plusButton);
|
||||
|
||||
this.readOnlyMutableListeners.on(this.nodes.plusButton, 'click', () => {
|
||||
|
@ -370,10 +366,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
* - Remove Block Button
|
||||
* - Settings Panel
|
||||
*/
|
||||
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
|
||||
const settingsIcon = $.svg('dots', 16, 16);
|
||||
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler, {
|
||||
innerHTML: IconMenu,
|
||||
});
|
||||
|
||||
$.append(this.nodes.settingsToggler, settingsIcon);
|
||||
$.append(this.nodes.actions, this.nodes.settingsToggler);
|
||||
|
||||
this.tooltip.onHover(
|
||||
|
@ -388,7 +384,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
* Appending Toolbar components to itself
|
||||
*/
|
||||
$.append(this.nodes.actions, this.makeToolbox());
|
||||
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
|
||||
$.append(this.nodes.actions, this.Editor.BlockSettings.getElement());
|
||||
|
||||
/**
|
||||
* Append toolbar to the Editor
|
||||
|
@ -407,8 +403,8 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
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'),
|
||||
filter: I18n.ui(I18nInternalNS.ui.popover, 'Filter'),
|
||||
nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -478,7 +474,7 @@ export default class Toolbar extends Module<ToolbarNodes> {
|
|||
}, true);
|
||||
|
||||
/**
|
||||
* Subscribe to the 'block-hovered' event if currenct view is not mobile
|
||||
* Subscribe to the 'block-hovered' event if current view is not mobile
|
||||
*
|
||||
* @see https://github.com/codex-team/editor.js/issues/1972
|
||||
*/
|
||||
|
|
|
@ -11,6 +11,7 @@ import Tooltip from '../../utils/tooltip';
|
|||
import { ModuleConfig } from '../../../types-internal/module-config';
|
||||
import InlineTool from '../../tools/inline';
|
||||
import { CommonInternalSettings } from '../../tools/base';
|
||||
import { IconChevronDown } from '@codexteam/icons';
|
||||
|
||||
/**
|
||||
* Inline Toolbar elements
|
||||
|
@ -51,6 +52,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
inputField: 'cdx-input',
|
||||
focusedButton: 'ce-inline-tool--focused',
|
||||
conversionToggler: 'ce-inline-toolbar__dropdown',
|
||||
conversionTogglerArrow: 'ce-inline-toolbar__dropdown-arrow',
|
||||
conversionTogglerHidden: 'ce-inline-toolbar__dropdown--hidden',
|
||||
conversionTogglerContent: 'ce-inline-toolbar__dropdown-content',
|
||||
togglerAndButtonsWrapper: 'ce-inline-toolbar__toggler-and-button-wrapper',
|
||||
|
@ -66,7 +68,8 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
/**
|
||||
* Margin above/below the Toolbar
|
||||
*/
|
||||
private readonly toolbarVerticalMargin: number = 5;
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
private readonly toolbarVerticalMargin: number = _.isMobileScreen() ? 20 : 6;
|
||||
|
||||
/**
|
||||
* TODO: Get rid of this
|
||||
|
@ -280,7 +283,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
/**
|
||||
* Check if node is contained by Inline Toolbar
|
||||
*
|
||||
* @param {Node} node — node to chcek
|
||||
* @param {Node} node — node to check
|
||||
*/
|
||||
public containsNode(node: Node): boolean {
|
||||
return this.nodes.wrapper.contains(node);
|
||||
|
@ -322,7 +325,7 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
const isClickedOnActionsWrapper = (event.target as Element).closest(`.${this.CSS.actionsWrapper}`);
|
||||
|
||||
// If click is on actions wrapper,
|
||||
// do not prevent default behaviour because actions might include interactive elements
|
||||
// do not prevent default behavior because actions might include interactive elements
|
||||
if (!isClickedOnActionsWrapper) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -428,10 +431,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
this.nodes.conversionToggler = $.make('div', this.CSS.conversionToggler);
|
||||
this.nodes.conversionTogglerContent = $.make('div', this.CSS.conversionTogglerContent);
|
||||
|
||||
const icon = $.svg('toggler-down', 13, 13);
|
||||
const iconWrapper = $.make('div', this.CSS.conversionTogglerArrow, {
|
||||
innerHTML: IconChevronDown,
|
||||
});
|
||||
|
||||
this.nodes.conversionToggler.appendChild(this.nodes.conversionTogglerContent);
|
||||
this.nodes.conversionToggler.appendChild(icon);
|
||||
this.nodes.conversionToggler.appendChild(iconWrapper);
|
||||
|
||||
this.nodes.togglerAndButtonsWrapper.appendChild(this.nodes.conversionToggler);
|
||||
|
||||
|
@ -454,10 +459,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
});
|
||||
});
|
||||
|
||||
this.tooltip.onHover(this.nodes.conversionToggler, I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'), {
|
||||
placement: 'top',
|
||||
hidingDelay: 100,
|
||||
});
|
||||
if (_.isMobileScreen() === false ) {
|
||||
this.tooltip.onHover(this.nodes.conversionToggler, I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'), {
|
||||
placement: 'top',
|
||||
hidingDelay: 100,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -581,10 +588,12 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
}));
|
||||
}
|
||||
|
||||
this.tooltip.onHover(button, tooltipContent, {
|
||||
placement: 'top',
|
||||
hidingDelay: 100,
|
||||
});
|
||||
if (_.isMobileScreen() === false ) {
|
||||
this.tooltip.onHover(button, tooltipContent, {
|
||||
placement: 'top',
|
||||
hidingDelay: 100,
|
||||
});
|
||||
}
|
||||
|
||||
instance.checkState(SelectionUtils.get());
|
||||
}
|
||||
|
@ -664,6 +673,15 @@ export default class InlineToolbar extends Module<InlineToolbarNodes> {
|
|||
|
||||
tool.surround(range);
|
||||
this.checkToolsState();
|
||||
|
||||
/**
|
||||
* If tool has "actions", so after click it will probably toggle them on.
|
||||
* For example, the Inline Link Tool will show the URL-input.
|
||||
* So we disable the Flipper for that case to allow Tool bind own Enter listener
|
||||
*/
|
||||
if (tool.renderActions !== undefined) {
|
||||
this.flipper.deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,17 +21,8 @@ import ToolsCollection from '../tools/collection';
|
|||
* Creates Instances from Plugins and binds external config to the instances
|
||||
*/
|
||||
|
||||
type ToolClass = BlockTool | InlineTool | BlockTune;
|
||||
|
||||
/**
|
||||
* Class properties:
|
||||
*
|
||||
* @typedef {Tools} Tools
|
||||
* @property {Tools[]} toolsAvailable - available Tools
|
||||
* @property {Tools[]} toolsUnavailable - unavailable Tools
|
||||
* @property {object} toolsClasses - all classes
|
||||
* @property {object} toolsSettings - Tools settings
|
||||
* @property {EditorConfig} config - Editor config
|
||||
* Modules that works with tools classes
|
||||
*/
|
||||
export default class Tools extends Module {
|
||||
/**
|
||||
|
@ -44,8 +35,6 @@ export default class Tools extends Module {
|
|||
|
||||
/**
|
||||
* Returns available Tools
|
||||
*
|
||||
* @returns {object<Tool>}
|
||||
*/
|
||||
public get available(): ToolsCollection {
|
||||
return this.toolsAvailable;
|
||||
|
@ -53,8 +42,6 @@ export default class Tools extends Module {
|
|||
|
||||
/**
|
||||
* Returns unavailable Tools
|
||||
*
|
||||
* @returns {Tool[]}
|
||||
*/
|
||||
public get unavailable(): ToolsCollection {
|
||||
return this.toolsUnavailable;
|
||||
|
@ -62,8 +49,6 @@ export default class Tools extends Module {
|
|||
|
||||
/**
|
||||
* Return Tools for the Inline Toolbar
|
||||
*
|
||||
* @returns {object} - object of Inline Tool's classes
|
||||
*/
|
||||
public get inlineTools(): ToolsCollection<InlineTool> {
|
||||
return this.available.inlineTools;
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
/* eslint-disable jsdoc/no-undefined-types */
|
||||
/**
|
||||
* Prebuilded sprite of SVG icons
|
||||
*/
|
||||
import sprite from '../../../dist/sprite.svg';
|
||||
|
||||
/**
|
||||
* Module UI
|
||||
*
|
||||
|
@ -16,6 +11,7 @@ import * as _ from '../utils';
|
|||
import Selection from '../selection';
|
||||
import Block from '../block';
|
||||
import Flipper from '../flipper';
|
||||
import { mobileScreenBreakpoint } from '../utils';
|
||||
|
||||
/**
|
||||
* HTML Elements used for UI
|
||||
|
@ -29,14 +25,12 @@ interface UINodes {
|
|||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @classdesc Makes Editor.js UI:
|
||||
* <codex-editor>
|
||||
* <ce-redactor />
|
||||
* <ce-toolbar />
|
||||
* <ce-inline-toolbar />
|
||||
* </codex-editor>
|
||||
*
|
||||
* @typedef {UI} UI
|
||||
* @property {EditorConfig} config - editor configuration {@link EditorJS#configuration}
|
||||
* @property {object} Editor - available editor modules {@link EditorJS#moduleInstances}
|
||||
|
@ -125,6 +119,7 @@ export default class UI extends Module<UINodes> {
|
|||
*/
|
||||
private resizeDebouncer: () => void = _.debounce(() => {
|
||||
this.windowResize();
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 200);
|
||||
|
||||
/**
|
||||
|
@ -163,11 +158,6 @@ export default class UI extends Module<UINodes> {
|
|||
*/
|
||||
this.addLoader();
|
||||
|
||||
/**
|
||||
* Append SVG sprite
|
||||
*/
|
||||
this.appendSVGSprite();
|
||||
|
||||
/**
|
||||
* Load and append CSS
|
||||
*/
|
||||
|
@ -235,12 +225,15 @@ export default class UI extends Module<UINodes> {
|
|||
return true;
|
||||
}
|
||||
|
||||
return Object.entries(this.Editor).filter(([moduleName, moduleClass]) => {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */
|
||||
return Object.entries(this.Editor).filter(([_moduleName, moduleClass]) => {
|
||||
return moduleClass.flipper instanceof Flipper;
|
||||
})
|
||||
.some(([moduleName, moduleClass]) => {
|
||||
.some(([_moduleName, moduleClass]) => {
|
||||
return moduleClass.flipper.hasFocus();
|
||||
});
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars, no-unused-vars */
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -266,7 +259,7 @@ export default class UI extends Module<UINodes> {
|
|||
* Check for mobile mode and cache a result
|
||||
*/
|
||||
private checkIsMobile(): void {
|
||||
this.isMobile = window.innerWidth < 650;
|
||||
this.isMobile = window.innerWidth < mobileScreenBreakpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -364,8 +357,8 @@ export default class UI extends Module<UINodes> {
|
|||
/**
|
||||
* Handle selection change to manipulate Inline Toolbar appearance
|
||||
*/
|
||||
this.readOnlyMutableListeners.on(document, 'selectionchange', (event: Event) => {
|
||||
this.selectionChanged(event);
|
||||
this.readOnlyMutableListeners.on(document, 'selectionchange', () => {
|
||||
this.selectionChanged();
|
||||
}, true);
|
||||
|
||||
this.readOnlyMutableListeners.on(window, 'resize', () => {
|
||||
|
@ -412,6 +405,7 @@ export default class UI extends Module<UINodes> {
|
|||
this.eventsDispatcher.emit(this.events.blockHovered, {
|
||||
block: this.Editor.BlockManager.getBlockByChildNode(hoveredBlock),
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 20), {
|
||||
passive: true,
|
||||
});
|
||||
|
@ -726,7 +720,6 @@ export default class UI extends Module<UINodes> {
|
|||
* All clicks on the redactor zone
|
||||
*
|
||||
* @param {MouseEvent} event - click event
|
||||
*
|
||||
* @description
|
||||
* - By clicks on the Editor's bottom zone:
|
||||
* - if last Block is empty, set a Caret to this
|
||||
|
@ -804,10 +797,8 @@ export default class UI extends Module<UINodes> {
|
|||
/**
|
||||
* Handle selection changes on mobile devices
|
||||
* Uses for showing the Inline Toolbar
|
||||
*
|
||||
* @param {Event} event - selection event
|
||||
*/
|
||||
private selectionChanged(event: Event): void {
|
||||
private selectionChanged(): void {
|
||||
const { CrossBlockSelection, BlockSelection } = this.Editor;
|
||||
const focusedElement = Selection.anchorElement;
|
||||
|
||||
|
@ -874,17 +865,4 @@ export default class UI extends Module<UINodes> {
|
|||
*/
|
||||
this.Editor.InlineToolbar.tryToShow(true, isNeedToShowConversionToolbar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append prebuilt sprite with SVG icons
|
||||
*/
|
||||
private appendSVGSprite(): void {
|
||||
const spriteHolder = $.make('div');
|
||||
|
||||
spriteHolder.hidden = true;
|
||||
spriteHolder.style.display = 'none';
|
||||
spriteHolder.innerHTML = sprite;
|
||||
|
||||
$.append(this.nodes.wrapper, spriteHolder);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ interface Element {
|
|||
* otherwise, returns false.
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill}
|
||||
*
|
||||
* @param {string} s - selector
|
||||
*/
|
||||
if (!Element.prototype.matches) {
|
||||
|
@ -46,7 +45,6 @@ if (!Element.prototype.matches) {
|
|||
* If there isn't such an ancestor, it returns null.
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill}
|
||||
*
|
||||
* @param {string} s - selector
|
||||
*/
|
||||
if (!Element.prototype.closest) {
|
||||
|
@ -76,7 +74,6 @@ if (!Element.prototype.closest) {
|
|||
* DOMString objects are inserted as equivalent Text nodes.
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/prepend#Polyfill}
|
||||
*
|
||||
* @param {Node | Node[] | string | string[]} nodes - nodes to prepend
|
||||
*/
|
||||
if (!Element.prototype.prepend) {
|
||||
|
|
|
@ -146,7 +146,7 @@ export default class SelectionUtils {
|
|||
/**
|
||||
* Check if passed selection is at Editor's zone
|
||||
*
|
||||
* @param selection - Selectoin object to check
|
||||
* @param selection - Selection object to check
|
||||
*/
|
||||
public static isSelectionAtEditor(selection: Selection): boolean {
|
||||
if (!selection) {
|
||||
|
@ -326,8 +326,6 @@ export default class SelectionUtils {
|
|||
*
|
||||
* @param element - element where to set focus
|
||||
* @param offset - offset of cursor
|
||||
*
|
||||
* @returns {DOMRect} of range
|
||||
*/
|
||||
public static setCursor(element: HTMLElement, offset = 0): DOMRect {
|
||||
const range = document.createRange();
|
||||
|
@ -452,7 +450,6 @@ export default class SelectionUtils {
|
|||
* @param {string} tagName - tag to found
|
||||
* @param {string} [className] - tag's class name
|
||||
* @param {number} [searchDepth] - count of tags that can be included. For better performance.
|
||||
*
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
public findParentTag(tagName: string, className?: string, searchDepth = 10): HTMLElement | null {
|
||||
|
@ -526,7 +523,7 @@ export default class SelectionUtils {
|
|||
/**
|
||||
* Expands selection range to the passed parent node
|
||||
*
|
||||
* @param {HTMLElement} element - element which contents should be selcted
|
||||
* @param {HTMLElement} element - element which contents should be selected
|
||||
*/
|
||||
public expandToTag(element: HTMLElement): void {
|
||||
const selection = window.getSelection();
|
||||
|
|
|
@ -67,11 +67,11 @@ export enum CommonInternalSettings {
|
|||
}
|
||||
|
||||
/**
|
||||
* Enum of Tool optoins provided by Block Tool
|
||||
* Enum of Tool options provided by Block Tool
|
||||
*/
|
||||
export enum InternalBlockToolSettings {
|
||||
/**
|
||||
* Is linebreaks enabled for Tool
|
||||
* Is line breaks enabled for Tool
|
||||
*/
|
||||
IsEnabledLineBreaks = 'enableLineBreaks',
|
||||
/**
|
||||
|
@ -116,7 +116,7 @@ export enum InternalTuneSettings {
|
|||
IsTune = 'isTune',
|
||||
}
|
||||
|
||||
export type ToolOptions = Omit<ToolSettings, 'class'>
|
||||
export type ToolOptions = Omit<ToolSettings, 'class'>;
|
||||
|
||||
interface ConstructorOptions {
|
||||
name: string;
|
||||
|
@ -174,8 +174,7 @@ export default abstract class BaseTool<Type extends Tool = Tool> {
|
|||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {ConstructorOptions} - Constructor options
|
||||
* @param {ConstructorOptions} options - Constructor options
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
|
|
|
@ -29,7 +29,6 @@ export default class ToolsFactory {
|
|||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param config - tools config
|
||||
* @param editorConfig - EditorJS config
|
||||
* @param api - EditorJS API module
|
||||
|
|
|
@ -3,9 +3,9 @@ import { BlockToolAPI } from '../block';
|
|||
import Shortcuts from '../utils/shortcuts';
|
||||
import BlockTool from '../tools/block';
|
||||
import ToolsCollection from '../tools/collection';
|
||||
import { API, BlockToolData, ToolboxConfigEntry } from '../../../types';
|
||||
import { API, BlockToolData, ToolboxConfigEntry, PopoverItem } from '../../../types';
|
||||
import EventsDispatcher from '../utils/events';
|
||||
import Popover, { PopoverEvent, PopoverItem } from '../utils/popover';
|
||||
import Popover, { PopoverEvent } from '../utils/popover';
|
||||
import I18n from '../i18n';
|
||||
import { I18nInternalNS } from '../i18n/namespace-internal';
|
||||
|
||||
|
@ -36,7 +36,7 @@ export enum ToolboxEvent {
|
|||
/**
|
||||
* Available i18n dict keys that should be passed to the constructor
|
||||
*/
|
||||
type toolboxTextLabelsKeys = 'filter' | 'nothingFound';
|
||||
type ToolboxTextLabelsKeys = 'filter' | 'nothingFound';
|
||||
|
||||
/**
|
||||
* Toolbox
|
||||
|
@ -70,7 +70,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
/**
|
||||
* Popover instance. There is a util for vertical lists.
|
||||
*/
|
||||
private popover: Popover;
|
||||
private popover: Popover | undefined;
|
||||
|
||||
/**
|
||||
* List of Tools available. Some of them will be shown in the Toolbox
|
||||
|
@ -80,34 +80,28 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
/**
|
||||
* Text labels used in the Toolbox. Should be passed from the i18n module
|
||||
*/
|
||||
private i18nLabels: Record<toolboxTextLabelsKeys, string>;
|
||||
private i18nLabels: Record<ToolboxTextLabelsKeys, string>;
|
||||
|
||||
/**
|
||||
* Current module HTML Elements
|
||||
*/
|
||||
private nodes: {
|
||||
toolbox: HTMLElement;
|
||||
toolbox: HTMLElement | null;
|
||||
} = {
|
||||
toolbox: null,
|
||||
};
|
||||
toolbox: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* CSS styles
|
||||
*
|
||||
* @returns {object.<string, string>}
|
||||
* @returns {Object<string, string>}
|
||||
*/
|
||||
private static get CSS(): { [name: string]: string } {
|
||||
return {
|
||||
toolbox: 'ce-toolbox',
|
||||
toolboxOpenedTop: 'ce-toolbox--opened-top',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Id of listener added used to remove it on destroy()
|
||||
*/
|
||||
private clickListenerId: string = null;
|
||||
|
||||
/**
|
||||
* Toolbox constructor
|
||||
*
|
||||
|
@ -115,7 +109,7 @@ 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, i18nLabels }: {api: API; tools: ToolsCollection<BlockTool>; i18nLabels: Record<toolboxTextLabelsKeys, string>}) {
|
||||
constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection<BlockTool>; i18nLabels: Record<ToolboxTextLabelsKeys, string>}) {
|
||||
super();
|
||||
|
||||
this.api = api;
|
||||
|
@ -128,6 +122,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
*/
|
||||
public make(): Element {
|
||||
this.popover = new Popover({
|
||||
scopeElement: this.api.ui.nodes.redactor,
|
||||
className: Toolbox.CSS.toolbox,
|
||||
searchable: true,
|
||||
filterLabel: this.i18nLabels.filter,
|
||||
|
@ -150,8 +145,8 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
/**
|
||||
* Returns true if the Toolbox has the Flipper activated and the Flipper has selected button
|
||||
*/
|
||||
public hasFocus(): boolean {
|
||||
return this.popover.hasFocus();
|
||||
public hasFocus(): boolean | undefined {
|
||||
return this.popover?.hasFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -165,10 +160,8 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
this.nodes.toolbox = null;
|
||||
}
|
||||
|
||||
this.api.listeners.offById(this.clickListenerId);
|
||||
|
||||
this.removeAllShortcuts();
|
||||
this.popover.off(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
||||
this.popover?.off(PopoverEvent.OverlayClicked, this.onOverlayClicked);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -189,16 +182,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.popover?.show();
|
||||
this.opened = true;
|
||||
this.emit(ToolboxEvent.Opened);
|
||||
}
|
||||
|
@ -207,9 +191,8 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
* Close Toolbox
|
||||
*/
|
||||
public close(): void {
|
||||
this.popover.hide();
|
||||
this.popover?.hide();
|
||||
this.opened = false;
|
||||
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpenedTop);
|
||||
this.emit(ToolboxEvent.Closed);
|
||||
}
|
||||
|
||||
|
@ -224,51 +207,29 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there popover should be opened downwards.
|
||||
* It happens in case there is enough space below or not enough space above
|
||||
*/
|
||||
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())
|
||||
.reduce((result, tool) => {
|
||||
const toolToolboxSettings = tool.toolbox;
|
||||
const result: BlockTool[] = [];
|
||||
|
||||
if (toolToolboxSettings) {
|
||||
const validToolboxSettings = toolToolboxSettings.filter(item => {
|
||||
return this.areToolboxSettingsValid(item, tool.name);
|
||||
});
|
||||
this.tools.forEach((tool) => {
|
||||
const toolToolboxSettings = tool.toolbox;
|
||||
|
||||
result.push({
|
||||
...tool,
|
||||
toolbox: validToolboxSettings,
|
||||
});
|
||||
}
|
||||
if (toolToolboxSettings) {
|
||||
result.push(tool);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -284,7 +245,7 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
icon: toolboxItem.icon,
|
||||
label: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)),
|
||||
name: tool.name,
|
||||
onClick: (e): void => {
|
||||
onActivate: (): void => {
|
||||
this.toolButtonActivated(tool.name, toolboxItem.data);
|
||||
},
|
||||
secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '',
|
||||
|
@ -292,12 +253,12 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
};
|
||||
|
||||
return this.toolsToBeDisplayed
|
||||
.reduce((result, tool) => {
|
||||
.reduce<PopoverItem[]>((result, tool) => {
|
||||
if (Array.isArray(tool.toolbox)) {
|
||||
tool.toolbox.forEach(item => {
|
||||
result.push(toPopoverItem(item, tool));
|
||||
});
|
||||
} else {
|
||||
} else if (tool.toolbox !== undefined) {
|
||||
result.push(toPopoverItem(tool.toolbox, tool));
|
||||
}
|
||||
|
||||
|
@ -305,29 +266,6 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
|
|||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates tool's toolbox settings
|
||||
*
|
||||
* @param toolToolboxSettings - item to validate
|
||||
* @param toolName - name of the tool used in console warning if item is not valid
|
||||
*/
|
||||
private areToolboxSettingsValid(toolToolboxSettings: ToolboxConfigEntry, toolName: string): boolean {
|
||||
/**
|
||||
* 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', toolName);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate all tools and enable theirs shortcuts if specified
|
||||
*/
|
||||
|
|
|
@ -24,7 +24,6 @@ declare const VERSION: string;
|
|||
* @typedef {object} ChainData
|
||||
* @property {object} data - data that will be passed to the success or fallback
|
||||
* @property {Function} function - function's that must be called asynchronously
|
||||
*
|
||||
* @interface ChainData
|
||||
*/
|
||||
export interface ChainData {
|
||||
|
@ -38,7 +37,7 @@ export interface ChainData {
|
|||
*/
|
||||
|
||||
/**
|
||||
* Returns basic keycodes as constants
|
||||
* Returns basic key codes as constants
|
||||
*
|
||||
* @returns {{}}
|
||||
*/
|
||||
|
@ -178,7 +177,6 @@ export const logLabeled = _log.bind(window, true);
|
|||
* Return string representation of the object type
|
||||
*
|
||||
* @param {*} object - object to get type
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -190,7 +188,6 @@ export function typeOf(object: any): string {
|
|||
* Check if passed variable is a function
|
||||
*
|
||||
* @param {*} fn - function to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -202,7 +199,6 @@ export function isFunction(fn: any): fn is (...args: any[]) => any {
|
|||
* Checks if passed argument is an object
|
||||
*
|
||||
* @param {*} v - object to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -214,7 +210,6 @@ export function isObject(v: any): v is object {
|
|||
* Checks if passed argument is a string
|
||||
*
|
||||
* @param {*} v - variable to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -226,7 +221,6 @@ export function isString(v: any): v is string {
|
|||
* Checks if passed argument is boolean
|
||||
*
|
||||
* @param {*} v - variable to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -238,7 +232,6 @@ export function isBoolean(v: any): v is boolean {
|
|||
* Checks if passed argument is number
|
||||
*
|
||||
* @param {*} v - variable to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -250,7 +243,6 @@ export function isNumber(v: any): v is number {
|
|||
* Checks if passed argument is undefined
|
||||
*
|
||||
* @param {*} v - variable to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -262,7 +254,6 @@ export function isUndefined(v: any): v is undefined {
|
|||
* Check if passed function is a class
|
||||
*
|
||||
* @param {Function} fn - function to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -274,7 +265,6 @@ export function isClass(fn: any): boolean {
|
|||
* Checks if object is empty
|
||||
*
|
||||
* @param {object} object - object to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isEmpty(object: object): boolean {
|
||||
|
@ -296,22 +286,23 @@ export function isPromise(object: any): object is Promise<any> {
|
|||
return Promise.resolve(object) === object;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
/**
|
||||
* Returns true if passed key code is printable (a-Z, 0-9, etc) character.
|
||||
*
|
||||
* @param {number} keyCode - key code
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrintableKey(keyCode: number): boolean {
|
||||
return (keyCode > 47 && keyCode < 58) || // number keys
|
||||
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)
|
||||
keyCode === 32 || keyCode === 13 || // Space bar & return key(s)
|
||||
keyCode === 229 || // processing key input for certain languages — Chinese, Japanese, etc.
|
||||
(keyCode > 64 && keyCode < 91) || // letter keys
|
||||
(keyCode > 95 && keyCode < 112) || // Numpad keys
|
||||
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
|
||||
(keyCode > 218 && keyCode < 223); // [\]' (in order)
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-magic-numbers */
|
||||
|
||||
/**
|
||||
* Fires a promise sequence asynchronously
|
||||
|
@ -319,7 +310,6 @@ export function isPrintableKey(keyCode: number): boolean {
|
|||
* @param {ChainData[]} chains - list or ChainData's
|
||||
* @param {Function} success - success callback
|
||||
* @param {Function} fallback - callback that fires in case of errors
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function sequence(
|
||||
|
@ -333,10 +323,8 @@ export async function sequence(
|
|||
* Decorator
|
||||
*
|
||||
* @param {ChainData} chainData - Chain data
|
||||
*
|
||||
* @param {Function} successCallback - success callback
|
||||
* @param {Function} fallbackCallback - fail callback
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function waitNextBlock(
|
||||
|
@ -370,7 +358,6 @@ export async function sequence(
|
|||
* Make array from array-like collection
|
||||
*
|
||||
* @param {ArrayLike} collection - collection to convert to array
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -400,7 +387,6 @@ export function delay(method: (...args: any[]) => any, timeout: number) {
|
|||
* Get file extension
|
||||
*
|
||||
* @param {File} file - file
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFileExtension(file: File): string {
|
||||
|
@ -411,7 +397,6 @@ export function getFileExtension(file: File): string {
|
|||
* Check if string is MIME type
|
||||
*
|
||||
* @param {string} type - string to check
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isValidMimeType(type: string): boolean {
|
||||
|
@ -492,6 +477,7 @@ export function throttle(func, wait, options: {leading?: boolean; trailing?: boo
|
|||
|
||||
const remaining = wait - (now - previous);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
context = this;
|
||||
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
|
@ -551,7 +537,7 @@ export function getUserOS(): {[key: string]: boolean} {
|
|||
linux: false,
|
||||
};
|
||||
|
||||
const userOS = Object.keys(OS).find((os: string) => navigator.appVersion.toLowerCase().indexOf(os) !== -1);
|
||||
const userOS = Object.keys(OS).find((os: string) => window.navigator.appVersion.toLowerCase().indexOf(os) !== -1);
|
||||
|
||||
if (userOS) {
|
||||
OS[userOS] = true;
|
||||
|
@ -566,7 +552,6 @@ export function getUserOS(): {[key: string]: boolean} {
|
|||
* Capitalizes first letter of the string
|
||||
*
|
||||
* @param {string} text - text to capitalize
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function capitalize(text: string): string {
|
||||
|
@ -610,7 +595,6 @@ export function deepMerge<T extends object>(target, ...sources): T {
|
|||
* To detect touch devices more carefully, use 'touchstart' event listener
|
||||
*
|
||||
* @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isTouchSupported: boolean = 'ontouchstart' in document.documentElement;
|
||||
|
@ -674,7 +658,9 @@ export function getValidUrl(url: string): string {
|
|||
* @returns {string}
|
||||
*/
|
||||
export function generateBlockId(): string {
|
||||
return nanoid(10);
|
||||
const idLen = 10;
|
||||
|
||||
return nanoid(idLen);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -690,11 +676,10 @@ export function openTab(url: string): void {
|
|||
* Returns random generated identifier
|
||||
*
|
||||
* @param {string} prefix - identifier prefix
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function generateId(prefix = ''): string {
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
return `${prefix}${(Math.floor(Math.random() * 1e8)).toString(16)}`;
|
||||
}
|
||||
|
||||
|
@ -761,7 +746,12 @@ export function cacheable<Target, Value, Arguments extends unknown[] = unknown[]
|
|||
}
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* All screens below this width will be treated as mobile;
|
||||
*/
|
||||
export const mobileScreenBreakpoint = 650;
|
||||
|
||||
/**
|
||||
* Deep copy function.
|
||||
|
@ -790,7 +780,7 @@ export function deepCopy<T extends Record<keyof T, unknown>>(target: T): T {
|
|||
* True if screen has mobile size
|
||||
*/
|
||||
export function isMobileScreen(): boolean {
|
||||
return window.matchMedia('(max-width: 650px)').matches;
|
||||
return window.matchMedia(`(max-width: ${mobileScreenBreakpoint}px)`).matches;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,9 +7,7 @@ import { isEmpty } from '../utils';
|
|||
* - {Function} on - appends subscriber to the event. If event doesn't exist - creates new one
|
||||
* - {Function} emit - fires all subscribers with data
|
||||
* - {Function off - unsubscribes callback
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @typedef {Events} Events
|
||||
* @property {object} subscribers - all subscribers grouped by event name
|
||||
*/
|
||||
|
|
|
@ -62,8 +62,6 @@ export default class Listeners {
|
|||
* @param {string} eventType - event type
|
||||
* @param {Function} handler - method that will be fired on event
|
||||
* @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
public on(
|
||||
element: EventTarget,
|
||||
|
@ -104,6 +102,7 @@ export default class Listeners {
|
|||
element: EventTarget,
|
||||
eventType: string,
|
||||
handler?: (event: Event) => void,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
options?: boolean | AddEventListenerOptions
|
||||
): void {
|
||||
const existingListeners = this.findAll(element, eventType, handler);
|
||||
|
@ -140,7 +139,6 @@ export default class Listeners {
|
|||
* @param {EventTarget} element - event target
|
||||
* @param {string} [eventType] - event type
|
||||
* @param {Function} [handler] - event handler
|
||||
*
|
||||
* @returns {ListenerData|null}
|
||||
*/
|
||||
public findOne(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData {
|
||||
|
@ -155,7 +153,6 @@ export default class Listeners {
|
|||
* @param {EventTarget} element - event target
|
||||
* @param {string} eventType - event type
|
||||
* @param {Function} handler - event handler
|
||||
*
|
||||
* @returns {ListenerData[]}
|
||||
*/
|
||||
public findAll(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData[] {
|
||||
|
@ -195,7 +192,6 @@ export default class Listeners {
|
|||
* Search method: looks for listener by passed element
|
||||
*
|
||||
* @param {EventTarget} element - searching element
|
||||
*
|
||||
* @returns {Array} listeners that found on element
|
||||
*/
|
||||
private findByEventTarget(element: EventTarget): ListenerData[] {
|
||||
|
@ -210,7 +206,6 @@ export default class Listeners {
|
|||
* Search method: looks for listener by passed event type
|
||||
*
|
||||
* @param {string} eventType - event type
|
||||
*
|
||||
* @returns {ListenerData[]} listeners that found on element
|
||||
*/
|
||||
private findByType(eventType: string): ListenerData[] {
|
||||
|
@ -225,7 +220,6 @@ export default class Listeners {
|
|||
* Search method: looks for listener by passed handler
|
||||
*
|
||||
* @param {Function} handler - event handler
|
||||
*
|
||||
* @returns {ListenerData[]} listeners that found on element
|
||||
*/
|
||||
private findByHandler(handler: (event: Event) => void): ListenerData[] {
|
||||
|
@ -240,7 +234,6 @@ export default class Listeners {
|
|||
* Returns listener data found by id
|
||||
*
|
||||
* @param {string} id - listener identifier
|
||||
*
|
||||
* @returns {ListenerData}
|
||||
*/
|
||||
private findById(id: string): ListenerData {
|
||||
|
|
|
@ -5,39 +5,7 @@ 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;
|
||||
}
|
||||
import { PopoverItem, PopoverItemWithConfirmation } from '../../../types';
|
||||
|
||||
/**
|
||||
* Event that can be triggered by the Popover
|
||||
|
@ -47,17 +15,37 @@ export enum PopoverEvent {
|
|||
* When popover overlay is clicked
|
||||
*/
|
||||
OverlayClicked = 'overlay-clicked',
|
||||
|
||||
/**
|
||||
* When popover closes
|
||||
*/
|
||||
Close = 'close'
|
||||
}
|
||||
|
||||
/**
|
||||
* Popover is the UI element for displaying vertical lists
|
||||
*/
|
||||
export default class Popover extends EventsDispatcher<PopoverEvent> {
|
||||
/**
|
||||
* Flipper - module for keyboard iteration between elements
|
||||
*/
|
||||
public flipper: Flipper;
|
||||
|
||||
/**
|
||||
* Items list to be displayed
|
||||
*/
|
||||
private readonly items: PopoverItem[];
|
||||
|
||||
/**
|
||||
* Arbitrary html element to be inserted before items list
|
||||
*/
|
||||
private readonly customContent: HTMLElement;
|
||||
|
||||
/**
|
||||
* List of html elements inside custom content area that should be available for keyboard navigation
|
||||
*/
|
||||
private readonly customContentFlippableItems: HTMLElement[] = [];
|
||||
|
||||
/**
|
||||
* Stores the visibility state.
|
||||
*/
|
||||
|
@ -73,12 +61,12 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
nothingFound: HTMLElement;
|
||||
overlay: HTMLElement;
|
||||
} = {
|
||||
wrapper: null,
|
||||
popover: null,
|
||||
items: null,
|
||||
nothingFound: null,
|
||||
overlay: null,
|
||||
}
|
||||
wrapper: null,
|
||||
popover: null,
|
||||
items: null,
|
||||
nothingFound: null,
|
||||
overlay: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Additional wrapper's class name
|
||||
|
@ -90,11 +78,6 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
*/
|
||||
private listeners: Listeners;
|
||||
|
||||
/**
|
||||
* Flipper - module for keyboard iteration between elements
|
||||
*/
|
||||
private flipper: Flipper;
|
||||
|
||||
/**
|
||||
* Pass true to enable local search field
|
||||
*/
|
||||
|
@ -118,20 +101,27 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
/**
|
||||
* Style classes
|
||||
*/
|
||||
private static get CSS(): {
|
||||
public static get CSS(): {
|
||||
popover: string;
|
||||
popoverOpened: string;
|
||||
itemsWrapper: string;
|
||||
item: string;
|
||||
itemHidden: string;
|
||||
itemFocused: string;
|
||||
itemActive: string;
|
||||
itemDisabled: string;
|
||||
itemLabel: string;
|
||||
itemIcon: string;
|
||||
itemSecondaryLabel: string;
|
||||
itemConfirmation: string;
|
||||
itemNoHover: string;
|
||||
itemNoFocus: string;
|
||||
noFoundMessage: string;
|
||||
noFoundMessageShown: string;
|
||||
popoverOverlay: string;
|
||||
popoverOverlayHidden: string;
|
||||
customContent: string;
|
||||
customContentHidden: string;
|
||||
} {
|
||||
return {
|
||||
popover: 'ce-popover',
|
||||
|
@ -140,6 +130,11 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
item: 'ce-popover__item',
|
||||
itemHidden: 'ce-popover__item--hidden',
|
||||
itemFocused: 'ce-popover__item--focused',
|
||||
itemActive: 'ce-popover__item--active',
|
||||
itemDisabled: 'ce-popover__item--disabled',
|
||||
itemConfirmation: 'ce-popover__item--confirmation',
|
||||
itemNoHover: 'ce-popover__item--no-visible-hover',
|
||||
itemNoFocus: 'ce-popover__item--no-visible-focus',
|
||||
itemLabel: 'ce-popover__item-label',
|
||||
itemIcon: 'ce-popover__item-icon',
|
||||
itemSecondaryLabel: 'ce-popover__item-secondary-label',
|
||||
|
@ -147,13 +142,25 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
noFoundMessageShown: 'ce-popover__no-found--shown',
|
||||
popoverOverlay: 'ce-popover__overlay',
|
||||
popoverOverlayHidden: 'ce-popover__overlay--hidden',
|
||||
customContent: 'ce-popover__custom-content',
|
||||
customContentHidden: 'ce-popover__custom-content--hidden',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ScrollLocker instance
|
||||
*/
|
||||
private scrollLocker = new ScrollLocker()
|
||||
private scrollLocker = new ScrollLocker();
|
||||
|
||||
/**
|
||||
* Editor container element
|
||||
*/
|
||||
private scopeElement: HTMLElement;
|
||||
|
||||
/**
|
||||
* Stores data on popover items that are in confirmation state
|
||||
*/
|
||||
private itemsRequiringConfirmation: { [itemIndex: number]: PopoverItem } = {};
|
||||
|
||||
/**
|
||||
* Creates the Popover
|
||||
|
@ -163,19 +170,28 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
* @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
|
||||
* @param options.customContent - arbitrary html element to be inserted before items list
|
||||
* @param options.customContentFlippableItems - list of html elements inside custom content area that should be available for keyboard navigation
|
||||
* @param options.scopeElement - editor container element
|
||||
*/
|
||||
constructor({ items, className, searchable, filterLabel, nothingFoundLabel }: {
|
||||
constructor({ items, className, searchable, filterLabel, nothingFoundLabel, customContent, customContentFlippableItems, scopeElement }: {
|
||||
items: PopoverItem[];
|
||||
className?: string;
|
||||
searchable?: boolean;
|
||||
filterLabel: string;
|
||||
nothingFoundLabel: string;
|
||||
customContent?: HTMLElement;
|
||||
customContentFlippableItems?: HTMLElement[];
|
||||
scopeElement: HTMLElement;
|
||||
}) {
|
||||
super();
|
||||
this.items = items;
|
||||
this.customContent = customContent;
|
||||
this.customContentFlippableItems = customContentFlippableItems;
|
||||
this.className = className || '';
|
||||
this.searchable = searchable;
|
||||
this.listeners = new Listeners();
|
||||
this.scopeElement = scopeElement;
|
||||
|
||||
this.filterLabel = filterLabel;
|
||||
this.nothingFoundLabel = nothingFoundLabel;
|
||||
|
@ -195,20 +211,33 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
* Shows the Popover
|
||||
*/
|
||||
public show(): void {
|
||||
/**
|
||||
* Open the popover above the button
|
||||
* if there is not enough available space below it
|
||||
*/
|
||||
if (!this.shouldOpenPopoverBottom) {
|
||||
this.nodes.wrapper.style.setProperty('--popover-height', this.calculateHeight() + 'px');
|
||||
this.nodes.wrapper.classList.add(this.className + '--opened-top');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear search and items scrolling
|
||||
*/
|
||||
this.search.clear();
|
||||
if (this.search) {
|
||||
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();
|
||||
this.flipper.activate(this.flippableElements);
|
||||
|
||||
if (this.searchable) {
|
||||
window.requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
this.search.focus();
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
}, 100);
|
||||
}
|
||||
|
||||
if (isMobileScreen()) {
|
||||
|
@ -239,13 +268,31 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
}
|
||||
|
||||
this.isShown = false;
|
||||
this.nodes.wrapper.classList.remove(this.className + '--opened-top');
|
||||
|
||||
/**
|
||||
* Remove confirmation state from items
|
||||
*/
|
||||
const confirmationStateItems = Array.from(this.nodes.items.querySelectorAll(`.${Popover.CSS.itemConfirmation}`));
|
||||
|
||||
confirmationStateItems.forEach((itemEl: HTMLElement) => this.cleanUpConfirmationStateForItem(itemEl));
|
||||
|
||||
this.disableSpecialHoverAndFocusBehavior();
|
||||
|
||||
this.emit(PopoverEvent.Close);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears memory
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.flipper.deactivate();
|
||||
this.listeners.removeAll();
|
||||
this.disableSpecialHoverAndFocusBehavior();
|
||||
|
||||
if (isMobileScreen()) {
|
||||
this.scrollLocker.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -260,7 +307,7 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
* Renders invisible clone of popover to get actual height.
|
||||
*/
|
||||
@cacheable
|
||||
public calculateHeight(): number {
|
||||
private calculateHeight(): number {
|
||||
let height = 0;
|
||||
const popoverClone = this.nodes.popover.cloneNode(true) as HTMLElement;
|
||||
|
||||
|
@ -290,6 +337,11 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
this.addSearch(this.nodes.popover);
|
||||
}
|
||||
|
||||
if (this.customContent) {
|
||||
this.customContent.classList.add(Popover.CSS.customContent);
|
||||
this.nodes.popover.appendChild(this.customContent);
|
||||
}
|
||||
|
||||
this.nodes.items = Dom.make('div', Popover.CSS.itemsWrapper);
|
||||
this.items.forEach(item => {
|
||||
this.nodes.items.appendChild(this.createItem(item));
|
||||
|
@ -302,11 +354,11 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
|
||||
this.nodes.popover.appendChild(this.nodes.nothingFound);
|
||||
|
||||
this.listeners.on(this.nodes.popover, 'click', (event: KeyboardEvent|MouseEvent) => {
|
||||
this.listeners.on(this.nodes.popover, 'click', (event: PointerEvent) => {
|
||||
const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement;
|
||||
|
||||
if (clickedItem) {
|
||||
this.itemClicked(clickedItem);
|
||||
this.itemClicked(clickedItem, event as PointerEvent);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -325,27 +377,43 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
items: this.items,
|
||||
placeholder: this.filterLabel,
|
||||
onSearch: (filteredItems): void => {
|
||||
const itemsVisible = [];
|
||||
const searchResultElements = [];
|
||||
|
||||
this.items.forEach((item, index) => {
|
||||
const itemElement = this.nodes.items.children[index];
|
||||
|
||||
if (filteredItems.includes(item)) {
|
||||
itemsVisible.push(itemElement);
|
||||
searchResultElements.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);
|
||||
this.nodes.nothingFound.classList.toggle(Popover.CSS.noFoundMessageShown, searchResultElements.length === 0);
|
||||
|
||||
/**
|
||||
* Update flipper items with only visible
|
||||
* In order to make keyboard navigation work correctly, flipper should be reactivated with only visible items.
|
||||
* As custom html content is not displayed while search, it should be excluded from keyboard navigation.
|
||||
*/
|
||||
this.flipper.deactivate();
|
||||
this.flipper.activate(itemsVisible);
|
||||
this.flipper.focusFirst();
|
||||
const allItemsDisplayed = filteredItems.length === this.items.length;
|
||||
|
||||
/**
|
||||
* Contains list of elements available for keyboard navigation considering search query applied
|
||||
*/
|
||||
const flippableElements = allItemsDisplayed ? this.flippableElements : searchResultElements;
|
||||
|
||||
if (this.customContent) {
|
||||
this.customContent.classList.toggle(Popover.CSS.customContentHidden, !allItemsDisplayed);
|
||||
}
|
||||
|
||||
if (this.flipper.isActivated) {
|
||||
/**
|
||||
* Update flipper items with only visible
|
||||
*/
|
||||
this.reactivateFlipper(flippableElements);
|
||||
this.flipper.focusFirst();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -362,16 +430,16 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
private createItem(item: PopoverItem): HTMLElement {
|
||||
const el = Dom.make('div', Popover.CSS.item);
|
||||
|
||||
el.dataset.itemName = item.name;
|
||||
if (item.name) {
|
||||
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(Dom.make('div', Popover.CSS.itemIcon, {
|
||||
innerHTML: item.icon || item.name.substring(0, 1).toUpperCase(),
|
||||
}));
|
||||
|
||||
el.appendChild(label);
|
||||
|
||||
|
@ -381,6 +449,14 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
}));
|
||||
}
|
||||
|
||||
if (item.isActive) {
|
||||
el.classList.add(Popover.CSS.itemActive);
|
||||
}
|
||||
|
||||
if (item.isDisabled) {
|
||||
el.classList.add(Popover.CSS.itemDisabled);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
|
@ -388,23 +464,182 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
* Item click handler
|
||||
*
|
||||
* @param itemEl - clicked item
|
||||
* @param event - click event
|
||||
*/
|
||||
private itemClicked(itemEl: HTMLElement): void {
|
||||
const allItems = this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`);
|
||||
const itemIndex = Array.from(allItems).indexOf(itemEl);
|
||||
private itemClicked(itemEl: HTMLElement, event: PointerEvent): void {
|
||||
const allItems = Array.from(this.nodes.items.children);
|
||||
const itemIndex = allItems.indexOf(itemEl);
|
||||
const clickedItem = this.items[itemIndex];
|
||||
|
||||
clickedItem.onClick(clickedItem);
|
||||
if (clickedItem.isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is any other item in confirmation state except the clicked one, clean it up
|
||||
*/
|
||||
allItems
|
||||
.filter(item => item !== itemEl)
|
||||
.forEach(item => {
|
||||
this.cleanUpConfirmationStateForItem(item);
|
||||
});
|
||||
|
||||
if (clickedItem.confirmation) {
|
||||
this.enableConfirmationStateForItem(clickedItem as PopoverItemWithConfirmation, itemEl, itemIndex);
|
||||
|
||||
return;
|
||||
}
|
||||
clickedItem.onActivate(clickedItem, event);
|
||||
|
||||
if (clickedItem.toggle) {
|
||||
clickedItem.isActive = !clickedItem.isActive;
|
||||
itemEl.classList.toggle(Popover.CSS.itemActive);
|
||||
}
|
||||
|
||||
if (clickedItem.closeOnActivate) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables confirmation state for specified item.
|
||||
* Replaces item element in popover so that is becomes highlighted in a special way
|
||||
*
|
||||
* @param item - item to enable confirmation state for
|
||||
* @param itemEl - html element corresponding to the item
|
||||
* @param itemIndex - index of the item in all items list
|
||||
*/
|
||||
private enableConfirmationStateForItem(item: PopoverItemWithConfirmation, itemEl: HTMLElement, itemIndex: number): void {
|
||||
/** Save root item requiring confirmation to restore original state on popover hide */
|
||||
if (this.itemsRequiringConfirmation[itemIndex] === undefined) {
|
||||
this.itemsRequiringConfirmation[itemIndex] = item;
|
||||
}
|
||||
const newItemData = {
|
||||
...item,
|
||||
...item.confirmation,
|
||||
confirmation: item.confirmation.confirmation,
|
||||
} as PopoverItem;
|
||||
|
||||
this.items[itemIndex] = newItemData;
|
||||
|
||||
const confirmationStateItemEl = this.createItem(newItemData as PopoverItem);
|
||||
|
||||
confirmationStateItemEl.classList.add(Popover.CSS.itemConfirmation, ...Array.from(itemEl.classList));
|
||||
itemEl.parentElement.replaceChild(confirmationStateItemEl, itemEl);
|
||||
|
||||
this.enableSpecialHoverAndFocusBehavior(confirmationStateItemEl);
|
||||
|
||||
this.reactivateFlipper(
|
||||
this.flippableElements,
|
||||
this.flippableElements.indexOf(confirmationStateItemEl)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Brings specified element corresponding to popover item to its original state
|
||||
*
|
||||
* @param itemEl - item in confirmation state
|
||||
*/
|
||||
private cleanUpConfirmationStateForItem(itemEl: Element): void {
|
||||
const allItems = Array.from(this.nodes.items.children);
|
||||
const index = allItems.indexOf(itemEl);
|
||||
|
||||
const originalItem = this.itemsRequiringConfirmation[index];
|
||||
|
||||
if (originalItem === undefined) {
|
||||
return;
|
||||
}
|
||||
const originalStateItemEl = this.createItem(originalItem);
|
||||
|
||||
itemEl.parentElement.replaceChild(originalStateItemEl, itemEl);
|
||||
this.items[index] = originalItem;
|
||||
|
||||
delete this.itemsRequiringConfirmation[index];
|
||||
|
||||
itemEl.removeEventListener('mouseleave', this.removeSpecialHoverBehavior);
|
||||
this.disableSpecialHoverAndFocusBehavior();
|
||||
|
||||
this.reactivateFlipper(
|
||||
this.flippableElements,
|
||||
this.flippableElements.indexOf(originalStateItemEl)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables special focus and hover behavior for item in confirmation state.
|
||||
* This is needed to prevent item from being highlighted as hovered/focused just after click.
|
||||
*
|
||||
* @param item - html element of the item to enable special behavior for
|
||||
*/
|
||||
private enableSpecialHoverAndFocusBehavior(item: HTMLElement): void {
|
||||
item.classList.add(Popover.CSS.itemNoHover);
|
||||
item.classList.add(Popover.CSS.itemNoFocus);
|
||||
|
||||
item.addEventListener('mouseleave', this.removeSpecialHoverBehavior, { once: true });
|
||||
this.flipper.onFlip(this.onFlip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables special focus and hover behavior.
|
||||
*/
|
||||
private disableSpecialHoverAndFocusBehavior(): void {
|
||||
this.removeSpecialFocusBehavior();
|
||||
this.removeSpecialHoverBehavior();
|
||||
|
||||
this.flipper.removeOnFlip(this.onFlip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes class responsible for special hover behavior on an item
|
||||
*/
|
||||
private removeSpecialHoverBehavior = (): void => {
|
||||
const el = this.nodes.items.querySelector(`.${Popover.CSS.itemNoHover}`);
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.classList.remove(Popover.CSS.itemNoHover);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes class responsible for special focus behavior on an item
|
||||
*/
|
||||
private removeSpecialFocusBehavior(): void {
|
||||
const el = this.nodes.items.querySelector(`.${Popover.CSS.itemNoFocus}`);
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.classList.remove(Popover.CSS.itemNoFocus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on flipper navigation
|
||||
*/
|
||||
private onFlip = (): void => {
|
||||
this.disableSpecialHoverAndFocusBehavior();
|
||||
};
|
||||
|
||||
/**
|
||||
* Reactivates flipper instance.
|
||||
* Should be used if popover items html elements get replaced to preserve workability of keyboard navigation
|
||||
*
|
||||
* @param items - html elements to navigate through
|
||||
* @param focusedIndex - index of element to be focused
|
||||
*/
|
||||
private reactivateFlipper(items: HTMLElement[], focusedIndex?: number): void {
|
||||
this.flipper.deactivate();
|
||||
this.flipper.activate(items, focusedIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
items: this.flippableElements,
|
||||
focusedItemClass: Popover.CSS.itemFocused,
|
||||
allowedKeys: [
|
||||
keyCodes.TAB,
|
||||
|
@ -414,4 +649,37 @@ export default class Popover extends EventsDispatcher<PopoverEvent> {
|
|||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of elements available for keyboard navigation.
|
||||
* Contains both usual popover items elements and custom html content.
|
||||
*/
|
||||
private get flippableElements(): HTMLElement[] {
|
||||
/**
|
||||
* Select html elements of popover items
|
||||
*/
|
||||
const popoverItemsElements = Array.from(this.nodes.wrapper.querySelectorAll(`.${Popover.CSS.item}`)) as HTMLElement[];
|
||||
|
||||
const customContentControlsElements = this.customContentFlippableItems || [];
|
||||
|
||||
/**
|
||||
* Combine elements inside custom content area with popover items elements
|
||||
*/
|
||||
return customContentControlsElements.concat(popoverItemsElements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if popover should be opened bottom.
|
||||
* It should happen when there is enough space below or not enough space above
|
||||
*/
|
||||
private get shouldOpenPopoverBottom(): boolean {
|
||||
const toolboxRect = this.nodes.wrapper.getBoundingClientRect();
|
||||
const scopeElementRect = this.scopeElement.getBoundingClientRect();
|
||||
const popoverHeight = this.calculateHeight();
|
||||
const popoverPotentialBottomEdge = toolboxRect.top + popoverHeight;
|
||||
const popoverPotentialTopEdge = toolboxRect.top - popoverHeight;
|
||||
const bottomEdgeForComparison = Math.min(window.innerHeight, scopeElementRect.bottom);
|
||||
|
||||
return popoverPotentialTopEdge < scopeElementRect.top || popoverPotentialBottomEdge <= bottomEdgeForComparison;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* Clears HTML from taint tags
|
||||
*
|
||||
* @version 2.0.0
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* clean(yourTaintString, yourConfig);
|
||||
|
@ -18,7 +17,6 @@ import * as _ from '../utils';
|
|||
/**
|
||||
* @typedef {object} SanitizerConfig
|
||||
* @property {object} tags - define tags restrictions
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* tags : {
|
||||
|
@ -65,7 +63,6 @@ export function sanitizeBlocks(
|
|||
*
|
||||
* @param {string} taintString - taint string
|
||||
* @param {SanitizerConfig} customConfig - allowed tags
|
||||
*
|
||||
* @returns {string} clean HTML
|
||||
*/
|
||||
export function clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {
|
||||
|
@ -163,7 +160,6 @@ function cleanObject(object: object, rules: SanitizerConfig|{[field: string]: Sa
|
|||
*
|
||||
* @param {string} taintString - string to clean
|
||||
* @param {SanitizerConfig|boolean} rule - sanitizer rule
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string {
|
||||
|
|
|
@ -10,12 +10,12 @@ export default class ScrollLocker {
|
|||
private static CSS = {
|
||||
scrollLocked: 'ce-scroll-locked',
|
||||
scrollLockedHard: 'ce-scroll-locked--hard',
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores scroll position, used for hard scroll lock
|
||||
*/
|
||||
private scrollPosition: null|number
|
||||
private scrollPosition: null|number;
|
||||
|
||||
/**
|
||||
* Locks body element scroll
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Dom from '../dom';
|
||||
import Listeners from './listeners';
|
||||
import { IconSearch } from '@codexteam/icons';
|
||||
|
||||
/**
|
||||
* Item that could be searched
|
||||
|
@ -113,14 +114,14 @@ export default class SearchInput {
|
|||
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);
|
||||
const iconWrapper = Dom.make('div', SearchInput.CSS.icon, {
|
||||
innerHTML: IconSearch,
|
||||
});
|
||||
|
||||
this.input = Dom.make('input', SearchInput.CSS.input, {
|
||||
placeholder,
|
||||
}) as HTMLInputElement;
|
||||
|
||||
iconWrapper.appendChild(icon);
|
||||
this.wrapper.appendChild(iconWrapper);
|
||||
this.wrapper.appendChild(this.input);
|
||||
|
||||
|
|
|
@ -94,7 +94,6 @@ class Shortcuts {
|
|||
*
|
||||
* @param element - Element shorcut is set for
|
||||
* @param shortcut - shortcut name
|
||||
*
|
||||
* @returns {number} index - shortcut index if exist
|
||||
*/
|
||||
private findShortcut(element: Element, shortcut: string): Shortcut | void {
|
||||
|
|
|
@ -63,20 +63,7 @@
|
|||
}
|
||||
|
||||
&__icon {
|
||||
display: inline-flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--color-gray-border);
|
||||
border-radius: 3px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
background: #fff;
|
||||
|
||||
svg {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
}
|
||||
@apply --tool-icon;
|
||||
}
|
||||
|
||||
&--last {
|
||||
|
|
|
@ -36,21 +36,27 @@
|
|||
|
||||
/**
|
||||
* Settings
|
||||
* @deprecated - use tunes config instead of creating html element with controls
|
||||
*/
|
||||
.cdx-settings-button {
|
||||
@apply --toolbar-button;
|
||||
|
||||
&:not(:nth-child(3n+3)) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
&:nth-child(n+4) {
|
||||
margin-top: 3px;
|
||||
}
|
||||
min-width: var(--toolbox-buttons-size);
|
||||
min-height: var(--toolbox-buttons-size);
|
||||
|
||||
&--active {
|
||||
color: var(--color-active-icon);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (--mobile) {
|
||||
width: var(--toolbox-buttons-size--mobile);
|
||||
height: var(--toolbox-buttons-size--mobile);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,9 +104,11 @@
|
|||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #FBFCFE;
|
||||
box-shadow: 0 1px 3px 0 rgba(18,30,57,0.08);
|
||||
@media (--can-hover) {
|
||||
&:hover {
|
||||
background: #FBFCFE;
|
||||
box-shadow: 0 1px 3px 0 rgba(18,30,57,0.08);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
.ce-inline-toolbar {
|
||||
--y-offset: 8px;
|
||||
|
||||
@apply --overlay-pane;
|
||||
transform: translateX(-50%) translateY(8px) scale(0.9);
|
||||
transform: translateX(-50%) translateY(8px) scale(0.94);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: transform 150ms ease, opacity 250ms ease;
|
||||
|
@ -16,7 +18,7 @@
|
|||
}
|
||||
|
||||
&--left-oriented {
|
||||
transform: translateX(-23px) translateY(8px) scale(0.9);
|
||||
transform: translateX(-23px) translateY(8px) scale(0.94);
|
||||
}
|
||||
|
||||
&--left-oriented&--showed {
|
||||
|
@ -24,7 +26,7 @@
|
|||
}
|
||||
|
||||
&--right-oriented {
|
||||
transform: translateX(-100%) translateY(8px) scale(0.9);
|
||||
transform: translateX(-100%) translateY(8px) scale(0.94);
|
||||
margin-left: 23px;
|
||||
}
|
||||
|
||||
|
@ -50,35 +52,32 @@
|
|||
}
|
||||
|
||||
&__dropdown {
|
||||
display: inline-flex;
|
||||
height: var(--toolbar-buttons-size);
|
||||
padding: 0 9px 0 10px;
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
margin: 0 6px 0 -6px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid var(--color-gray-border);
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-light);
|
||||
@media (--can-hover) {
|
||||
&:hover {
|
||||
background: var(--bg-light);
|
||||
}
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-content{
|
||||
&-content,
|
||||
&-arrow {
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
|
||||
svg {
|
||||
height: 12px;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
.icon--toggler-down {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__shortcut {
|
||||
|
@ -90,19 +89,10 @@
|
|||
|
||||
.ce-inline-tool {
|
||||
@apply --toolbar-button;
|
||||
|
||||
border-radius: 0;
|
||||
line-height: normal;
|
||||
width: auto;
|
||||
padding: 0 5px !important;
|
||||
min-width: 24px;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&--link {
|
||||
.icon--unlink {
|
||||
|
@ -132,6 +122,13 @@
|
|||
display: none;
|
||||
font-weight: 500;
|
||||
border-top: 1px solid rgba(201,201,204,.48);
|
||||
-webkit-appearance: none;
|
||||
font-family: inherit;
|
||||
|
||||
@media (--mobile){
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--grayText);
|
||||
|
|
|
@ -17,11 +17,10 @@
|
|||
justify-content: center;
|
||||
margin-right: var(--icon-margin-right);
|
||||
|
||||
.icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
svg {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
color: var(--grayText);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
flex-direction: column;
|
||||
padding: 6px;
|
||||
min-width: 200px;
|
||||
width: 200px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
|
@ -42,12 +43,14 @@
|
|||
}
|
||||
|
||||
@media (--mobile) {
|
||||
--offset: 5px;
|
||||
|
||||
position: fixed;
|
||||
max-width: none;
|
||||
min-width: auto;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
bottom: calc(5px + env(safe-area-inset-bottom));
|
||||
min-width: calc(100% - var(--offset) * 2);
|
||||
left: var(--offset);
|
||||
right: var(--offset);
|
||||
bottom: calc(var(--offset) + env(safe-area-inset-bottom));
|
||||
top: auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
@ -64,19 +67,77 @@
|
|||
&__item {
|
||||
@apply --popover-button;
|
||||
|
||||
@media (--can-hover) {
|
||||
&:hover {
|
||||
|
||||
&:not(.ce-popover__item--no-visible-hover) {
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
|
||||
.ce-popover__item-icon {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
@apply --button-disabled;
|
||||
|
||||
.ce-popover__item-icon {
|
||||
box-shadow: 0 0 0 1px var(--color-line-gray);
|
||||
}
|
||||
}
|
||||
|
||||
&--focused {
|
||||
@apply --button-focused;
|
||||
&:not(.ce-popover__item--no-visible-focus) {
|
||||
@apply --button-focused;
|
||||
}
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--active {
|
||||
@apply --button-active;
|
||||
}
|
||||
|
||||
&--confirmation {
|
||||
background: var(--color-confirm);
|
||||
|
||||
.ce-popover__item-icon {
|
||||
color: var(--color-confirm);
|
||||
}
|
||||
|
||||
.ce-popover__item-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:not(.ce-popover__item--no-visible-hover) {
|
||||
@media (--can-hover) {
|
||||
&:hover {
|
||||
background: var(--color-confirm-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.ce-popover__item--no-visible-focus) {
|
||||
&.ce-popover__item--focused {
|
||||
background: var(--color-confirm-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&-icon {
|
||||
@apply --tool-icon;
|
||||
}
|
||||
|
||||
&-label {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 25px;
|
||||
|
@ -98,6 +159,12 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--confirmation, &--active, &--focused {
|
||||
.ce-popover__item-icon {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__no-found {
|
||||
|
@ -110,10 +177,6 @@
|
|||
&--shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (--mobile) {
|
||||
|
@ -141,4 +204,17 @@
|
|||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&__custom-content:not(:empty) {
|
||||
padding: 4px;
|
||||
|
||||
@media (--not-mobile) {
|
||||
margin-top: 5px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__custom-content--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +1,15 @@
|
|||
.ce-settings {
|
||||
@apply --overlay-pane;
|
||||
top: var(--toolbar-buttons-size);
|
||||
left: 0;
|
||||
min-width: 114px;
|
||||
box-sizing: content-box;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
--gap: 8px;
|
||||
|
||||
@media (--mobile){
|
||||
bottom: 40px;
|
||||
right: auto;
|
||||
top: auto;
|
||||
}
|
||||
@media (--not-mobile){
|
||||
position: absolute;
|
||||
top: calc(var(--toolbox-buttons-size) + var(--gap));
|
||||
left: 0;
|
||||
|
||||
&::before{
|
||||
left: auto;
|
||||
right: 12px;
|
||||
|
||||
@media (--mobile){
|
||||
bottom: -5px;
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
display: none;
|
||||
|
||||
&--opened {
|
||||
display: block;
|
||||
animation-duration: 0.1s;
|
||||
animation-name: panelShowing;
|
||||
}
|
||||
|
||||
&__plugin-zone {
|
||||
&:not(:empty){
|
||||
padding: 3px 3px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__default-zone {
|
||||
&:not(:empty){
|
||||
padding: 3px;
|
||||
&--opened-top {
|
||||
top: calc(-1 * (var(--gap) + var(--popover-height)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,28 +34,5 @@
|
|||
&--selected {
|
||||
color: var(--color-active-icon);
|
||||
}
|
||||
|
||||
&--delete {
|
||||
transition: background-color 300ms ease;
|
||||
will-change: background-color;
|
||||
|
||||
.icon {
|
||||
transition: transform 200ms ease-out;
|
||||
will-change: transform;
|
||||
}
|
||||
}
|
||||
|
||||
&--confirm {
|
||||
background-color: var(--color-confirm) !important;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: color-mod(var(--color-confirm) blackness(+5%)) !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,12 +57,12 @@
|
|||
&__settings-btn {
|
||||
@apply --toolbox-button;
|
||||
|
||||
margin-left: 5px;
|
||||
margin-left: 3px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
@media (--not-mobile){
|
||||
width: 18px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
|
@ -74,6 +74,14 @@
|
|||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
&__plus,
|
||||
&__settings-btn {
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -101,10 +101,12 @@
|
|||
}
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
vertical-align: middle;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
path {
|
||||
stroke: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,4 +139,4 @@
|
|||
top: calc(-1 * var(--window-scroll-offset));
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
/**
|
||||
* Gray border, loaders
|
||||
* @deprecated — use --color-line-gray instead
|
||||
*/
|
||||
--color-gray-border: rgba(201, 201, 204, 0.48);
|
||||
|
||||
|
@ -48,17 +49,19 @@
|
|||
*/
|
||||
--narrow-mode-right-padding: 50px;
|
||||
|
||||
/**
|
||||
* Toolbar buttons height and width
|
||||
*/
|
||||
--toolbar-buttons-size: 34px;
|
||||
|
||||
/**
|
||||
* Toolbar Plus Button and Toolbox buttons height and width
|
||||
*/
|
||||
--toolbox-buttons-size: 26px;
|
||||
--toolbox-buttons-size--mobile: 36px;
|
||||
|
||||
/**
|
||||
* Size of svg icons got from the CodeX Icons pack
|
||||
*/
|
||||
--icon-size: 20px;
|
||||
--icon-size--mobile: 28px;
|
||||
|
||||
|
||||
/**
|
||||
* The main `.cdx-block` wrapper has such vertical paddings
|
||||
* And the Block Actions toggler too
|
||||
|
@ -69,6 +72,9 @@
|
|||
* Confirm deletion bg
|
||||
*/
|
||||
--color-confirm: #E24A4A;
|
||||
--color-confirm-hover: #CE4343;
|
||||
|
||||
--color-line-gray: #EFF0F1;
|
||||
|
||||
--overlay-pane: {
|
||||
position: absolute;
|
||||
|
@ -78,11 +84,6 @@
|
|||
border-radius: 6px;
|
||||
z-index: 2;
|
||||
|
||||
@media (--mobile){
|
||||
box-shadow: 0 8px 6px -6px rgb(33 48 73 / 19%);
|
||||
border-bottom-color: #c7c7c7;
|
||||
}
|
||||
|
||||
&--left-oriented {
|
||||
&::before {
|
||||
left: 15px;
|
||||
|
@ -104,6 +105,17 @@
|
|||
background: rgba(34, 186, 255, 0.08) !important;
|
||||
};
|
||||
|
||||
--button-active: {
|
||||
background: rgba(56, 138, 229, 0.1);
|
||||
color: var(--color-active-icon);
|
||||
};
|
||||
|
||||
--button-disabled: {
|
||||
color: var(--grayText);
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Styles for Toolbox Buttons and Plus Button
|
||||
*/
|
||||
|
@ -144,22 +156,31 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--toolbar-buttons-size);
|
||||
height: var(--toolbar-buttons-size);
|
||||
line-height: var(--toolbar-buttons-size);
|
||||
padding: 0 !important;
|
||||
text-align: center;
|
||||
|
||||
padding: 6px 1px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
vertical-align: bottom;
|
||||
color: #000;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-light);
|
||||
svg {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
|
||||
@media (--mobile) {
|
||||
width: var(--icon-size--mobile);
|
||||
height: var(--icon-size--mobile);
|
||||
}
|
||||
}
|
||||
|
||||
@media (--can-hover) {
|
||||
&:hover {
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
|
@ -197,12 +218,6 @@
|
|||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
@media (--can-hover) {
|
||||
&:hover {
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
}
|
||||
|
||||
@media (--mobile) {
|
||||
font-size: 16px;
|
||||
padding: 4px;
|
||||
|
@ -216,24 +231,29 @@
|
|||
display: inline-flex;
|
||||
width: var(--toolbox-buttons-size);
|
||||
height: var(--toolbox-buttons-size);
|
||||
border: 1px solid var(--color-gray-border);
|
||||
box-shadow: 0 0 0 1px var(--color-gray-border);
|
||||
border-radius: 5px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
box-sizing: content-box;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
|
||||
svg {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
}
|
||||
|
||||
@media (--mobile) {
|
||||
width: var(--toolbox-buttons-size--mobile);
|
||||
height: var(--toolbox-buttons-size--mobile);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
svg {
|
||||
width: var(--icon-size--mobile);
|
||||
height: var(--icon-size--mobile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 21cbdea6e5e61094b046f47e8cb423a817cec3ed
|
||||
Subproject commit 6e45413ccdfd021f1800eb6e5bf7440184d5ab7c
|
|
@ -92,7 +92,7 @@ export default class Stub implements BlockTool {
|
|||
*/
|
||||
private make(): HTMLElement {
|
||||
const wrapper = $.make('div', this.CSS.wrapper);
|
||||
const icon = $.svg('sad-face', 52, 52);
|
||||
const icon = `<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#D76B6B" fill-rule="nonzero" d="M26 52C11.64 52 0 40.36 0 26S11.64 0 26 0s26 11.64 26 26-11.64 26-26 26zm0-3.25c12.564 0 22.75-10.186 22.75-22.75S38.564 3.25 26 3.25 3.25 13.436 3.25 26 13.436 48.75 26 48.75zM15.708 33.042a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm23.834 0a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm-15.875 5.452a1.083 1.083 0 1 1-1.834-1.155c1.331-2.114 3.49-3.179 6.334-3.179 2.844 0 5.002 1.065 6.333 3.18a1.083 1.083 0 1 1-1.833 1.154c-.913-1.45-2.366-2.167-4.5-2.167s-3.587.717-4.5 2.167z"/></svg>`;
|
||||
const infoContainer = $.make('div', this.CSS.info);
|
||||
const title = $.make('div', this.CSS.title, {
|
||||
textContent: this.title,
|
||||
|
@ -101,7 +101,7 @@ export default class Stub implements BlockTool {
|
|||
textContent: this.subtitle,
|
||||
});
|
||||
|
||||
wrapper.appendChild(icon);
|
||||
wrapper.innerHTML = icon;
|
||||
|
||||
infoContainer.appendChild(title);
|
||||
infoContainer.appendChild(subtitle);
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
/**
|
||||
* Allow to import .svg from components/modules/ui from TypeScript file
|
||||
*/
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
|
@ -126,3 +126,33 @@ Cypress.Commands.add('render', { prevSubject: true }, async (subject: EditorJS,
|
|||
|
||||
return subject;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Select passed text in element
|
||||
* Note. Previous subject should have 'textNode' as firstChild
|
||||
*
|
||||
* Usage
|
||||
* cy.get('[data-cy=editorjs]')
|
||||
* .find('.ce-paragraph')
|
||||
* .selectText('block te')
|
||||
*
|
||||
* @param text - text to select
|
||||
*/
|
||||
Cypress.Commands.add('selectText', {
|
||||
prevSubject: true,
|
||||
}, (subject, text: string) => {
|
||||
const el = subject[0];
|
||||
const document = el.ownerDocument;
|
||||
const range = document.createRange();
|
||||
const textNode = el.firstChild;
|
||||
const selectionPositionStart = textNode.textContent.indexOf(text);
|
||||
const selectionPositionEnd = selectionPositionStart + text.length;
|
||||
|
||||
range.setStart(textNode, selectionPositionStart);
|
||||
range.setEnd(textNode, selectionPositionEnd);
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(range);
|
||||
|
||||
return subject;
|
||||
});
|
||||
|
|
|
@ -47,6 +47,19 @@ declare global {
|
|||
* @param data — data to render
|
||||
*/
|
||||
render(data: OutputData): Chainable<EditorJS>;
|
||||
|
||||
/**
|
||||
* Select passed text in element
|
||||
* Note. Previous subject should have 'textNode' as firstChild
|
||||
*
|
||||
* Usage
|
||||
* cy.get('[data-cy=editorjs]')
|
||||
* .find('.ce-paragraph')
|
||||
* .selectText('block te')
|
||||
*
|
||||
* @param text - text to select
|
||||
*/
|
||||
selectText(text: string): Chainable<Subject>;
|
||||
}
|
||||
|
||||
interface ApplicationWindow {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { BlockMutationType } from '../../../../types/events/block/mutation-type';
|
||||
import EditorJS from '../../../../types';
|
||||
|
||||
/**
|
||||
* There will be described test cases of BlockAPI
|
||||
|
@ -22,18 +23,22 @@ describe('BlockAPI', () => {
|
|||
*/
|
||||
const EditorJSApiMock = Cypress.sinon.match.any;
|
||||
|
||||
beforeEach(() => {
|
||||
if (this && this.editorInstance) {
|
||||
beforeEach(function () {
|
||||
const config = {
|
||||
data: editorDataMock,
|
||||
onChange: (): void => {
|
||||
console.log('something changed');
|
||||
},
|
||||
};
|
||||
|
||||
cy.createEditor(config).as('editorInstance');
|
||||
|
||||
cy.spy(config, 'onChange').as('onChange');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
} else {
|
||||
const config = {
|
||||
data: editorDataMock,
|
||||
onChange: (): void => { console.log('something changed'); },
|
||||
};
|
||||
|
||||
cy.createEditor(config).as('editorInstance');
|
||||
|
||||
cy.spy(config, 'onChange').as('onChange');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -45,8 +50,8 @@ describe('BlockAPI', () => {
|
|||
* Check that blocks.dispatchChange() triggers Editor 'onChange' callback
|
||||
*/
|
||||
it('should trigger onChange with corresponded block', () => {
|
||||
cy.get('@editorInstance').then(async (editor: any) => {
|
||||
const block = editor.blocks.getById(firstBlock.id);
|
||||
cy.get('@editorInstance').then(async (editor: unknown) => {
|
||||
const block = (editor as EditorJS).blocks.getById(firstBlock.id);
|
||||
|
||||
block.dispatchChange();
|
||||
|
||||
|
@ -59,5 +64,4 @@ describe('BlockAPI', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -16,13 +16,15 @@ describe('api.blocks', () => {
|
|||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
if (this && this.editorInstance) {
|
||||
beforeEach(function () {
|
||||
cy.createEditor({
|
||||
data: editorDataMock,
|
||||
}).as('editorInstance');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
} else {
|
||||
cy.createEditor({
|
||||
data: editorDataMock,
|
||||
}).as('editorInstance');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -117,4 +119,26 @@ describe('api.blocks', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* api.blocks.insert(type, data, config, index, needToFocus, replace, id)
|
||||
*/
|
||||
describe('.insert()', function () {
|
||||
it('should preserve block id if it is passed', function () {
|
||||
cy.get('@editorInstance').then(async (editor: any) => {
|
||||
const type = 'paragraph';
|
||||
const data = { text: 'codex' };
|
||||
const config = undefined;
|
||||
const index = undefined;
|
||||
const needToFocus = undefined;
|
||||
const replace = undefined;
|
||||
const id = 'test-id-123';
|
||||
|
||||
const block = editor.blocks.insert(type, data, config, index, needToFocus, replace, id);
|
||||
|
||||
expect(block).not.to.be.undefined;
|
||||
expect(block.id).to.be.eq(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { ToolboxConfig, BlockToolData, ToolboxConfigEntry } from '../../../../types';
|
||||
import { ToolboxConfig, BlockToolData, ToolboxConfigEntry, PasteConfig } from '../../../../types';
|
||||
import EditorJS from '../../../../types';
|
||||
import { HTMLPasteEvent, TunesMenuConfig } from '../../../../types/tools';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
||||
const 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"></path></svg>';
|
||||
|
||||
|
@ -94,22 +98,22 @@ describe('Editor Tools Api', () => {
|
|||
.should('contain.text', TestTool.toolbox[1].title);
|
||||
});
|
||||
|
||||
it('should insert block with overriden data on entry click in case toolbox entry provides data overrides', () => {
|
||||
it('should insert block with overridden data on entry click in case toolbox entry provides data overrides', () => {
|
||||
const text = 'Text';
|
||||
const dataOverrides = {
|
||||
testProp: 'new value',
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool with default data to be overriden
|
||||
* Tool with default data to be overridden
|
||||
*/
|
||||
class TestTool {
|
||||
private _data = {
|
||||
testProp: 'default value',
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool contructor
|
||||
* Tool constructor
|
||||
*
|
||||
* @param data - previously saved data
|
||||
*/
|
||||
|
@ -118,7 +122,7 @@ describe('Editor Tools Api', () => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns toolbox config as list of entries with overriden data
|
||||
* Returns toolbox config as list of entries with overridden data
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
return [
|
||||
|
@ -179,8 +183,8 @@ describe('Editor Tools Api', () => {
|
|||
.type(text);
|
||||
|
||||
cy.get('@editorInstance')
|
||||
.then(async (editor: any) => {
|
||||
const editorData = await editor.save();
|
||||
.then(async (editor: unknown) => {
|
||||
const editorData = await (editor as EditorJS).save();
|
||||
|
||||
expect(editorData.blocks[0].data).to.be.deep.eq({
|
||||
...dataOverrides,
|
||||
|
@ -188,20 +192,43 @@ describe('Editor Tools Api', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not display tool in toolbox if the tool has single toolbox entry configured and it has icon missing', () => {
|
||||
/**
|
||||
* Tool with one of the toolbox entries with icon missing
|
||||
*/
|
||||
context('Tunes — renderSettings()', () => {
|
||||
it('should contain a single block tune configured in tool\'s renderSettings() method', () => {
|
||||
/** Tool with single tunes menu entry configured */
|
||||
class TestTool {
|
||||
/**
|
||||
* Returns toolbox config as list of entries one of which has missing icon
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
/** Returns toolbox config as list of entries */
|
||||
public static get toolbox(): ToolboxConfigEntry {
|
||||
return {
|
||||
title: 'Entry 2',
|
||||
title: 'Test tool',
|
||||
icon: ICON,
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns configuration for block tunes menu */
|
||||
public renderSettings(): TunesMenuConfig {
|
||||
return {
|
||||
label: 'Test tool tune',
|
||||
icon: ICON,
|
||||
name: 'testToolTune',
|
||||
|
||||
onActivate: (): void => {},
|
||||
};
|
||||
}
|
||||
|
||||
/** Save method stub */
|
||||
public save(): void {}
|
||||
|
||||
/** Renders a block */
|
||||
public render(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
|
||||
element.contentEditable = 'true';
|
||||
element.setAttribute('data-name', 'testBlock');
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
|
@ -218,32 +245,68 @@ describe('Editor Tools Api', () => {
|
|||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
// Insert test tool block
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.should('not.exist');
|
||||
.get(`[data-item-name="testTool"]`)
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('[data-name=testBlock]')
|
||||
.type('some text')
|
||||
.click();
|
||||
|
||||
// Open block tunes
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
// Expect preconfigured tune to exist in tunes menu
|
||||
cy.get('[data-item-name=testToolTune]').should('exist');
|
||||
});
|
||||
|
||||
it('should skip toolbox entries that have no icon', () => {
|
||||
const skippedEntryTitle = 'Entry 2';
|
||||
|
||||
/**
|
||||
* Tool with one of the toolbox entries with icon missing
|
||||
*/
|
||||
it('should contain multiple block tunes if configured in tool\'s renderSettings() method', () => {
|
||||
/** Tool with single tunes menu entry configured */
|
||||
class TestTool {
|
||||
/**
|
||||
* Returns toolbox config as list of entries one of which has missing icon
|
||||
*/
|
||||
public static get toolbox(): ToolboxConfig {
|
||||
/** Returns toolbox config as list of entries */
|
||||
public static get toolbox(): ToolboxConfigEntry {
|
||||
return {
|
||||
title: 'Test tool',
|
||||
icon: ICON,
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns configuration for block tunes menu */
|
||||
public renderSettings(): TunesMenuConfig {
|
||||
return [
|
||||
{
|
||||
title: 'Entry 1',
|
||||
label: 'Test tool tune 1',
|
||||
icon: ICON,
|
||||
name: 'testToolTune1',
|
||||
|
||||
onActivate: (): void => {},
|
||||
},
|
||||
{
|
||||
title: skippedEntryTitle,
|
||||
label: 'Test tool tune 2',
|
||||
icon: ICON,
|
||||
name: 'testToolTune2',
|
||||
|
||||
onActivate: (): void => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Save method stub */
|
||||
public save(): void {}
|
||||
|
||||
/** Renders a block */
|
||||
public render(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
|
||||
element.contentEditable = 'true';
|
||||
element.setAttribute('data-name', 'testBlock');
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
|
@ -260,10 +323,528 @@ describe('Editor Tools Api', () => {
|
|||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
// Insert test tool block
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-popover__item[data-item-name=testTool]')
|
||||
.should('have.length', 1)
|
||||
.should('not.contain', skippedEntryTitle);
|
||||
.get(`[data-item-name="testTool"]`)
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('[data-name=testBlock]')
|
||||
.type('some text')
|
||||
.click();
|
||||
|
||||
// Open block tunes
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
// Expect preconfigured tunes to exist in tunes menu
|
||||
cy.get('[data-item-name=testToolTune1]').should('exist');
|
||||
cy.get('[data-item-name=testToolTune2]').should('exist');
|
||||
});
|
||||
|
||||
it('should contain block tunes represented as custom html if so configured in tool\'s renderSettings() method', () => {
|
||||
const sampleText = 'sample text';
|
||||
|
||||
/** Tool with single tunes menu entry configured */
|
||||
class TestTool {
|
||||
/** Returns toolbox config as list of entries */
|
||||
public static get toolbox(): ToolboxConfigEntry {
|
||||
return {
|
||||
title: 'Test tool',
|
||||
icon: ICON,
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns configuration for block tunes menu */
|
||||
public renderSettings(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
|
||||
element.textContent = sampleText;
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/** Save method stub */
|
||||
public save(): void {}
|
||||
|
||||
/** Renders a block */
|
||||
public render(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
|
||||
element.contentEditable = 'true';
|
||||
element.setAttribute('data-name', 'testBlock');
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-toolbar__plus')
|
||||
.click();
|
||||
|
||||
// Insert test tool block
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get(`[data-item-name="testTool"]`)
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('[data-name=testBlock]')
|
||||
.type('some text')
|
||||
.click();
|
||||
|
||||
// Open block tunes
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
// Expect preconfigured custom html tunes to exist in tunes menu
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover')
|
||||
.should('contain.text', sampleText);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @todo cover all the pasteConfig properties
|
||||
*/
|
||||
context('Paste — pasteConfig()', () => {
|
||||
context('tags', () => {
|
||||
/**
|
||||
* tags: ['H1', 'H2']
|
||||
*/
|
||||
it('should use corresponding tool when the array of tag names specified', () => {
|
||||
/**
|
||||
* Test tool with pasteConfig.tags specified
|
||||
*/
|
||||
class TestImgTool {
|
||||
/** config specified handled tag */
|
||||
public static get pasteConfig(): PasteConfig {
|
||||
return {
|
||||
tags: [ 'img' ], // only tag name specified. Attributes should be sanitized
|
||||
};
|
||||
}
|
||||
|
||||
/** onPaste callback will be stubbed below */
|
||||
public onPaste(): void {}
|
||||
|
||||
/** save is required for correct implementation of the BlockTool class */
|
||||
public save(): void {}
|
||||
|
||||
/** render is required for correct implementation of the BlockTool class */
|
||||
public render(): HTMLElement {
|
||||
return document.createElement('img');
|
||||
}
|
||||
}
|
||||
|
||||
const toolsOnPaste = cy.spy(TestImgTool.prototype, 'onPaste');
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestImgTool,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<img>',
|
||||
})
|
||||
.then(() => {
|
||||
expect(toolsOnPaste).to.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* tags: ['img'] -> <img>
|
||||
*/
|
||||
it('should sanitize all attributes from tag, if only tag name specified ', () => {
|
||||
/**
|
||||
* Variable used for spying the pasted element we are passing to the Tool
|
||||
*/
|
||||
let pastedElement;
|
||||
|
||||
/**
|
||||
* Test tool with pasteConfig.tags specified
|
||||
*/
|
||||
class TestImageTool {
|
||||
/** config specified handled tag */
|
||||
public static get pasteConfig(): PasteConfig {
|
||||
return {
|
||||
tags: [ 'img' ], // only tag name specified. Attributes should be sanitized
|
||||
};
|
||||
}
|
||||
|
||||
/** onPaste callback will be stubbed below */
|
||||
public onPaste(): void {}
|
||||
|
||||
/** save is required for correct implementation of the BlockTool class */
|
||||
public save(): void {}
|
||||
|
||||
/** render is required for correct implementation of the BlockTool class */
|
||||
public render(): HTMLElement {
|
||||
return document.createElement('img');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub the onPaste method to access the PasteEvent data for assertion
|
||||
*/
|
||||
cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
|
||||
pastedElement = event.detail.data;
|
||||
});
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testImageTool: TestImageTool,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<img src="foo" onerror="alert(123)"/>', // all attributes should be sanitized
|
||||
})
|
||||
.then(() => {
|
||||
expect(pastedElement).not.to.be.undefined;
|
||||
expect(pastedElement.tagName.toLowerCase()).eq('img');
|
||||
expect(pastedElement.attributes.length).eq(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* tags: [{
|
||||
* img: {
|
||||
* src: true
|
||||
* }
|
||||
* }]
|
||||
* -> <img src="">
|
||||
*
|
||||
*/
|
||||
it('should leave attributes if entry specified as a sanitizer config ', () => {
|
||||
/**
|
||||
* Variable used for spying the pasted element we are passing to the Tool
|
||||
*/
|
||||
let pastedElement;
|
||||
|
||||
/**
|
||||
* Test tool with pasteConfig.tags specified
|
||||
*/
|
||||
class TestImageTool {
|
||||
/** config specified handled tag */
|
||||
public static get pasteConfig(): PasteConfig {
|
||||
return {
|
||||
tags: [
|
||||
{
|
||||
img: {
|
||||
src: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** onPaste callback will be stubbed below */
|
||||
public onPaste(): void {}
|
||||
|
||||
/** save is required for correct implementation of the BlockTool class */
|
||||
public save(): void {}
|
||||
|
||||
/** render is required for correct implementation of the BlockTool class */
|
||||
public render(): HTMLElement {
|
||||
return document.createElement('img');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub the onPaste method to access the PasteEvent data for assertion
|
||||
*/
|
||||
cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
|
||||
pastedElement = event.detail.data;
|
||||
});
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testImageTool: TestImageTool,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<img src="foo" onerror="alert(123)"/>',
|
||||
})
|
||||
.then(() => {
|
||||
expect(pastedElement).not.to.be.undefined;
|
||||
|
||||
/**
|
||||
* Check that the <img> has only "src" attribute
|
||||
*/
|
||||
expect(pastedElement.tagName.toLowerCase()).eq('img');
|
||||
expect(pastedElement.getAttribute('src')).eq('foo');
|
||||
expect(pastedElement.attributes.length).eq(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* tags: [
|
||||
* 'video',
|
||||
* {
|
||||
* source: {
|
||||
* src: true
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
it('should support mixed tag names and sanitizer config ', () => {
|
||||
/**
|
||||
* Variable used for spying the pasted element we are passing to the Tool
|
||||
*/
|
||||
let pastedElement;
|
||||
|
||||
/**
|
||||
* Test tool with pasteConfig.tags specified
|
||||
*/
|
||||
class TestTool {
|
||||
/** config specified handled tag */
|
||||
public static get pasteConfig(): PasteConfig {
|
||||
return {
|
||||
tags: [
|
||||
'video', // video should not have attributes
|
||||
{
|
||||
source: { // source should have only src attribute
|
||||
src: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** onPaste callback will be stubbed below */
|
||||
public onPaste(): void {}
|
||||
|
||||
/** save is required for correct implementation of the BlockTool class */
|
||||
public save(): void {}
|
||||
|
||||
/** render is required for correct implementation of the BlockTool class */
|
||||
public render(): HTMLElement {
|
||||
return document.createElement('tbody');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub the onPaste method to access the PasteEvent data for assertion
|
||||
*/
|
||||
cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
|
||||
pastedElement = event.detail.data;
|
||||
});
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<video width="100"><source src="movie.mp4" type="video/mp4"></video>',
|
||||
})
|
||||
.then(() => {
|
||||
expect(pastedElement).not.to.be.undefined;
|
||||
|
||||
/**
|
||||
* Check that <video> has no attributes
|
||||
*/
|
||||
expect(pastedElement.tagName.toLowerCase()).eq('video');
|
||||
expect(pastedElement.attributes.length).eq(0);
|
||||
|
||||
/**
|
||||
* Check that the <source> has only 'src' attribute
|
||||
*/
|
||||
expect(pastedElement.firstChild.tagName.toLowerCase()).eq('source');
|
||||
expect(pastedElement.firstChild.getAttribute('src')).eq('movie.mp4');
|
||||
expect(pastedElement.firstChild.attributes.length).eq(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* tags: [
|
||||
* {
|
||||
* td: { width: true },
|
||||
* tr: { height: true }
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
it('should support config with several keys as the single entry', () => {
|
||||
/**
|
||||
* Variable used for spying the pasted element we are passing to the Tool
|
||||
*/
|
||||
let pastedElement;
|
||||
|
||||
/**
|
||||
* Test tool with pasteConfig.tags specified
|
||||
*/
|
||||
class TestTool {
|
||||
/** config specified handled tag */
|
||||
public static get pasteConfig(): PasteConfig {
|
||||
return {
|
||||
tags: [
|
||||
{
|
||||
video: {
|
||||
width: true,
|
||||
},
|
||||
source: {
|
||||
src: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** onPaste callback will be stubbed below */
|
||||
public onPaste(): void {}
|
||||
|
||||
/** save is required for correct implementation of the BlockTool class */
|
||||
public save(): void {}
|
||||
|
||||
/** render is required for correct implementation of the BlockTool class */
|
||||
public render(): HTMLElement {
|
||||
return document.createElement('tbody');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub the onPaste method to access the PasteEvent data for assertion
|
||||
*/
|
||||
cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
|
||||
pastedElement = event.detail.data;
|
||||
});
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<video width="100"><source src="movie.mp4" type="video/mp4"></video>',
|
||||
})
|
||||
.then(() => {
|
||||
expect(pastedElement).not.to.be.undefined;
|
||||
expect(pastedElement.tagName.toLowerCase()).eq('video');
|
||||
|
||||
/**
|
||||
* Check that the <tr> has the 'height' attribute
|
||||
*/
|
||||
expect(pastedElement.firstChild.tagName.toLowerCase()).eq('source');
|
||||
expect(pastedElement.firstChild.getAttribute('src')).eq('movie.mp4');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* It covers a workaround HTMLJanitor bug with tables (incorrect sanitizing of table.innerHTML)
|
||||
* https://github.com/guardian/html-janitor/issues/3
|
||||
*/
|
||||
it('should correctly sanitize Table structure (test for HTMLJanitor bug)', () => {
|
||||
/**
|
||||
* Variable used for spying the pasted element we are passing to the Tool
|
||||
*/
|
||||
let pastedElement;
|
||||
|
||||
/**
|
||||
* Test tool with pasteConfig.tags specified
|
||||
*/
|
||||
class TestTool {
|
||||
/** config specified handled tag */
|
||||
public static get pasteConfig(): PasteConfig {
|
||||
return {
|
||||
tags: [
|
||||
'table',
|
||||
'tbody',
|
||||
{
|
||||
td: {
|
||||
width: true,
|
||||
},
|
||||
tr: {
|
||||
height: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** onPaste callback will be stubbed below */
|
||||
public onPaste(): void {}
|
||||
|
||||
/** save is required for correct implementation of the BlockTool class */
|
||||
public save(): void {}
|
||||
|
||||
/** render is required for correct implementation of the BlockTool class */
|
||||
public render(): HTMLElement {
|
||||
return document.createElement('tbody');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub the onPaste method to access the PasteEvent data for assertion
|
||||
*/
|
||||
cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
|
||||
pastedElement = event.detail.data;
|
||||
});
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTool: TestTool,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<table><tr height="50"><td width="300">Ho-Ho-Ho</td></tr></table>',
|
||||
})
|
||||
.then(() => {
|
||||
expect(pastedElement).not.to.be.undefined;
|
||||
expect(pastedElement.tagName.toLowerCase()).eq('table');
|
||||
|
||||
/**
|
||||
* Check that the <tr> has the 'height' attribute
|
||||
*/
|
||||
expect(pastedElement.querySelector('tr')).not.to.be.undefined;
|
||||
expect(pastedElement.querySelector('tr').getAttribute('height')).eq('50');
|
||||
|
||||
/**
|
||||
* Check that the <td> has the 'width' attribute
|
||||
*/
|
||||
expect(pastedElement.querySelector('td')).not.to.be.undefined;
|
||||
expect(pastedElement.querySelector('td').getAttribute('width')).eq('300');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
import { TunesMenuConfig } from '../../../../types/tools';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
||||
describe('Editor Tunes Api', () => {
|
||||
it('should render a popover entry for block tune if configured', () => {
|
||||
/** Test tune that should appear be rendered in block tunes menu */
|
||||
class TestTune {
|
||||
/** Set Tool is Tune */
|
||||
public static readonly isTune = true;
|
||||
|
||||
/** Tune's appearance in block settings menu */
|
||||
public render(): TunesMenuConfig {
|
||||
return {
|
||||
icon: 'ICON',
|
||||
label: 'Test tune',
|
||||
name: 'testTune',
|
||||
|
||||
onActivate: (): void => { },
|
||||
};
|
||||
}
|
||||
|
||||
/** Save method stub */
|
||||
public save(): void {}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTune: TestTune,
|
||||
},
|
||||
tunes: [ 'testTune' ],
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.type('some text')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
cy.get('[data-item-name=testTune]').should('exist');
|
||||
});
|
||||
|
||||
it('should render several popover entries for block tune if configured', () => {
|
||||
/** Test tune that should appear be rendered in block tunes menu */
|
||||
class TestTune {
|
||||
/** Set Tool is Tune */
|
||||
public static readonly isTune = true;
|
||||
|
||||
/** Tune's appearance in block settings menu */
|
||||
public render(): TunesMenuConfig {
|
||||
return [
|
||||
{
|
||||
icon: 'ICON1',
|
||||
label: 'Tune entry 1',
|
||||
name: 'testTune1',
|
||||
|
||||
onActivate: (): void => { },
|
||||
}, {
|
||||
icon: 'ICON2',
|
||||
label: 'Tune entry 2',
|
||||
name: 'testTune2',
|
||||
|
||||
onActivate: (): void => { },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Save method stub */
|
||||
public save(): void {}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTune: TestTune,
|
||||
},
|
||||
tunes: [ 'testTune' ],
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.type('some text')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
cy.get('[data-item-name=testTune1]').should('exist');
|
||||
cy.get('[data-item-name=testTune2]').should('exist');
|
||||
});
|
||||
|
||||
it('should display custom html returned by tune\'s render() method inside tunes menu', () => {
|
||||
const sampleText = 'sample text';
|
||||
|
||||
/** Test tune that should appear be rendered in block tunes menu */
|
||||
class TestTune {
|
||||
/** Set Tool is Tune */
|
||||
public static readonly isTune = true;
|
||||
|
||||
/** Tune's appearance in block settings menu */
|
||||
public render(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
|
||||
element.textContent = sampleText;
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/** Save method stub */
|
||||
public save(): void {}
|
||||
}
|
||||
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
testTune: TestTune,
|
||||
},
|
||||
tunes: [ 'testTune' ],
|
||||
}).as('editorInstance');
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.type('some text')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-toolbar__settings-btn')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('.ce-popover')
|
||||
.should('contain.text', sampleText);
|
||||
});
|
||||
});
|
|
@ -2,16 +2,18 @@
|
|||
import Header from '@editorjs/header';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
describe.only('Block ids', () => {
|
||||
beforeEach(() => {
|
||||
if (this && this.editorInstance) {
|
||||
describe('Block ids', () => {
|
||||
beforeEach(function () {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
} else {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,48 +2,53 @@ import Header from '@editorjs/header';
|
|||
import Image from '@editorjs/simple-image';
|
||||
import * as _ from '../../../src/components/utils';
|
||||
|
||||
describe('Copy pasting from Editor', () => {
|
||||
beforeEach(() => {
|
||||
if (this && this.editorInstance) {
|
||||
describe('Copy pasting from Editor', function () {
|
||||
beforeEach(function () {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
image: Image,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
} else {
|
||||
cy.createEditor({
|
||||
tools: {
|
||||
header: Header,
|
||||
image: Image,
|
||||
},
|
||||
}).as('editorInstance');
|
||||
}
|
||||
});
|
||||
|
||||
context('pasting', () => {
|
||||
it('should paste plain text', () => {
|
||||
context('pasting', function () {
|
||||
it('should paste plain text', function () {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/plain': 'Some plain text',
|
||||
})
|
||||
.wait(0)
|
||||
.should('contain', 'Some plain text');
|
||||
});
|
||||
|
||||
it('should paste inline html data', () => {
|
||||
it('should paste inline html data', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<p><b>Some text</b></p>',
|
||||
})
|
||||
.should('contain.html', '<b>Some text</b>');
|
||||
});
|
||||
|
||||
it('should paste several blocks if plain text contains new lines', () => {
|
||||
it('should paste several blocks if plain text contains new lines', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/plain': 'First block\n\nSecond block',
|
||||
});
|
||||
|
||||
|
@ -55,11 +60,12 @@ describe('Copy pasting from Editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should paste several blocks if html contains several paragraphs', () => {
|
||||
it('should paste several blocks if html contains several paragraphs', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<p>First block</p><p>Second block</p>',
|
||||
});
|
||||
|
||||
|
@ -71,11 +77,12 @@ describe('Copy pasting from Editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should paste using custom data type', () => {
|
||||
it('should paste using custom data type', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'application/x-editor-js': JSON.stringify([
|
||||
{
|
||||
tool: 'paragraph',
|
||||
|
@ -100,11 +107,12 @@ describe('Copy pasting from Editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should parse block tags', () => {
|
||||
it('should parse block tags', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/html': '<h2>First block</h2><p>Second block</p>',
|
||||
});
|
||||
|
||||
|
@ -117,11 +125,12 @@ describe('Copy pasting from Editor', () => {
|
|||
.should('contain', 'Second block');
|
||||
});
|
||||
|
||||
it('should parse pattern', () => {
|
||||
it('should parse pattern', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
.paste({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'text/plain': 'https://codex.so/public/app/img/external/codex2x.png',
|
||||
});
|
||||
|
||||
|
@ -132,8 +141,8 @@ describe('Copy pasting from Editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
context('copying', () => {
|
||||
it('should copy inline fragment', () => {
|
||||
context('copying', function () {
|
||||
it('should copy inline fragment', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -147,7 +156,7 @@ describe('Copy pasting from Editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should copy several blocks', () => {
|
||||
it('should copy several blocks', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -171,7 +180,7 @@ describe('Copy pasting from Editor', () => {
|
|||
* Need to wait for custom data as it is set asynchronously
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(0).then(() => {
|
||||
cy.wait(0).then(function () {
|
||||
expect(clipboardData['application/x-editor-js']).not.to.be.undefined;
|
||||
|
||||
const data = JSON.parse(clipboardData['application/x-editor-js']);
|
||||
|
@ -185,8 +194,8 @@ describe('Copy pasting from Editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
context('cutting', () => {
|
||||
it('should cut inline fragment', () => {
|
||||
context('cutting', function () {
|
||||
it('should cut inline fragment', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -200,7 +209,7 @@ describe('Copy pasting from Editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should cut several blocks', () => {
|
||||
it('should cut several blocks', function () {
|
||||
cy.get('[data-cy=editorjs]')
|
||||
.get('div.ce-block')
|
||||
.click()
|
||||
|
@ -224,7 +233,7 @@ describe('Copy pasting from Editor', () => {
|
|||
* Need to wait for custom data as it is set asynchronously
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(0).then(() => {
|
||||
cy.wait(0).then(function () {
|
||||
expect(clipboardData['application/x-editor-js']).not.to.be.undefined;
|
||||
|
||||
const data = JSON.parse(clipboardData['application/x-editor-js']);
|
||||
|
@ -241,7 +250,7 @@ describe('Copy pasting from Editor', () => {
|
|||
.should('not.contain', 'Second block');
|
||||
});
|
||||
|
||||
it('should cut lots of blocks', () => {
|
||||
it('should cut lots of blocks', function () {
|
||||
const numberOfBlocks = 50;
|
||||
|
||||
for (let i = 0; i < numberOfBlocks; i++) {
|
||||
|
@ -264,7 +273,7 @@ describe('Copy pasting from Editor', () => {
|
|||
* Need to wait for custom data as it is set asynchronously
|
||||
*/
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(0).then(() => {
|
||||
cy.wait(0).then(function () {
|
||||
expect(clipboardData['application/x-editor-js']).not.to.be.undefined;
|
||||
|
||||
const data = JSON.parse(clipboardData['application/x-editor-js']);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ToolboxConfig } from '../../../types';
|
|||
|
||||
describe('Editor i18n', () => {
|
||||
context('Toolbox', () => {
|
||||
it('should translate tool title in a toolbox', () => {
|
||||
it('should translate tool title in a toolbox', function () {
|
||||
if (this && this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ describe('Editor i18n', () => {
|
|||
.should('contain.text', toolNamesDictionary.Heading);
|
||||
});
|
||||
|
||||
it('should translate titles of toolbox entries', () => {
|
||||
it('should translate titles of toolbox entries', function () {
|
||||
if (this && this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ describe('Editor i18n', () => {
|
|||
.should('contain.text', toolNamesDictionary.Title2);
|
||||
});
|
||||
|
||||
it('should use capitalized tool name as translation key if toolbox title is missing', () => {
|
||||
it('should use capitalized tool name as translation key if toolbox title is missing', function () {
|
||||
if (this && this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
}
|
||||
|
@ -141,4 +141,4 @@ describe('Editor i18n', () => {
|
|||
.should('contain.text', toolNamesDictionary.TestTool);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// eslint-disable-next-line spaced-comment
|
||||
// eslint-disable-next-line spaced-comment, @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="../support/index.d.ts" />
|
||||
|
||||
describe('Editor basic initialization', () => {
|
||||
|
@ -8,11 +8,13 @@ describe('Editor basic initialization', () => {
|
|||
*/
|
||||
const editorConfig = {};
|
||||
|
||||
beforeEach(() => {
|
||||
if (this && this.editorInstance) {
|
||||
beforeEach(function () {
|
||||
cy.createEditor(editorConfig).as('editorInstance');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (this.editorInstance) {
|
||||
this.editorInstance.destroy();
|
||||
} else {
|
||||
cy.createEditor(editorConfig).as('editorInstance');
|
||||
}
|
||||
});
|
||||
|
||||
|
|