diff --git a/.eslintrc b/.eslintrc index 9d39bb96..ef566548 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,27 +1,7 @@ { "extends": [ - "codex" + "codex/ts" ], - "rules": { - /** - * Temporary suppress some errors. We need to fix them partially in next patches - */ - "import/no-duplicates": ["warn"], - "@typescript-eslint/triple-slash-reference": ["off"], - "jsdoc/no-undefined-types": ["warn", {"definedTypes": [ - "ConstructorOptions", - "API", - "BlockToolConstructable", - "EditorConfig", - "Tool", - "ToolSettings" - ]}] - }, - "settings": { - "jsdoc": { - "mode": "typescript" - } - }, "globals": { "Node": true, "Range": true, diff --git a/.github/workflows/create-a-release-draft.yml b/.github/workflows/create-a-release-draft.yml index 65c46c56..4016b7b8 100644 --- a/.github/workflows/create-a-release-draft.yml +++ b/.github/workflows/create-a-release-draft.yml @@ -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 diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index c384afd8..ee311b35 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -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 diff --git a/.github/workflows/publish-package-to-npm.yml b/.github/workflows/publish-package-to-npm.yml index 69364cdf..a614f700 100644 --- a/.github/workflows/publish-package-to-npm.yml +++ b/.github/workflows/publish-package-to-npm.yml @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..8b3ac96d --- /dev/null +++ b/.vscode/settings.json @@ -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" + ] +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 392e2f10..981cd59e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,11 +4,21 @@ ### 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 impovement is a key to the new straightforward way of configuring tune's appearance in Block Tunes menu. +- `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 @@ -18,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 @@ -112,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. @@ -167,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) @@ -178,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 @@ -227,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) @@ -340,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 @@ -356,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 @@ -388,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 @@ -396,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 diff --git a/docs/tools.md b/docs/tools.md index ec0dd7d7..7df5ceb6 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -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 diff --git a/example/tools/embed b/example/tools/embed index 35742f01..23de06be 160000 --- a/example/tools/embed +++ b/example/tools/embed @@ -1 +1 @@ -Subproject commit 35742f01ae5875d442b145121d3c9b71b23aea56 +Subproject commit 23de06be69bb9e636a2278b0d54f8c2d85d7ae13 diff --git a/example/tools/header b/example/tools/header index 585bca27..056ff5e5 160000 --- a/example/tools/header +++ b/example/tools/header @@ -1 +1 @@ -Subproject commit 585bca271f7696cd17533fa5877d1f72b3a03d2e +Subproject commit 056ff5e52677d239dfe73b9ddc6e074474a54a63 diff --git a/example/tools/link b/example/tools/link index 0fc365ef..13372270 160000 --- a/example/tools/link +++ b/example/tools/link @@ -1 +1 @@ -Subproject commit 0fc365ef256decb8f765fb72b060d5bef9254aa3 +Subproject commit 13372270afdee5dfb0f1509b491888db7f06a8e4 diff --git a/example/tools/nested-list b/example/tools/nested-list index 9add9538..6da4d453 160000 --- a/example/tools/nested-list +++ b/example/tools/nested-list @@ -1 +1 @@ -Subproject commit 9add95389afca0711c05260a92283fae8eb209eb +Subproject commit 6da4d45354b8b05b384ea175d7685c733c80a9c8 diff --git a/example/tools/table b/example/tools/table index ad0d9012..a45d7329 160000 --- a/example/tools/table +++ b/example/tools/table @@ -1 +1 @@ -Subproject commit ad0d9012d149e3ca4b41a5ce096b31767cc8c1fd +Subproject commit a45d7329f877552bfa6c3c4e10c9bdd13f3e9e31 diff --git a/package.json b/package.json index a1a4154a..2fa60ef3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.26.0-rc.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" } } diff --git a/src/assets/arrow-down.svg b/src/assets/arrow-down.svg deleted file mode 100644 index 2ce78b4e..00000000 --- a/src/assets/arrow-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/arrow-up.svg b/src/assets/arrow-up.svg deleted file mode 100644 index 4bc8ed14..00000000 --- a/src/assets/arrow-up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/bold.svg b/src/assets/bold.svg deleted file mode 100644 index 1cbd3b96..00000000 --- a/src/assets/bold.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/assets/cross.svg b/src/assets/cross.svg deleted file mode 100644 index 54055697..00000000 --- a/src/assets/cross.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/dots.svg b/src/assets/dots.svg deleted file mode 100644 index 57967964..00000000 --- a/src/assets/dots.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/assets/italic.svg b/src/assets/italic.svg deleted file mode 100644 index 94c777cb..00000000 --- a/src/assets/italic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/link.svg b/src/assets/link.svg deleted file mode 100644 index fee4a278..00000000 --- a/src/assets/link.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/plus.svg b/src/assets/plus.svg deleted file mode 100644 index 8ab786f5..00000000 --- a/src/assets/plus.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/sad-face.svg b/src/assets/sad-face.svg deleted file mode 100644 index 1d86e3f6..00000000 --- a/src/assets/sad-face.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/search.svg b/src/assets/search.svg deleted file mode 100644 index 1485338b..00000000 --- a/src/assets/search.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/toggler-down.svg b/src/assets/toggler-down.svg deleted file mode 100644 index 86570a92..00000000 --- a/src/assets/toggler-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/unlink.svg b/src/assets/unlink.svg deleted file mode 100644 index 5dea3d82..00000000 --- a/src/assets/unlink.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/codex.ts b/src/codex.ts index 29dc4b8f..730e5b09 100644 --- a/src/codex.ts +++ b/src/codex.ts @@ -19,7 +19,6 @@ declare const VERSION: string; * Short Description (눈_눈;) * * @version 2.18.0 - * * @license Apache-2.0 * @author CodeX-Team */ diff --git a/src/components/__module.ts b/src/components/__module.ts index f18de55f..c5391422 100644 --- a/src/components/__module.ts +++ b/src/components/__module.ts @@ -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 { +export default class Module> { /** * Each module can provide some UI elements that will be stored in this property */ @@ -92,8 +91,9 @@ export default class Module { /** * @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) { diff --git a/src/components/block-tunes/block-tune-delete.ts b/src/components/block-tunes/block-tune-delete.ts index 27bd612d..781e00a5 100644 --- a/src/components/block-tunes/block-tune-delete.ts +++ b/src/components/block-tunes/block-tune-delete.ts @@ -1,11 +1,10 @@ /** * @class DeleteTune * @classdesc Editor's default tune that moves up selected block - * * @copyright 2018 */ import { API, BlockTune, PopoverItem } from '../../../types'; -import $ from '../dom'; +import { IconCross } from '@codexteam/icons'; /** * @@ -37,22 +36,20 @@ export default class DeleteTune implements BlockTune { */ public render(): PopoverItem { return { - icon: $.svg('cross', 14, 14).outerHTML, + icon: IconCross, label: this.api.i18n.t('Delete'), name: 'delete', confirmation: { label: this.api.i18n.t('Click to delete'), - onActivate: (item, e): void => this.handleClick(e), + onActivate: (): void => this.handleClick(), }, }; } /** * Delete block conditions passed - * - * @param {MouseEvent} event - click event */ - public handleClick(event: MouseEvent): void { + public handleClick(): void { this.api.blocks.delete(); } } diff --git a/src/components/block-tunes/block-tune-move-down.ts b/src/components/block-tunes/block-tune-move-down.ts index 614a778f..805ee5f9 100644 --- a/src/components/block-tunes/block-tune-move-down.ts +++ b/src/components/block-tunes/block-tune-move-down.ts @@ -1,13 +1,13 @@ /** * @class MoveDownTune * @classdesc Editor's default tune - Moves down highlighted block - * * @copyright 2018 */ -import $ from '../dom'; import { API, BlockTune, PopoverItem } from '../../../types'; import Popover from '../utils/popover'; +import { IconChevronDown } from '@codexteam/icons'; + /** * @@ -46,7 +46,7 @@ export default class MoveDownTune implements BlockTune { */ public render(): PopoverItem { return { - icon: $.svg('arrow-down', 14, 14).outerHTML, + icon: IconChevronDown, label: this.api.i18n.t('Move down'), onActivate: (item, event): void => this.handleClick(event), name: 'move-down', @@ -72,6 +72,7 @@ export default class MoveDownTune implements BlockTune { window.setTimeout(() => { button.classList.remove(this.CSS.animation); + // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 500); return; diff --git a/src/components/block-tunes/block-tune-move-up.ts b/src/components/block-tunes/block-tune-move-up.ts index 1d07e3bf..47950a99 100644 --- a/src/components/block-tunes/block-tune-move-up.ts +++ b/src/components/block-tunes/block-tune-move-up.ts @@ -1,12 +1,11 @@ /** * @class MoveUpTune * @classdesc Editor's default tune that moves up selected block - * * @copyright 2018 */ -import $ from '../dom'; -import { API, BlockTune, BlockAPI, PopoverItem } from '../../../types'; +import { API, BlockTune, PopoverItem } from '../../../types'; import Popover from '../../components/utils/popover'; +import { IconChevronUp } from '@codexteam/icons'; /** * @@ -45,7 +44,7 @@ export default class MoveUpTune implements BlockTune { */ public render(): PopoverItem { return { - icon: $.svg('arrow-up', 14, 14).outerHTML, + icon: IconChevronUp, label: this.api.i18n.t('Move up'), onActivate: (item, e): void => this.handleClick(e), name: 'move-up', @@ -71,6 +70,7 @@ export default class MoveUpTune implements BlockTune { window.setTimeout(() => { button.classList.remove(this.CSS.animation); + // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 500); return; diff --git a/src/components/block/api.ts b/src/components/block/api.ts index a5049253..d760ab63 100644 --- a/src/components/block/api.ts +++ b/src/components/block/api.ts @@ -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} */ validate(data: BlockToolData): Promise { diff --git a/src/components/block/index.ts b/src/components/block/index.ts index d2007f6a..a84dd311 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -60,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 - * */ /** @@ -74,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', } @@ -89,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 @@ -244,7 +243,7 @@ export default class Block extends EventsDispatcher { * @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 */ @@ -281,7 +280,7 @@ export default class Block extends EventsDispatcher { } /** - * 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[]} */ @@ -396,7 +395,7 @@ export default class Block extends EventsDispatcher { /** * 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} */ @@ -417,7 +416,7 @@ export default class Block extends EventsDispatcher { } /** - * 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} */ @@ -487,7 +486,7 @@ export default class Block extends EventsDispatcher { /** * 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); @@ -619,7 +618,7 @@ export default class Block extends EventsDispatcher { }; }) .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'); }); } @@ -628,7 +627,6 @@ export default class Block extends EventsDispatcher { * 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} valid */ @@ -855,10 +853,10 @@ export default class Block extends EventsDispatcher { * 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 => { @@ -874,7 +872,7 @@ export default class Block extends EventsDispatcher { } /** - * 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 => { diff --git a/src/components/blocks.ts b/src/components/blocks.ts index 1f25a7fc..b3713ad4 100644 --- a/src/components/blocks.ts +++ b/src/components/blocks.ts @@ -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 */ diff --git a/src/components/core.ts b/src/components/core.ts index 471bf7ff..08a7beed 100644 --- a/src/components/core.ts +++ b/src/components/core.ts @@ -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; /** diff --git a/src/components/dom.ts b/src/components/dom.ts index 75628ea2..7294ab0c 100644 --- a/src/components/dom.ts +++ b/src/components/dom.ts @@ -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 = ``; - - 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; diff --git a/src/components/domIterator.ts b/src/components/domIterator.ts index d11d3b4d..5c3137b4 100644 --- a/src/components/domIterator.ts +++ b/src/components/domIterator.ts @@ -175,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)(); } diff --git a/src/components/flipper.ts b/src/components/flipper.ts index ed2df15f..28998460 100644 --- a/src/components/flipper.ts +++ b/src/components/flipper.ts @@ -74,7 +74,7 @@ export default class Flipper { /** * Contains list of callbacks to be executed on each flip */ - private flipCallbacks: Array<() => void> = [] + private flipCallbacks: Array<() => void> = []; /** * @param {FlipperOptions} options - different constructing settings @@ -279,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(); } /** diff --git a/src/components/i18n/index.ts b/src/components/i18n/index.ts index c88871b7..b522a365 100644 --- a/src/components/i18n/index.ts +++ b/src/components/i18n/index.ts @@ -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 */ diff --git a/src/components/inline-tools/inline-tool-bold.ts b/src/components/inline-tools/inline-tool-bold.ts index a33753de..9ddef46c 100644 --- a/src/components/inline-tools/inline-tool-bold.ts +++ b/src/components/inline-tools/inline-tool-bold.ts @@ -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 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 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); diff --git a/src/components/inline-tools/inline-tool-italic.ts b/src/components/inline-tools/inline-tool-italic.ts index da728b81..e271bd45 100644 --- a/src/components/inline-tools/inline-tool-italic.ts +++ b/src/components/inline-tools/inline-tool-italic.ts @@ -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 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 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); diff --git a/src/components/inline-tools/inline-tool-link.ts b/src/components/inline-tools/inline-tool-link.ts index 6c5db6d5..19a370d2 100644 --- a/src/components/inline-tools/inline-tool-link.ts +++ b/src/components/inline-tools/inline-tool-link.ts @@ -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 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); } diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index 57e7b6d5..a711d52f 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -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); @@ -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, }); - } + }; } diff --git a/src/components/modules/api/caret.ts b/src/components/modules/api/caret.ts index 63519d24..0e104632 100644 --- a/src/components/modules/api/caret.ts +++ b/src/components/modules/api/caret.ts @@ -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); - } + }; } diff --git a/src/components/modules/api/notifier.ts b/src/components/modules/api/notifier.ts index dae16ba8..85b457d4 100644 --- a/src/components/modules/api/notifier.ts +++ b/src/components/modules/api/notifier.ts @@ -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({ diff --git a/src/components/modules/api/readonly.ts b/src/components/modules/api/readonly.ts index 7b804839..5c527052 100644 --- a/src/components/modules/api/readonly.ts +++ b/src/components/modules/api/readonly.ts @@ -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 { diff --git a/src/components/modules/api/sanitizer.ts b/src/components/modules/api/sanitizer.ts index 79c91656..1f1a259c 100644 --- a/src/components/modules/api/sanitizer.ts +++ b/src/components/modules/api/sanitizer.ts @@ -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 { diff --git a/src/components/modules/api/selection.ts b/src/components/modules/api/selection.ts index 6cd7bcc1..a796ddd1 100644 --- a/src/components/modules/api/selection.ts +++ b/src/components/modules/api/selection.ts @@ -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 { diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index 7aa62018..a0ebea77 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -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'; @@ -233,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; @@ -424,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)(); } @@ -482,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)(); } diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 9ac893bc..fb5f3039 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -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 { @@ -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 { diff --git a/src/components/modules/blockSelection.ts b/src/components/modules/blockSelection.ts index 682b7aeb..c0e552a7 100644 --- a/src/components/modules/blockSelection.ts +++ b/src/components/modules/blockSelection.ts @@ -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} */ public copySelectedBlocks(e: ClipboardEvent): Promise { diff --git a/src/components/modules/caret.ts b/src/components/modules/caret.ts index 94b54711..0494dcaf 100644 --- a/src/components/modules/caret.ts +++ b/src/components/modules/caret.ts @@ -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
'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 { *

| right first-level siblings *

| * - * * @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[] { diff --git a/src/components/modules/crossBlockSelection.ts b/src/components/modules/crossBlockSelection.ts index fa8c4962..fc0c40c9 100644 --- a/src/components/modules/crossBlockSelection.ts +++ b/src/components/modules/crossBlockSelection.ts @@ -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. diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index e901897b..bc568169 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -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 { 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: + * - 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 { 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[]): 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; } } + diff --git a/src/components/modules/readonly.ts b/src/components/modules/readonly.ts index 77a5fc8c..973f459b 100644 --- a/src/components/modules/readonly.ts +++ b/src/components/modules/readonly.ts @@ -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 */ diff --git a/src/components/modules/rectangleSelection.ts b/src/components/modules/rectangleSelection.ts index 03f249f7..d545036d 100644 --- a/src/components/modules/rectangleSelection.ts +++ b/src/components/modules/rectangleSelection.ts @@ -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} + * @returns {Object} */ private genHTML(): {container: Element; overlay: Element} { const { UI } = this.Editor; diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index 6f800069..fd794354 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -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} */ public async insertBlock(item: OutputBlockData): Promise { diff --git a/src/components/modules/saver.ts b/src/components/modules/saver.ts index be5a16a5..28084f06 100644 --- a/src/components/modules/saver.ts +++ b/src/components/modules/saver.ts @@ -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 diff --git a/src/components/modules/toolbar/blockSettings.ts b/src/components/modules/toolbar/blockSettings.ts index 16f72e36..9629a1f5 100644 --- a/src/components/modules/toolbar/blockSettings.ts +++ b/src/components/modules/toolbar/blockSettings.ts @@ -1,6 +1,5 @@ import Module from '../../__module'; import $ from '../../dom'; -import * as _ from '../../utils'; import SelectionUtils from '../../selection'; import Block from '../../block'; import Popover, { PopoverEvent } from '../../utils/popover'; @@ -53,7 +52,7 @@ export default class BlockSettings extends Module { * @todo remove once BlockSettings becomes standalone non-module class */ public get flipper(): Flipper { - return this.popover.flipper; + return this.popover?.flipper; } /** @@ -64,7 +63,7 @@ export default class BlockSettings extends Module { /** * Popover instance. There is a util for vertical lists. */ - private popover: Popover; + private popover: Popover | undefined; /** * Panel with block settings with 2 sections: @@ -192,5 +191,5 @@ export default class BlockSettings extends Module { */ private onOverlayClicked = (): void => { this.close(); - } + }; } diff --git a/src/components/modules/toolbar/conversion.ts b/src/components/modules/toolbar/conversion.ts index 30f1bd7d..754c7212 100644 --- a/src/components/modules/toolbar/conversion.ts +++ b/src/components/modules/toolbar/conversion.ts @@ -50,7 +50,7 @@ export default class ConversionToolbar extends Module { /** * 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 { public async replaceWithBlock(replacingToolName: string, blockDataOverrides?: BlockToolData): Promise { /** * 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 { /** * 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 { _.delay(() => { this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock); + // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 10)(); } diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index 5c469fa0..c147df4d 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -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 { * * @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 { } else { this.blockActions.hide(); } + // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 50)(); } @@ -341,8 +336,9 @@ export default class Toolbar extends Module { * - 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 { * - 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( @@ -478,7 +474,7 @@ export default class Toolbar extends Module { }, 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 */ diff --git a/src/components/modules/toolbar/inline.ts b/src/components/modules/toolbar/inline.ts index 1a4b6d06..327e3dff 100644 --- a/src/components/modules/toolbar/inline.ts +++ b/src/components/modules/toolbar/inline.ts @@ -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 { 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 { /** * 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 { /** * 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 { 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 { 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 { }); }); - 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 { })); } - 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 { 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(); + } } /** diff --git a/src/components/modules/tools.ts b/src/components/modules/tools.ts index 5ff3cf7a..e46c9feb 100644 --- a/src/components/modules/tools.ts +++ b/src/components/modules/tools.ts @@ -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} */ 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 { return this.available.inlineTools; diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index ead5c9ba..4003bfc2 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -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: * * * * * - * * @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 { */ 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 { */ this.addLoader(); - /** - * Append SVG sprite - */ - this.appendSVGSprite(); - /** * Load and append CSS */ @@ -235,12 +225,15 @@ export default class UI extends Module { 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 { * 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 { /** * 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 { 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 { * 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 { /** * 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 { */ 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); - } } diff --git a/src/components/polyfills.ts b/src/components/polyfills.ts index 42b86a05..d6f96aa1 100644 --- a/src/components/polyfills.ts +++ b/src/components/polyfills.ts @@ -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) { diff --git a/src/components/selection.ts b/src/components/selection.ts index d4bbcbf9..0a4f1f0a 100644 --- a/src/components/selection.ts +++ b/src/components/selection.ts @@ -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(); diff --git a/src/components/tools/base.ts b/src/components/tools/base.ts index a4147979..f89345a9 100644 --- a/src/components/tools/base.ts +++ b/src/components/tools/base.ts @@ -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 +export type ToolOptions = Omit; interface ConstructorOptions { name: string; @@ -174,8 +174,7 @@ export default abstract class BaseTool { /** * @class - * - * @param {ConstructorOptions} - Constructor options + * @param {ConstructorOptions} options - Constructor options */ constructor({ name, diff --git a/src/components/tools/factory.ts b/src/components/tools/factory.ts index f994d396..b00427b0 100644 --- a/src/components/tools/factory.ts +++ b/src/components/tools/factory.ts @@ -29,7 +29,6 @@ export default class ToolsFactory { /** * @class - * * @param config - tools config * @param editorConfig - EditorJS config * @param api - EditorJS API module diff --git a/src/components/ui/toolbox.ts b/src/components/ui/toolbox.ts index e213ac9a..1d0c25db 100644 --- a/src/components/ui/toolbox.ts +++ b/src/components/ui/toolbox.ts @@ -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 { /** * 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,21 +80,21 @@ export default class Toolbox extends EventsDispatcher { /** * Text labels used in the Toolbox. Should be passed from the i18n module */ - private i18nLabels: Record; + private i18nLabels: Record; /** * Current module HTML Elements */ private nodes: { - toolbox: HTMLElement; + toolbox: HTMLElement | null; } = { - toolbox: null, - }; + toolbox: null, + }; /** * CSS styles * - * @returns {object.} + * @returns {Object} */ private static get CSS(): { [name: string]: string } { return { @@ -102,11 +102,6 @@ export default class Toolbox extends EventsDispatcher { }; } - /** - * Id of listener added used to remove it on destroy() - */ - private clickListenerId: string = null; - /** * Toolbox constructor * @@ -114,7 +109,7 @@ export default class Toolbox extends EventsDispatcher { * @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; i18nLabels: Record}) { + constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection; i18nLabels: Record}) { super(); this.api = api; @@ -150,8 +145,8 @@ export default class Toolbox extends EventsDispatcher { /** * 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 { 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,7 +182,7 @@ export default class Toolbox extends EventsDispatcher { return; } - this.popover.show(); + this.popover?.show(); this.opened = true; this.emit(ToolboxEvent.Opened); } @@ -198,7 +191,7 @@ export default class Toolbox extends EventsDispatcher { * Close Toolbox */ public close(): void { - this.popover.hide(); + this.popover?.hide(); this.opened = false; this.emit(ToolboxEvent.Closed); } @@ -219,31 +212,24 @@ export default class Toolbox extends EventsDispatcher { */ 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; } /** @@ -259,7 +245,7 @@ export default class Toolbox extends EventsDispatcher { icon: toolboxItem.icon, label: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)), name: tool.name, - onActivate: (e): void => { + onActivate: (): void => { this.toolButtonActivated(tool.name, toolboxItem.data); }, secondaryLabel: tool.shortcut ? _.beautifyShortcut(tool.shortcut) : '', @@ -267,12 +253,12 @@ export default class Toolbox extends EventsDispatcher { }; return this.toolsToBeDisplayed - .reduce((result, tool) => { + .reduce((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)); } @@ -280,29 +266,6 @@ export default class Toolbox extends EventsDispatcher { }, []); } - /** - * 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 */ diff --git a/src/components/utils.ts b/src/components/utils.ts index ec3b28bd..27ed6228 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -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 { 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(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: 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; } /** diff --git a/src/components/utils/events.ts b/src/components/utils/events.ts index 3f886fe7..3e5bc754 100644 --- a/src/components/utils/events.ts +++ b/src/components/utils/events.ts @@ -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 */ diff --git a/src/components/utils/listeners.ts b/src/components/utils/listeners.ts index e0e35609..4b6189a7 100644 --- a/src/components/utils/listeners.ts +++ b/src/components/utils/listeners.ts @@ -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 { diff --git a/src/components/utils/popover.ts b/src/components/utils/popover.ts index 5fa77c93..96c5e2be 100644 --- a/src/components/utils/popover.ts +++ b/src/components/utils/popover.ts @@ -61,12 +61,12 @@ export default class Popover extends EventsDispatcher { 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 @@ -150,7 +150,7 @@ export default class Popover extends EventsDispatcher { /** * ScrollLocker instance */ - private scrollLocker = new ScrollLocker() + private scrollLocker = new ScrollLocker(); /** * Editor container element @@ -236,6 +236,7 @@ export default class Popover extends EventsDispatcher { if (this.searchable) { setTimeout(() => { this.search.focus(); + // eslint-disable-next-line @typescript-eslint/no-magic-numbers }, 100); } @@ -436,11 +437,9 @@ export default class Popover extends EventsDispatcher { 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); @@ -601,7 +600,7 @@ export default class Popover extends EventsDispatcher { } el.classList.remove(Popover.CSS.itemNoHover); - } + }; /** * Removes class responsible for special focus behavior on an item @@ -621,7 +620,7 @@ export default class Popover extends EventsDispatcher { */ private onFlip = (): void => { this.disableSpecialHoverAndFocusBehavior(); - } + }; /** * Reactivates flipper instance. diff --git a/src/components/utils/sanitizer.ts b/src/components/utils/sanitizer.ts index 5b4bc8a2..9d55a82a 100644 --- a/src/components/utils/sanitizer.ts +++ b/src/components/utils/sanitizer.ts @@ -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 { diff --git a/src/components/utils/scroll-locker.ts b/src/components/utils/scroll-locker.ts index 394ddcec..af9a5e86 100644 --- a/src/components/utils/scroll-locker.ts +++ b/src/components/utils/scroll-locker.ts @@ -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 diff --git a/src/components/utils/search-input.ts b/src/components/utils/search-input.ts index a5b8e3ae..00fbc74b 100644 --- a/src/components/utils/search-input.ts +++ b/src/components/utils/search-input.ts @@ -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); diff --git a/src/components/utils/shortcuts.ts b/src/components/utils/shortcuts.ts index 3e45210f..12adf10c 100644 --- a/src/components/utils/shortcuts.ts +++ b/src/components/utils/shortcuts.ts @@ -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 { diff --git a/src/styles/conversion-toolbar.css b/src/styles/conversion-toolbar.css index 0676ecb0..817ea544 100644 --- a/src/styles/conversion-toolbar.css +++ b/src/styles/conversion-toolbar.css @@ -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 { diff --git a/src/styles/export.css b/src/styles/export.css index 889f7cfd..3e3bb524 100644 --- a/src/styles/export.css +++ b/src/styles/export.css @@ -40,10 +40,23 @@ */ .cdx-settings-button { @apply --toolbar-button; + 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; + } } /** @@ -91,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 { diff --git a/src/styles/inline-toolbar.css b/src/styles/inline-toolbar.css index b118df87..c935fe5a 100644 --- a/src/styles/inline-toolbar.css +++ b/src/styles/inline-toolbar.css @@ -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); diff --git a/src/styles/input.css b/src/styles/input.css index 1c94d8fe..d4b5f819 100644 --- a/src/styles/input.css +++ b/src/styles/input.css @@ -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; } } diff --git a/src/styles/settings.css b/src/styles/settings.css index 40b4f9d6..1906bda3 100644 --- a/src/styles/settings.css +++ b/src/styles/settings.css @@ -34,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); - } - } } } diff --git a/src/styles/toolbar.css b/src/styles/toolbar.css index f3fef0ef..0358eab5 100644 --- a/src/styles/toolbar.css +++ b/src/styles/toolbar.css @@ -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; + } + } } /** diff --git a/src/styles/ui.css b/src/styles/ui.css index e594e071..9032e791 100644 --- a/src/styles/ui.css +++ b/src/styles/ui.css @@ -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%; -} \ No newline at end of file +} diff --git a/src/styles/variables.css b/src/styles/variables.css index 9012c0f9..8c600cda 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -49,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 @@ -82,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; @@ -159,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 { @@ -234,15 +240,20 @@ 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); + } } } } diff --git a/src/tools/paragraph b/src/tools/paragraph index 21cbdea6..6e45413c 160000 --- a/src/tools/paragraph +++ b/src/tools/paragraph @@ -1 +1 @@ -Subproject commit 21cbdea6e5e61094b046f47e8cb423a817cec3ed +Subproject commit 6e45413ccdfd021f1800eb6e5bf7440184d5ab7c diff --git a/src/tools/stub/index.ts b/src/tools/stub/index.ts index 025066c6..d5cd1be8 100644 --- a/src/tools/stub/index.ts +++ b/src/tools/stub/index.ts @@ -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 = ``; 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); diff --git a/src/types-internal/svg.d.ts b/src/types-internal/svg.d.ts deleted file mode 100644 index 5b2ab770..00000000 --- a/src/types-internal/svg.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Allow to import .svg from components/modules/ui from TypeScript file - */ -declare module '*.svg' { - const content: string; - export default content; -} diff --git a/test/cypress/support/commands.ts b/test/cypress/support/commands.ts index b2a24276..21929c5d 100644 --- a/test/cypress/support/commands.ts +++ b/test/cypress/support/commands.ts @@ -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; +}); diff --git a/test/cypress/support/index.d.ts b/test/cypress/support/index.d.ts index 477e3e7d..4fce3b37 100644 --- a/test/cypress/support/index.d.ts +++ b/test/cypress/support/index.d.ts @@ -47,6 +47,19 @@ declare global { * @param data — data to render */ render(data: OutputData): Chainable; + + /** + * 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; } interface ApplicationWindow { diff --git a/test/cypress/tests/api/block.spec.ts b/test/cypress/tests/api/block.spec.ts index 09b55efb..53db9309 100644 --- a/test/cypress/tests/api/block.spec.ts +++ b/test/cypress/tests/api/block.spec.ts @@ -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', () => { }); }); }); - }); diff --git a/test/cypress/tests/api/blocks.spec.ts b/test/cypress/tests/api/blocks.spec.ts index 7eb74869..ab3f2ac8 100644 --- a/test/cypress/tests/api/blocks.spec.ts +++ b/test/cypress/tests/api/blocks.spec.ts @@ -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); + }); + }); + }); }); diff --git a/test/cypress/tests/api/tools.spec.ts b/test/cypress/tests/api/tools.spec.ts index aa73a933..e29ad7ef 100644 --- a/test/cypress/tests/api/tools.spec.ts +++ b/test/cypress/tests/api/tools.spec.ts @@ -1,5 +1,6 @@ -import { ToolboxConfig, BlockToolData, ToolboxConfigEntry } from '../../../../types'; -import { TunesMenuConfig } from '../../../../types/tools'; +import { ToolboxConfig, BlockToolData, ToolboxConfigEntry, PasteConfig } from '../../../../types'; +import EditorJS from '../../../../types'; +import { HTMLPasteEvent, TunesMenuConfig } from '../../../../types/tools'; /* eslint-disable @typescript-eslint/no-empty-function */ @@ -97,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 */ @@ -121,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 [ @@ -182,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, @@ -191,86 +192,9 @@ 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 - */ - class TestTool { - /** - * Returns toolbox config as list of entries one of which has missing icon - */ - public static get toolbox(): ToolboxConfig { - return { - title: 'Entry 2', - }; - } - } - - 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(); - - cy.get('[data-cy=editorjs]') - .get('div.ce-popover__item[data-item-name=testTool]') - .should('not.exist'); - }); - - it('should skip toolbox entries that have no icon', () => { - const skippedEntryTitle = 'Entry 2'; - - /** - * Tool with one of the toolbox entries with icon missing - */ - class TestTool { - /** - * Returns toolbox config as list of entries one of which has missing icon - */ - public static get toolbox(): ToolboxConfig { - return [ - { - title: 'Entry 1', - icon: ICON, - }, - { - title: skippedEntryTitle, - }, - ]; - } - } - - 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(); - - cy.get('[data-cy=editorjs]') - .get('div.ce-popover__item[data-item-name=testTool]') - .should('have.length', 1) - .should('not.contain', skippedEntryTitle); - }); }); - context('Tunes', () => { + 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 { @@ -490,4 +414,437 @@ describe('Editor Tools Api', () => { .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': '', + }) + .then(() => { + expect(toolsOnPaste).to.be.called; + }); + }); + + /** + * tags: ['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': '', // 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 + * } + * }] + * -> + * + */ + 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': '', + }) + .then(() => { + expect(pastedElement).not.to.be.undefined; + + /** + * Check that the 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': '', + }) + .then(() => { + expect(pastedElement).not.to.be.undefined; + + /** + * Check that