Compare commits

...

25 Commits

Author SHA1 Message Date
Ilya Maroz a5bea55d35
Merge branch 'next' of github.com:codex-team/editor.js into feature/collaborative-editing 2022-12-01 16:54:26 +00:00
github-actions[bot] 320a30afcc
bump: 2.26.0 is ready (#2189)
* Bump version

* Update package.json

Co-authored-by: github-actions <action@github.com>
Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
2022-11-29 22:03:33 +04:00
Peter Savchenko 509023e307
chore(submodule): paragraph submodule updated with the new icons (#2191)
* do not show tooltip on mobile

* paragraph submodule updated with the new icon
2022-11-29 15:01:02 +04:00
Peter Savchenko d56c07c3dc
chore(icons): icons size increased for mobile devices (#2190)
* popover icons size increased on mobile devices

* update icon size on mobile

* some mobile styles improvements

* inline tools click on mobile devices improved
2022-11-29 12:02:23 +04:00
Peter Savchenko 1a72d2153b
fix(toolbox): shortcuts appearance fixed, icons became optional (#2188)
* fix(toolbox): shortcuts appearing fixed, icons became optional

* rm only from test

* Update tools.spec.ts

* rm useless tests

* Update CHANGELOG.md
2022-11-29 01:57:48 +04:00
github-actions[bot] 04118b2d6e
bump: 2.26.0-rc.7
Co-authored-by: github-actions <action@github.com>
2022-11-28 20:28:41 +04:00
Peter Savchenko 73c9bdf40b
fix(block-events): split block with removing of selected text fragment (#2186)
* fix(block-events): split block with removing of selected text fragment

* Update BlockEvents.spec.ts
2022-11-28 20:23:30 +04:00
github-actions[bot] a8026e6829
Bump version (#2183)
Co-authored-by: github-actions <action@github.com>
2022-11-26 11:37:53 +04:00
Peter Savchenko 0bf5007f6c
fix ci (#2182) 2022-11-26 11:32:04 +04:00
github-actions[bot] 6088aaead6
bump: 2.26.0-rc.5
Co-authored-by: github-actions <action@github.com>
2022-11-26 10:48:28 +04:00
dependabot[bot] cdaf10a58f
build(deps): bump moment from 2.29.2 to 2.29.4 (#2104)
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
2022-11-26 10:42:23 +04:00
Peter Savchenko e048c4d46b
fix release drafter 2022-11-26 03:13:48 +04:00
github-actions[bot] fb86d71828
bump: 2.26.0-rc.4
Co-authored-by: github-actions <action@github.com>
Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
2022-11-26 03:09:00 +04:00
Valerii Varankin 811a41366b
Fix: check for Files substitutions before handling files paste (#1928)
* FIX: Paste text from MS Word instead of image

* changelog added

* Update CHANGELOG.md

* rm submodule change

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
2022-11-26 01:24:17 +04:00
Peter Savchenko 0d0f9e8de2
chore(submodules): submodules updated, changelog fixed (#2179)
* submodules updated

* changelog updated
2022-11-26 00:53:08 +04:00
azibodusi osain 9b7da504e4
feat(blocks-api): the insert() method now has the optional `id` param
* refactor: added id to the insert method  to allow user pass and existing id to the method

When  working with multiple editor at the same and need to link  all  blocks to each editor and keeping the same ids in all.

* moved the position of the block_id params to the end to aaviod breaking cha

* doc: updated  the  documentation on insert method params

* refactor :  formatted  the code to  add/remove  space

* refactor: moved the position of the `id` and its description to the respective position

* refactor: rollback to previous commit

* added back the removed default value

* fix error, remove garbage

* test added, changelog added

Co-authored-by: Umang G. Patel <23169768+robonetphy@users.noreply.github.com>
Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
2022-11-25 23:59:38 +04:00
github-actions[bot] a1071b7229
Bump version (#2178)
Co-authored-by: github-actions <action@github.com>
2022-11-25 22:29:01 +04:00
Peter Savchenko b1b582b150
feat(icons): codex icons package is used instead of svg sprite (#2173)
* chore(icons): migrating to the coded icon pack

* conversion toolbar

* inline toolbar, part 1

* inline-link tool has the new icons

* added a test for creating a link by Enter keydown in link input

* rm last icons, svg sprite, loaders

* rollback .ce-settings styles

* Update CHANGELOG.md

* Update settings.json
2022-11-25 22:26:23 +04:00
github-actions[bot] f0f8f8fcb5
Bump version (#2177)
Co-authored-by: github-actions <action@github.com>
2022-11-25 22:01:02 +04:00
Peter Savchenko 3272efc3f7
chore(linting): eslint updated, code linted (#2174)
* update eslint + autofix

* a bunch of eslint fixes

* some spelling & eslint fixes

* fix some eslint errors and spells

* Update __module.ts

* a bunch of eslint fixes in tests

* Update cypress.yml

* Update cypress.yml

* fix cypress docker image name

* fixes for tests

* more tests fixed

* rm rule ignore

* rm another ignored rule

* Update .eslintrc
2022-11-25 21:56:50 +04:00
github-actions[bot] 19f3790cfd
Bump version (#2169)
Co-authored-by: github-actions <action@github.com>
2022-11-22 00:30:24 +04:00
Umang G. Patel f659015be6
fix(tools-api): pasteConfig.tags now supports a sanitize config (#2100)
* event handlers function added

* santization config added

* integrate with paste event

* lint removed

* remove old changes

* object based sanitization configuration support

* paste config updated

* logic updated

* extract tag name from paste-config

* tool tags added

* multi tag sanitization added

* the comments added

* lint removed

* Update types/configs/paste-config.d.ts

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* update the changes

* lint removed\

* return empty array by get tags

* submoduble reset

* Update src/components/modules/paste.ts

Co-authored-by: Jorge <46056498+jorgectf@users.noreply.github.com>

* changelog added

* tool comments added

* chore: docs, code comments updated

* fix: xss in processDataTransfer

* base tests added

* test added

* rm 'only' from test suite

* rm log

* reorder test

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: Jorge <46056498+jorgectf@users.noreply.github.com>
2022-11-22 00:28:53 +04:00
ilyamore88 665d5d1388
Merge branch 'next' of github.com:codex-team/editor.js into feature/collaborative-editing 2022-11-16 19:14:42 +00:00
github-actions[bot] cd06bfc9c5
Bump version up to 2.26.0-rc.0 (#2105)
* Bump version

* Update package.json

* Update changelog

Co-authored-by: github-actions <action@github.com>
Co-authored-by: Tanya Fomina <fomina.tatianaaa@yandex.ru>
2022-11-03 22:39:51 +03:00
Tatiana Fomina 581289c03e
Block tunes as a popover (#2091)
* Default tunes to popover

* Add the rest of default tunes

* Add popover

* Cleanup

* Rename custom content

* Cleanup

* Add ability to open block settings upwards

* Fix tests

* Cleanup default tunes

* Rename and cleanup

* Add ability to display rendered custom tunes

* cleanup

* Rename

* Add flag to close tunes popover

* Cleanup

* i18n

* Cleanup

* Fix build and tests

* Fix for iframe

* Add comments

* Display active item, move closeOnActivate to popover

* Add confirmation support to popover

* Handle boolean value in confirmation param

* Clarify flippable logic in popover

* Comments

* Pass editor element as a param of popover constructor

* Fix readability

* Tests

* Fix flipper for confirmation element

* Update confirmation config structure

* Rename onClick to onActivate

* Fix tests and build

* Make confirmation props optional

* Simplify processing tunes

* Renamings

* Fix text block tunes

* Docs

* Update event type

* Move enabling confirmation state to separate method

* move popover types

* Unhardcode color

* Support toggling

* Add support of disabled items

* Fix tab in empty block leading to selecting second item in popover

* Remove margins for styles api settings button class

* Fix arrow navigation between blocks after opening block tunes

* Cleaup in default tunes code

* Fix chaining confirmations

* Colors

* Types

* Change the way flippable elements of popover custom area are set

* Remove borders around popover icons

* Fix untabbable inline toolbar

* Fix locked scroll after closing tunes popover on mobile

* Cleanup

* Set max popover width

* Make popover icon's border outside

* Fix tab issue

* Fix focus/hover issue

* Reformat

* Cleanup

* Fix opening block tunes via keyboard

* Add disableSpecialHoverAndFocusBehavior

* Add deprecated comment

* Cleanup

* Fix popover active state

* Fix checklist deletion with confirmation

* Fix checklist deletion 2

* Fix popover focus

* Fix popover items being impossible to flip after searching

* Fix popover item highlighting issue

* Update flipper.spec.ts

* Fixes after review

* Add Tunes Api tests

* Fix multiple popover entries configured by one tune

* Add tool's renderSettings() tests

* Add popover confirmation state test

* Fix popover width on mobile

* Add popover tests

* Add changelog and update version

* Update changelog

* Fix block tunes being unable to open after tune activation

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
2022-11-03 20:52:33 +03:00
126 changed files with 5082 additions and 4758 deletions

View File

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

View File

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

View File

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

View File

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

39
.vscode/settings.json vendored Normal file
View File

@ -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"
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,6 @@ declare const VERSION: string;
* Short Description (_눈;)
*
* @version 2.18.0
*
* @license Apache-2.0
* @author CodeX-Team <https://ifmo.su>
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,6 @@ export default class ToolsFactory {
/**
* @class
*
* @param config - tools config
* @param editorConfig - EditorJS config
* @param api - EditorJS API module

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
/**
* Allow to import .svg from components/modules/ui from TypeScript file
*/
declare module '*.svg' {
const content: string;
export default content;
}

View File

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

View File

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

View File

@ -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', () => {
});
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More