Release 2.16 (#966)

* 2.16.0

* [Refactor] Separate internal and external settings (#845)

* Enable flipping tools via standalone class (#830)

* Enable flipping tools via standalone class

* use flipper to refactor (#842)

* use flipper to refactor

* save changes

* update

* fix flipper on inline toolbar

* ready for testing

* requested changes

* update doc

* updates

* destroy flippers

* some requested changes

* update

* update

* ready

* update

* last changes

* update docs

* Hghl active button of CT, simplify activate/deactivate

* separate dom iterator

* unhardcode directions

* fixed a link in readme.md (#856)

* Fix Block selection via CMD+A (#829)

* Fix Block selection via CMD+A

* Delete editor.js.map

* update

* update

* Update CHANGELOG.md

* Improve style of selected blocks (#858)

* Cross-block-selection style improved

* Update CHANGELOG.md

* Fix case when property 'observer' in modificationObserver is not defined (#866)

* Bump lodash.template from 4.4.0 to 4.5.0 (#885)

Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0)

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

* Bump eslint-utils from 1.3.1 to 1.4.2 (#886)

Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.2)

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

* Bump mixin-deep from 1.3.1 to 1.3.2 (#887)

Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

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

* update bundle and readme

* Update README.md

* upd codeowners, fix funding

* Minor Docs Fix according to main Readme (#916)

* Inline Toolbar now contains Conversion Toolbar (#932)

* Block lifecycle hooks (#906)

* [Fix] Arrow selection (#964)

* Fix arrow selection

* Add docs

* [issue-926]: fix dom iterator leafing when items are empty (#958)

* [issue-926]: fix dom iterator leafing when items are empty

* update Changelog

* Issue 869 (#963)

* Fix issue 943 (#965)

* [Draft] Feature/tooltip enhancements (#907)

* initial

* update

* make module standalone

* use tooltips as external module

* update

* build via prod mode

* add tooltips as external module

* add declaration file and options param

* add api tooltip

* update

* removed submodule

* removed due to the incorrect setip

* setup tooltips again

* wip

* update tooltip module

* toolbox, inline toolbar

* Tooltips in block tunes not uses shorthand

* shorthand in a plus and block settings

* fix doc

* Update tools-inline.md

* Delete tooltip.css

* Update CHANGELOG.md

* Update codex.tooltips

* Update api.md

* [issue-779]: Grammarly conflicts (#956)

* grammarly conflicts

* update

* upd bundle

* Submodule Header now on master

* Submodule Marker now on master

* Submodule Paragraph now on master

* Submodule InlineCode now on master

* Submodule Simple Image now on master

* [issue-868]: Deleting multiple blocks triggers back button in Firefox (#967)

* Deleting multiple blocks triggers back button in Firefox

@evgenusov

* Update editor.js

* Update CHANGELOG.md

* pass options on removeEventListener (#904)

* pass options on removeEventListener by removeAll

* rebuild

* Merge branch 'release/2.16' into pr/904

* Update CHANGELOG.md

* Update inline.ts

* [Fix] Selection rangecount (#968)

* Fix #952 (#969)

* Update codex.tooltips

* Selection bugfix (#970)

* Selection bugfix

* fix cross block selection

* close inline toolbar when blocks selected via shift

* remove inline toolbar closing on cross block selection mouse up due to the bug (#972)

* [Feature] Log levels (#971)

* Decrease margins (#973)

* Decrease margins

* Update editor.licenses.txt

* Update src/components/domIterator.ts

Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com>

* [Fix] Fix delete blocks api method (#974)

* Update docs/usage.md

Co-Authored-By: Murod Khaydarov <murod.haydarov@gmail.com>

* rm unused

* Update yarn.lock file

* upd bundle, changelog
This commit is contained in:
Peter Savchenko 2019-11-30 23:42:39 +03:00 committed by GitHub
parent 0600233e63
commit ac93017c70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 3902 additions and 2317 deletions

3
.gitmodules vendored
View file

@ -46,3 +46,6 @@
[submodule "example/tools/warning"] [submodule "example/tools/warning"]
path = example/tools/warning path = example/tools/warning
url = https://github.com/editor-js/warning url = https://github.com/editor-js/warning
[submodule "src/components/external/codex.tooltips"]
path = src/components/external/codex.tooltips
url = https://github.com/codex-team/codex.tooltips

View file

@ -175,7 +175,7 @@
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS
Copyright © 2015-2018 CodeX Copyright © 2015-present CodeX
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

26
dist/editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
MIT MIT
MIT License MIT License
Copyright (c) 2014-2018 Sebastian McKenzie and other contributors Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the
@ -28,7 +28,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MIT MIT
MIT License MIT License
Copyright (c) 2014-2018 Sebastian McKenzie <sebmck@gmail.com> Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the
@ -150,6 +150,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
codex-tooltip
MIT
Copyright 2019 CodeX https://codex.so
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
core-js core-js
MIT MIT
Copyright (c) 2014-2018 Denis Pushkarev Copyright (c) 2014-2018 Denis Pushkarev
@ -379,3 +390,24 @@ Apache License
regenerator-runtime regenerator-runtime
MIT MIT
MIT License
Copyright (c) 2014-present, Facebook, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

18
dist/sprite.svg vendored
View file

@ -8,7 +8,7 @@
<path d="M8.024 4.1v8.6a1.125 1.125 0 0 1-2.25 0V4.1L2.18 7.695A1.125 1.125 0 1 1 .59 6.104L6.103.588c.44-.439 1.151-.439 1.59 0l5.516 5.516a1.125 1.125 0 0 1-1.59 1.59L8.023 4.1z"/> <path d="M8.024 4.1v8.6a1.125 1.125 0 0 1-2.25 0V4.1L2.18 7.695A1.125 1.125 0 1 1 .59 6.104L6.103.588c.44-.439 1.151-.439 1.59 0l5.516 5.516a1.125 1.125 0 0 1-1.59 1.59L8.023 4.1z"/>
</symbol> </symbol>
<symbol id="bold"><path d="M5.997 14H1.72c-.618 0-1.058-.138-1.323-.415C.132 13.308 0 12.867 0 12.262V1.738C0 1.121.135.676.406.406.676.136 1.114 0 1.719 0h4.536c.669 0 1.248.041 1.738.124.49.083.93.242 1.318.478a3.458 3.458 0 0 1 1.461 1.752c.134.366.2.753.2 1.16 0 1.401-.7 2.426-2.1 3.075 1.84.586 2.76 1.726 2.76 3.42 0 .782-.2 1.487-.602 2.114a3.61 3.61 0 0 1-1.623 1.39 5.772 5.772 0 0 1-1.471.377c-.554.073-1.2.11-1.939.11zm-.21-6.217h-2.95v4.087h3.046c1.916 0 2.874-.69 2.874-2.072 0-.707-.248-1.22-.745-1.537-.496-.319-1.238-.478-2.225-.478zM2.837 2.13v3.619h2.597c.707 0 1.252-.067 1.638-.2.385-.134.68-.389.883-.765.16-.267.239-.566.239-.897 0-.707-.252-1.176-.755-1.409-.503-.232-1.27-.348-2.301-.348H2.836z"/> <symbol id="bold" viewBox="0 0 12 14"><path d="M5.997 14H1.72c-.618 0-1.058-.138-1.323-.415C.132 13.308 0 12.867 0 12.262V1.738C0 1.121.135.676.406.406.676.136 1.114 0 1.719 0h4.536c.669 0 1.248.041 1.738.124.49.083.93.242 1.318.478a3.458 3.458 0 0 1 1.461 1.752c.134.366.2.753.2 1.16 0 1.401-.7 2.426-2.1 3.075 1.84.586 2.76 1.726 2.76 3.42 0 .782-.2 1.487-.602 2.114a3.61 3.61 0 0 1-1.623 1.39 5.772 5.772 0 0 1-1.471.377c-.554.073-1.2.11-1.939.11zm-.21-6.217h-2.95v4.087h3.046c1.916 0 2.874-.69 2.874-2.072 0-.707-.248-1.22-.745-1.537-.496-.319-1.238-.478-2.225-.478zM2.837 2.13v3.619h2.597c.707 0 1.252-.067 1.638-.2.385-.134.68-.389.883-.765.16-.267.239-.566.239-.897 0-.707-.252-1.176-.755-1.409-.503-.232-1.27-.348-2.301-.348H2.836z"/>
</symbol> </symbol>
<symbol id="cross" viewBox="0 0 237 237"> <symbol id="cross" viewBox="0 0 237 237">
<path transform="rotate(45 280.675 51.325)" d="M191 191V73c0-5.523 4.477-10 10-10h25c5.523 0 10 4.477 10 10v118h118c5.523 0 10 4.477 10 10v25c0 5.523-4.477 10-10 10H236v118c0 5.523-4.477 10-10 10h-25c-5.523 0-10-4.477-10-10V236H73c-5.523 0-10-4.477-10-10v-25c0-5.523 4.477-10 10-10h118z"/> <path transform="rotate(45 280.675 51.325)" d="M191 191V73c0-5.523 4.477-10 10-10h25c5.523 0 10 4.477 10 10v118h118c5.523 0 10 4.477 10 10v25c0 5.523-4.477 10-10 10H236v118c0 5.523-4.477 10-10 10h-25c-5.523 0-10-4.477-10-10V236H73c-5.523 0-10-4.477-10-10v-25c0-5.523 4.477-10 10-10h118z"/>
@ -22,11 +22,13 @@
</g> </g>
</symbol> </symbol>
<symbol id="italic"> <symbol id="italic" viewBox="0 0 4 11">
<path d="M19.211 15.326l-1.44 7.108c-.1.493-.305.865-.615 1.117a1.64 1.64 0 0 1-1.064.379c-.4 0-.697-.13-.894-.388-.197-.258-.247-.627-.15-1.108l1.426-7.036c.098-.486.297-.853.597-1.1.299-.245.648-.368 1.047-.368.399 0 .703.123.912.369.21.246.27.588.181 1.027zm-.831-2.663c-.38 0-.682-.116-.909-.35-.227-.232-.301-.561-.223-.987.07-.385.266-.703.588-.952.322-.25.665-.374 1.03-.374.353 0 .645.113.876.34.232.225.308.554.229.986-.077.42-.27.747-.58.983-.308.236-.646.354-1.011.354z"/> <path d="M3.289 4.17L2.164 9.713c-.078.384-.238.674-.48.87-.243.198-.52.296-.831.296-.312 0-.545-.1-.699-.302-.153-.202-.192-.49-.116-.864L1.15 4.225c.077-.38.232-.665.466-.857a1.25 1.25 0 01.818-.288c.312 0 .55.096.713.288.163.192.21.46.141.801zm-.667-2.09c-.295 0-.53-.09-.706-.273-.176-.181-.233-.439-.173-.77.055-.302.207-.55.457-.745C2.45.097 2.716 0 3 0c.273 0 .5.088.68.265.179.176.238.434.177.771-.06.327-.21.583-.45.767-.24.185-.502.277-.785.277z"/>
</symbol> </symbol>
<symbol id="link"><path d="M15.439 21.153a4.202 4.202 0 0 0 2.72.63l-.985.986a4.202 4.202 0 1 1-5.943-5.945l2.093-2.093a4.202 4.202 0 0 1 5.934-.009l-1.655 1.656a5.886 5.886 0 0 1-.02.019 1.835 1.835 0 0 0-2.585.009l-2.093 2.093a1.836 1.836 0 0 0 2.534 2.654zm3.122-8.306a4.202 4.202 0 0 0-2.72-.63l.985-.986a4.202 4.202 0 1 1 5.943 5.945l-2.093 2.093a4.202 4.202 0 0 1-5.934.009l1.655-1.656.02-.019a1.835 1.835 0 0 0 2.585-.009l2.093-2.093a1.836 1.836 0 0 0-2.534-2.654z"/> <symbol id="link" viewBox="0 0 14 10">
<path d="M6 0v2H5a3 3 0 000 6h1v2H5A5 5 0 115 0h1zm2 0h1a5 5 0 110 10H8V8h1a3 3 0 000-6H8V0zM5 4h4a1 1 0 110 2H5a1 1 0 110-2z"/>
</symbol> </symbol>
<symbol id="plus" viewBox="0 0 14 14"> <symbol id="plus" viewBox="0 0 14 14">
<path d="M8.05 5.8h4.625a1.125 1.125 0 0 1 0 2.25H8.05v4.625a1.125 1.125 0 0 1-2.25 0V8.05H1.125a1.125 1.125 0 0 1 0-2.25H5.8V1.125a1.125 1.125 0 0 1 2.25 0V5.8z"/> <path d="M8.05 5.8h4.625a1.125 1.125 0 0 1 0 2.25H8.05v4.625a1.125 1.125 0 0 1-2.25 0V8.05H1.125a1.125 1.125 0 0 1 0-2.25H5.8V1.125a1.125 1.125 0 0 1 2.25 0V5.8z"/>
@ -36,7 +38,11 @@
<path fill="#D76B6B" fill-rule="nonzero" d="M26 52C11.64 52 0 40.36 0 26S11.64 0 26 0s26 11.64 26 26-11.64 26-26 26zm0-3.25c12.564 0 22.75-10.186 22.75-22.75S38.564 3.25 26 3.25 3.25 13.436 3.25 26 13.436 48.75 26 48.75zM15.708 33.042a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm23.834 0a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm-15.875 5.452a1.083 1.083 0 1 1-1.834-1.155c1.331-2.114 3.49-3.179 6.334-3.179 2.844 0 5.002 1.065 6.333 3.18a1.083 1.083 0 1 1-1.833 1.154c-.913-1.45-2.366-2.167-4.5-2.167s-3.587.717-4.5 2.167z"/> <path fill="#D76B6B" fill-rule="nonzero" d="M26 52C11.64 52 0 40.36 0 26S11.64 0 26 0s26 11.64 26 26-11.64 26-26 26zm0-3.25c12.564 0 22.75-10.186 22.75-22.75S38.564 3.25 26 3.25 3.25 13.436 3.25 26 13.436 48.75 26 48.75zM15.708 33.042a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm23.834 0a2.167 2.167 0 1 1 0-4.334 2.167 2.167 0 0 1 0 4.334zm-15.875 5.452a1.083 1.083 0 1 1-1.834-1.155c1.331-2.114 3.49-3.179 6.334-3.179 2.844 0 5.002 1.065 6.333 3.18a1.083 1.083 0 1 1-1.833 1.154c-.913-1.45-2.366-2.167-4.5-2.167s-3.587.717-4.5 2.167z"/>
</symbol> </symbol>
<symbol id="unlink" viewBox="0 0 16 18"> <symbol id="toggler-down">
<path transform="rotate(-45 8.358 11.636)" d="M9.14 9.433c.008-.12-.087-.686-.112-.81a1.4 1.4 0 0 0-1.64-1.106l-3.977.772a1.4 1.4 0 0 0 .535 2.749l.935-.162s.019 1.093.592 2.223l-1.098.148A3.65 3.65 0 1 1 2.982 6.08l3.976-.773c1.979-.385 3.838.919 4.28 2.886.51 2.276-1.084 2.816-1.073 2.935.011.12-.394-1.59-1.026-1.696zm3.563-.875l2.105 3.439a3.65 3.65 0 0 1-6.19 3.868L6.47 12.431c-1.068-1.71-.964-2.295-.49-3.07.067-.107 1.16-1.466 1.48-.936-.12.036.9 1.33.789 1.398-.656.41-.28.76.13 1.415l2.145 3.435a1.4 1.4 0 0 0 2.375-1.484l-1.132-1.941c.42-.435 1.237-1.054.935-2.69zm1.88-2.256h3.4a1.125 1.125 0 0 1 0 2.25h-3.4a1.125 1.125 0 0 1 0-2.25zM11.849.038c.62 0 1.125.503 1.125 1.125v3.4a1.125 1.125 0 0 1-2.25 0v-3.4c0-.622.503-1.125 1.125-1.125z"/> <path d="M6.5 9.294a.792.792 0 01-.562-.232L2.233 5.356a.794.794 0 011.123-1.123L6.5 7.377l3.144-3.144a.794.794 0 011.123 1.123L7.062 9.062a.792.792 0 01-.562.232z"/>
</symbol>
<symbol id="unlink" viewBox="0 0 15 11">
<path d="M13.073 2.099l-1.448 1.448A3 3 0 009 2H8V0h1c1.68 0 3.166.828 4.073 2.099zM6.929 4l-.879.879L7.172 6H5a1 1 0 110-2h1.929zM6 0v2H5a3 3 0 100 6h1v2H5A5 5 0 115 0h1zm6.414 7l2.122 2.121-1.415 1.415L11 8.414l-2.121 2.122L7.464 9.12 9.586 7 7.464 4.879 8.88 3.464 11 5.586l2.121-2.122 1.415 1.415L12.414 7z"/>
</symbol></svg> </symbol></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -1,5 +1,24 @@
# Changelog # Changelog
### 2.16
- `Improvements` — Inline Toolbar design improved
- `Improvements` — Conversion Toolbar now included in the Inline Toolbar [#853](https://github.com/codex-team/editor.js/issues/853)
- `Improvements` — All buttons now have beautiful Tooltips provided by [CodeX Tooltips](https://github.com/codex-team/codex.tooltips)
- `New` — new Tooltips API for displaying tooltips near your custom elements
`New` *API* — Block [lifecycle hooks](tools.md#block-lifecycle-hooks)
`New` *Inline Tools API* — Ability to specify Tool's title via `title` static getter.
- `Fix` — On selection from end to start backspace is working as expected now [#869](https://github.com/codex-team/editor.js/issues/869)
`Fix` — Fix flipper with empty dom iterator [#926](https://github.com/codex-team/editor.js/issues/926)
- `Fix` — Normalize node before walking through children at `isEmpty` method [943](https://github.com/codex-team/editor.js/issues/943)
`Fix` — Fixed Grammarly conflict [#779](https://github.com/codex-team/editor.js/issues/779)
`Improvements` — Module Listeners now correctly removes events with options [#904](https://github.com/codex-team/editor.js/pull/904)
`Improvements` — Styles API: `.cdx-block` default vertical margins decreased from 0.7 to 0.4 ems.
`Fix` — Fixed History Back on block deletion by Backspace in Firefox [#967](https://github.com/codex-team/editor.js/pull/967)
- `Fix` — Fixed `getRangeCount` call if range count is 0 [#938](https://github.com/codex-team/editor.js/issues/938)
- `New` — Log levels now available to suppress Editor.js console messages [#962](https://github.com/codex-team/editor.js/issues/962)
- `Fix` — Fixed wrong navigation on block deletion
### 2.15.1 ### 2.15.1
- `Refactoring` — Constants of tools settings separated by internal and external to correspond API - `Refactoring` — Constants of tools settings separated by internal and external to correspond API

View file

@ -160,7 +160,55 @@ It makes following steps:
After executing the `destroy` method, editor inctance becomes an empty object. This way you will free occupied JS Heap on your page. After executing the `destroy` method, editor inctance becomes an empty object. This way you will free occupied JS Heap on your page.
### Shorthands ### Tooltip API
Methods for showing Tooltip helper near your elements. Parameters are the same as in [CodeX Tooltips](http://github.com/codex-team/codex.tooltips) lib.
![](https://capella.pics/00e7094a-fdb9-429b-8015-9c56f19b4ef5.jpg)
#### Show
Method shows tooltip with custom content on passed element
```js
this.api.tooltip.show(element, content, options);
```
| parameter | type | description |
| -- | -- | -- |
| `element` | _HTMLElement_ | Tooltip will be showed near this element |
| `content` | _String_ or _Node_ | Content that will be appended to the Tooltip |
| `options` | _Object_ | Some displaying options, see below |
Available showing options
| name | type | action |
| -- | -- | -- |
| placement | `top`, `bottom`, `left`, `right` | Where to place the tooltip. Default value is `bottom' |
| marginTop | _Number_ | Offset above the tooltip with `top` placement |
| marginBottom | _Number_ | Offset below the tooltip with `bottom` placement |
| marginLeft | _Number_ | Offset at left from the tooltip with `left` placement |
| marginRight | _Number_ | Offset at right from the tooltip with `right` placement |
| delay | _Number_ | Delay before showing, in ms. Default is `70` |
| hidingDelay | _Number_ | Delay before hiding, in ms. Default is `0` |
#### Hide
Method hides the Tooltip.
```js
this.api.tooltip.hide();
```
#### onHover
Decorator for showing tooltip near some element by "mouseenter" and hide by "mouseleave".
```js
this.api.tooltip.onHover(element, content, options);
```
### API Shorthands
Editor`s API provides some shorthands for API methods. Editor`s API provides some shorthands for API methods.
@ -179,3 +227,4 @@ const editor = EditorJS();
editor.focus(); editor.focus();
editor.save(); editor.save();
``` ```

View file

@ -44,7 +44,7 @@ Then require this script.
### Save sources to project ### Save sources to project
Copy [editorjs.js](../dist/editor.js) file to your project and load it. Copy [editor.js](../dist/editor.js) file to your project and load it.
```html ```html
<script src="editor.js"></script> <script src="editor.js"></script>

View file

@ -123,3 +123,28 @@ static get sanitize() {
``` ```
Read more about Sanitizer configuration at the [Tools#sanitize](tools.md#sanitize) Read more about Sanitizer configuration at the [Tools#sanitize](tools.md#sanitize)
### Specifying a title
You can pass your Tool's title via `title` static getter. It can be used, for example, in the Tooltip with
icon description that appears by hover.
![](https://capella.pics/00e7094a-fdb9-429b-8015-9c56f19b4ef5.jpg)
```ts
export default class BoldInlineTool implements InlineTool {
/**
* Specifies Tool as Inline Toolbar Tool
*
* @return {boolean}
*/
public static isInline = true;
/**
* Title for hover-tooltip
*/
public static title: string = 'Bold';
// ... other methods
}
```

View file

@ -462,3 +462,17 @@ class ListTool {
} }
} }
``` ```
## Block Lifecycle hooks
### `rendered()`
Called after Block contents is added to the page
### `updated()`
Called each time Block contents is updated
### `removed()`
Called after Block contents is removed from the page but before Block instance deleted

View file

@ -102,4 +102,26 @@ var editor = new EditorJS({
``` ```
If you are using your custom `Initial Block`, `placeholder` property is passed in `config` to your Tool constructor. If you are using your custom `Initial Block`, `placeholder` property is passed in `config` to your Tool constructor.
## Log level
You can specify log level for Editor.js console messages via `logLevel` property of configuration:
```js
var editor = new EditorJS({
//...
logLevel: 'WARN'
//..
})
```
Possible values:
| Value | Description |
| ----- | ---------------------------- |
| `VERBOSE` | Show all messages |
| `INFO` | Show info and debug messages |
| `WARN` | Show errors and warns only |
| `ERROR` | Show errors only |

View file

@ -265,7 +265,7 @@
{ {
type: 'image', type: 'image',
data: { data: {
url: 'https://codex.so/upload/redactor_images/o_e48549d1855c7fc1807308dd14990126.jpg', url: 'https://capella.pics/bbe6896c-ca1f-439e-8cb6-ebfda0d397d6.jpg',
caption: '', caption: '',
stretched: false, stretched: false,
withBorder: true, withBorder: true,

View file

@ -254,7 +254,7 @@
{ {
type: 'image', type: 'image',
data: { data: {
url: 'https://codex.so/upload/redactor_images/o_e48549d1855c7fc1807308dd14990126.jpg', url: 'https://capella.pics/bbe6896c-ca1f-439e-8cb6-ebfda0d397d6.jpg',
caption: '', caption: '',
stretched: false, stretched: false,
withBorder: true, withBorder: true,

@ -1 +1 @@
Subproject commit 4be61b52257911ce4bfbbddd55199e7eb952e839 Subproject commit 0e143926c9c8d693f2441d0f6a1982d28dcd5bf8

@ -1 +1 @@
Subproject commit 522f4bcf56776ffb573f462be6a4eb87d6fc9e7b Subproject commit 37a5e8d1db305cf75acd6622d9b82e2f308f71c6

@ -1 +1 @@
Subproject commit c306bcb33c88eaa3c172eaf387fbcd06ae6b297f Subproject commit a2a0dabb0a6f5f93d96264ee3774e53f2a64898e

@ -1 +1 @@
Subproject commit da9f8a109706d4b4593e19afbfc05614466eb987 Subproject commit 0fd96a70b371af0cc0720b8c2c0d0888b8a44bc5

View file

@ -1,6 +1,6 @@
{ {
"name": "@editorjs/editorjs", "name": "@editorjs/editorjs",
"version": "2.15.1", "version": "2.16.0",
"description": "Editor.js — Native JS, based on API and Open Source", "description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js", "main": "dist/editor.js",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",

View file

@ -1 +1 @@
<svg width="12" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M5.997 14H1.72c-.618 0-1.058-.138-1.323-.415C.132 13.308 0 12.867 0 12.262V1.738C0 1.121.135.676.406.406.676.136 1.114 0 1.719 0h4.536c.669 0 1.248.041 1.738.124.49.083.93.242 1.318.478a3.458 3.458 0 0 1 1.461 1.752c.134.366.2.753.2 1.16 0 1.401-.7 2.426-2.1 3.075 1.84.586 2.76 1.726 2.76 3.42 0 .782-.2 1.487-.602 2.114a3.61 3.61 0 0 1-1.623 1.39 5.772 5.772 0 0 1-1.471.377c-.554.073-1.2.11-1.939.11zm-.21-6.217h-2.95v4.087h3.046c1.916 0 2.874-.69 2.874-2.072 0-.707-.248-1.22-.745-1.537-.496-.319-1.238-.478-2.225-.478zM2.837 2.13v3.619h2.597c.707 0 1.252-.067 1.638-.2.385-.134.68-.389.883-.765.16-.267.239-.566.239-.897 0-.707-.252-1.176-.755-1.409-.503-.232-1.27-.348-2.301-.348H2.836z"/></svg> <svg width="12" height="14" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M5.997 14H1.72c-.618 0-1.058-.138-1.323-.415C.132 13.308 0 12.867 0 12.262V1.738C0 1.121.135.676.406.406.676.136 1.114 0 1.719 0h4.536c.669 0 1.248.041 1.738.124.49.083.93.242 1.318.478a3.458 3.458 0 0 1 1.461 1.752c.134.366.2.753.2 1.16 0 1.401-.7 2.426-2.1 3.075 1.84.586 2.76 1.726 2.76 3.42 0 .782-.2 1.487-.602 2.114a3.61 3.61 0 0 1-1.623 1.39 5.772 5.772 0 0 1-1.471.377c-.554.073-1.2.11-1.939.11zm-.21-6.217h-2.95v4.087h3.046c1.916 0 2.874-.69 2.874-2.072 0-.707-.248-1.22-.745-1.537-.496-.319-1.238-.478-2.225-.478zM2.837 2.13v3.619h2.597c.707 0 1.252-.067 1.638-.2.385-.134.68-.389.883-.765.16-.267.239-.566.239-.897 0-.707-.252-1.176-.755-1.409-.503-.232-1.27-.348-2.301-.348H2.836z"/></svg>

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 794 B

View file

@ -1,3 +1,3 @@
<svg width="34" height="34" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 11">
<path d="M19.211 15.326l-1.44 7.108c-.1.493-.305.865-.615 1.117a1.64 1.64 0 0 1-1.064.379c-.4 0-.697-.13-.894-.388-.197-.258-.247-.627-.15-1.108l1.426-7.036c.098-.486.297-.853.597-1.1.299-.245.648-.368 1.047-.368.399 0 .703.123.912.369.21.246.27.588.181 1.027zm-.831-2.663c-.38 0-.682-.116-.909-.35-.227-.232-.301-.561-.223-.987.07-.385.266-.703.588-.952.322-.25.665-.374 1.03-.374.353 0 .645.113.876.34.232.225.308.554.229.986-.077.42-.27.747-.58.983-.308.236-.646.354-1.011.354z"/> <path d="M3.289 4.17L2.164 9.713c-.078.384-.238.674-.48.87-.243.198-.52.296-.831.296-.312 0-.545-.1-.699-.302-.153-.202-.192-.49-.116-.864L1.15 4.225c.077-.38.232-.665.466-.857a1.25 1.25 0 01.818-.288c.312 0 .55.096.713.288.163.192.21.46.141.801zm-.667-2.09c-.295 0-.53-.09-.706-.273-.176-.181-.233-.439-.173-.77.055-.302.207-.55.457-.745C2.45.097 2.716 0 3 0c.273 0 .5.088.68.265.179.176.238.434.177.771-.06.327-.21.583-.45.767-.24.185-.502.277-.785.277z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 530 B

View file

@ -1 +1,3 @@
<svg width="34" height="34" xmlns="http://www.w3.org/2000/svg"><path d="M15.439 21.153a4.202 4.202 0 0 0 2.72.63l-.985.986a4.202 4.202 0 1 1-5.943-5.945l2.093-2.093a4.202 4.202 0 0 1 5.934-.009l-1.655 1.656a5.886 5.886 0 0 1-.02.019 1.835 1.835 0 0 0-2.585.009l-2.093 2.093a1.836 1.836 0 0 0 2.534 2.654zm3.122-8.306a4.202 4.202 0 0 0-2.72-.63l.985-.986a4.202 4.202 0 1 1 5.943 5.945l-2.093 2.093a4.202 4.202 0 0 1-5.934.009l1.655-1.656.02-.019a1.835 1.835 0 0 0 2.585-.009l2.093-2.093a1.836 1.836 0 0 0-2.534-2.654z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 10">
<path d="M6 0v2H5a3 3 0 000 6h1v2H5A5 5 0 115 0h1zm2 0h1a5 5 0 110 10H8V8h1a3 3 0 000-6H8V0zM5 4h4a1 1 0 110 2H5a1 1 0 110-2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 526 B

After

Width:  |  Height:  |  Size: 199 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 9.294a.792.792 0 01-.562-.232L2.233 5.356a.794.794 0 011.123-1.123L6.5 7.377l3.144-3.144a.794.794 0 011.123 1.123L7.062 9.062a.792.792 0 01-.562.232z"/>
</svg>

After

Width:  |  Height:  |  Size: 240 B

View file

@ -1,3 +1,3 @@
<svg width="16" height="18" viewBox="0 0 16 18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 11">
<path transform="rotate(-45 8.358 11.636)" d="M9.14 9.433c.008-.12-.087-.686-.112-.81a1.4 1.4 0 0 0-1.64-1.106l-3.977.772a1.4 1.4 0 0 0 .535 2.749l.935-.162s.019 1.093.592 2.223l-1.098.148A3.65 3.65 0 1 1 2.982 6.08l3.976-.773c1.979-.385 3.838.919 4.28 2.886.51 2.276-1.084 2.816-1.073 2.935.011.12-.394-1.59-1.026-1.696zm3.563-.875l2.105 3.439a3.65 3.65 0 0 1-6.19 3.868L6.47 12.431c-1.068-1.71-.964-2.295-.49-3.07.067-.107 1.16-1.466 1.48-.936-.12.036.9 1.33.789 1.398-.656.41-.28.76.13 1.415l2.145 3.435a1.4 1.4 0 0 0 2.375-1.484l-1.132-1.941c.42-.435 1.237-1.054.935-2.69zm1.88-2.256h3.4a1.125 1.125 0 0 1 0 2.25h-3.4a1.125 1.125 0 0 1 0-2.25zM11.849.038c.62 0 1.125.503 1.125 1.125v3.4a1.125 1.125 0 0 1-2.25 0v-3.4c0-.622.503-1.125 1.125-1.125z"/> <path d="M13.073 2.099l-1.448 1.448A3 3 0 009 2H8V0h1c1.68 0 3.166.828 4.073 2.099zM6.929 4l-.879.879L7.172 6H5a1 1 0 110-2h1.929zM6 0v2H5a3 3 0 100 6h1v2H5A5 5 0 115 0h1zm6.414 7l2.122 2.121-1.415 1.415L11 8.414l-2.121 2.122L7.464 9.12 9.586 7 7.464 4.879 8.88 3.464 11 5.586l2.121-2.122 1.415 1.415L12.414 7z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 892 B

After

Width:  |  Height:  |  Size: 384 B

View file

@ -17,10 +17,8 @@ export default class DeleteTune implements BlockTune {
/** /**
* Styles * Styles
* @type {{wrapper: string}}
*/ */
private CSS = { private CSS = {
wrapper: 'ass',
button: 'ce-settings__button', button: 'ce-settings__button',
buttonDelete: 'ce-settings__button--delete', buttonDelete: 'ce-settings__button--delete',
buttonConfirm: 'ce-settings__button--confirm', buttonConfirm: 'ce-settings__button--confirm',
@ -64,6 +62,12 @@ export default class DeleteTune implements BlockTune {
this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {}); this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {});
this.nodes.button.appendChild($.svg('cross', 12, 12)); this.nodes.button.appendChild($.svg('cross', 12, 12));
this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false); this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false);
/**
* Enable tooltip module
*/
this.api.tooltip.onHover(this.nodes.button, 'Delete');
return this.nodes.button; return this.nodes.button;
} }
@ -95,8 +99,8 @@ export default class DeleteTune implements BlockTune {
this.api.events.off('block-settings-closed', this.resetConfirmation); this.api.events.off('block-settings-closed', this.resetConfirmation);
this.api.blocks.delete(); this.api.blocks.delete();
this.api.toolbar.close(); this.api.toolbar.close();
this.api.tooltip.hide();
/** /**
* Prevent firing ui~documentClicked that can drop currentBlock pointer * Prevent firing ui~documentClicked that can drop currentBlock pointer

View file

@ -46,6 +46,12 @@ export default class MoveDownTune implements BlockTune {
(event) => this.handleClick(event as MouseEvent, moveDownButton), (event) => this.handleClick(event as MouseEvent, moveDownButton),
false, false,
); );
/**
* Enable tooltip module on button
*/
this.api.tooltip.onHover(moveDownButton, 'Move down');
return moveDownButton; return moveDownButton;
} }
@ -88,5 +94,7 @@ export default class MoveDownTune implements BlockTune {
/** Change blocks positions */ /** Change blocks positions */
this.api.blocks.swap(currentBlockIndex, currentBlockIndex + 1); this.api.blocks.swap(currentBlockIndex, currentBlockIndex + 1);
/** Hide the Tooltip */
this.api.tooltip.hide();
} }
} }

View file

@ -47,6 +47,12 @@ export default class MoveUpTune implements BlockTune {
(event) => this.handleClick(event as MouseEvent, moveUpButton), (event) => this.handleClick(event as MouseEvent, moveUpButton),
false, false,
); );
/**
* Enable tooltip module on button
*/
this.api.tooltip.onHover(moveUpButton, 'Move up');
return moveUpButton; return moveUpButton;
} }
@ -94,5 +100,8 @@ export default class MoveUpTune implements BlockTune {
/** Change blocks positions */ /** Change blocks positions */
this.api.blocks.swap(currentBlockIndex, currentBlockIndex - 1); this.api.blocks.swap(currentBlockIndex, currentBlockIndex - 1);
/** Hide the Tooltip */
this.api.tooltip.hide();
} }
} }

View file

@ -11,7 +11,7 @@ import {
import {SavedData} from '../types-internal/block-data'; import {SavedData} from '../types-internal/block-data';
import $ from './dom'; import $ from './dom';
import _ from './utils'; import * as _ from './utils';
/** /**
* @class Block * @class Block
@ -28,6 +28,21 @@ import DeleteTune from './block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down'; import MoveDownTune from './block-tunes/block-tune-move-down';
import SelectionUtils from './selection'; import SelectionUtils from './selection';
/**
* Available Block Tool API methods
*/
export enum BlockToolAPI {
/**
* @todo remove method in 3.0.0
* @deprecated use 'rendered' hook instead
*/
APPEND_CALLBACK = 'appendCallback',
RENDERED = 'rendered',
UPDATED = 'updated',
REMOVED = 'removed',
ON_PASTE = 'onPaste',
}
/** /**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance * @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
* *
@ -165,10 +180,20 @@ export default class Block {
* @return {HTMLElement} * @return {HTMLElement}
*/ */
get pluginsContent(): HTMLElement { get pluginsContent(): HTMLElement {
const pluginsContent = this.holder.querySelector(`.${Block.CSS.content}`); const blockContentNodes = this.holder.querySelector(`.${Block.CSS.content}`);
if (pluginsContent && pluginsContent.childNodes.length) { if (blockContentNodes && blockContentNodes.childNodes.length) {
return pluginsContent.childNodes[0] as HTMLElement; /**
* Editors Block content can contain different Nodes from extensions
* We use DOM isExtensionNode to ignore such Nodes and return first Block that does not match filtering list
*/
for (let child = blockContentNodes.childNodes.length - 1; child >= 0; child--) {
const contentNode = blockContentNodes.childNodes[child];
if (!$.isExtensionNode(contentNode)) {
return contentNode as HTMLElement;
}
}
} }
return null; return null;
@ -337,6 +362,29 @@ export default class Block {
*/ */
private mutationObserver: MutationObserver; private mutationObserver: MutationObserver;
/**
* Debounce Timer
* @type {number}
*/
private readonly modificationDebounceTimer = 450;
/**
* Is fired when DOM mutation has been happened
*/
private didMutated = _.debounce((): void => {
/**
* Drop cache
*/
this.cachedInputs = [];
/**
* Update current input
*/
this.updateCurrentInput();
this.call(BlockToolAPI.UPDATED);
}, this.modificationDebounceTimer);
/** /**
* @constructor * @constructor
* @param {String} toolName - Tool name that passed on initialization * @param {String} toolName - Tool name that passed on initialization
@ -375,12 +423,16 @@ export default class Block {
* @param {String} methodName * @param {String} methodName
* @param {Object} params * @param {Object} params
*/ */
public call(methodName: string, params: object) { public call(methodName: string, params?: object) {
/** /**
* call Tool's method with the instance context * call Tool's method with the instance context
*/ */
if (this.tool[methodName] && this.tool[methodName] instanceof Function) { if (this.tool[methodName] && this.tool[methodName] instanceof Function) {
this.tool[methodName].call(this.tool, params); try {
this.tool[methodName].call(this.tool, params);
} catch (e) {
_.log(`Error during '${methodName}' call: ${e.message}`, 'error');
}
} }
} }
@ -485,7 +537,15 @@ export default class Block {
/** /**
* Observe DOM mutations to update Block inputs * Observe DOM mutations to update Block inputs
*/ */
this.mutationObserver.observe(this.holder, {childList: true, subtree: true}); this.mutationObserver.observe(
this.holder.firstElementChild,
{
childList: true,
subtree: true,
characterData: true,
attributes: true,
},
);
} }
/** /**
@ -495,21 +555,6 @@ export default class Block {
this.mutationObserver.disconnect(); this.mutationObserver.disconnect();
} }
/**
* Is fired when DOM mutation has been happened
*/
private didMutated = (): void => {
/**
* Drop cache
*/
this.cachedInputs = [];
/**
* Update current input
*/
this.updateCurrentInput();
}
/** /**
* Make default Block wrappers and put Tool`s content there * Make default Block wrappers and put Tool`s content there
* @returns {HTMLDivElement} * @returns {HTMLDivElement}

View file

@ -1,6 +1,6 @@
import _ from './utils'; import * as _ from './utils';
import $ from './dom'; import $ from './dom';
import Block from './block'; import Block, {BlockToolAPI} from './block';
/** /**
* @class Blocks * @class Blocks
@ -12,7 +12,6 @@ import Block from './block';
* *
*/ */
export default class Blocks { export default class Blocks {
/** /**
* Get length of Block instances array * Get length of Block instances array
* *
@ -47,16 +46,27 @@ export default class Blocks {
* blocks[0] = new Block(...) * blocks[0] = new Block(...)
* *
* @param {Blocks} instance Blocks instance * @param {Blocks} instance Blocks instance
* @param {Number|String} index block index * @param {Number|String} property block index or any Blocks class property to set
* @param {Block} block Block to set * @param {Block} value value to set
* @returns {Boolean} * @returns {Boolean}
*/ */
public static set(instance: Blocks, index: number, block: Block) { public static set(instance: Blocks, property: number | string, value: Block | any) {
if (isNaN(Number(index))) {
return false; /**
* If property name is not a number (method or other property, access it via reflect
*/
if (isNaN(Number(property))) {
Reflect.set(instance, property, value);
return true;
} }
instance.insert(+index, block); /**
* If property is number, call insert method to emulate array behaviour
*
* @example
* blocks[0] = new Block();
*/
instance.insert(+property, value);
return true; return true;
} }
@ -65,15 +75,22 @@ export default class Blocks {
* Proxy trap to implement array-like getter * Proxy trap to implement array-like getter
* *
* @param {Blocks} instance Blocks instance * @param {Blocks} instance Blocks instance
* @param {Number|String} index Block index * @param {Number|String} property Blocks class property
* @returns {Block|*} * @returns {Block|*}
*/ */
public static get(instance: Blocks, index: number) { public static get(instance: Blocks, property: any | number) {
if (isNaN(Number(index))) {
return instance[index]; /**
* If property is not a number, get it via Reflect object
*/
if (isNaN(Number(property))) {
return Reflect.get(instance, property);
} }
return instance.get(+index); /**
* If property is a number (Block index) return Block by passed index
*/
return instance.get(+property);
} }
/** /**
@ -103,7 +120,7 @@ export default class Blocks {
*/ */
public push(block: Block): void { public push(block: Block): void {
this.blocks.push(block); this.blocks.push(block);
this.workingArea.appendChild(block.holder); this.insertToDOM(block);
} }
/** /**
@ -145,6 +162,7 @@ export default class Blocks {
if (replace) { if (replace) {
this.blocks[index].holder.remove(); this.blocks[index].holder.remove();
this.blocks[index].call(BlockToolAPI.REMOVED);
} }
const deleteCount = replace ? 1 : 0; const deleteCount = replace ? 1 : 0;
@ -154,14 +172,14 @@ export default class Blocks {
if (index > 0) { if (index > 0) {
const previousBlock = this.blocks[index - 1]; const previousBlock = this.blocks[index - 1];
previousBlock.holder.insertAdjacentElement('afterend', block.holder); this.insertToDOM(block, 'afterend', previousBlock);
} else { } else {
const nextBlock = this.blocks[index + 1]; const nextBlock = this.blocks[index + 1];
if (nextBlock) { if (nextBlock) {
nextBlock.holder.insertAdjacentElement('beforebegin', block.holder); this.insertToDOM(block, 'beforebegin', nextBlock);
} else { } else {
this.workingArea.appendChild(block.holder); this.insertToDOM(block);
} }
} }
} }
@ -176,6 +194,9 @@ export default class Blocks {
} }
this.blocks[index].holder.remove(); this.blocks[index].holder.remove();
this.blocks[index].call(BlockToolAPI.REMOVED);
this.blocks.splice(index, 1); this.blocks.splice(index, 1);
} }
@ -184,6 +205,9 @@ export default class Blocks {
*/ */
public removeAll(): void { public removeAll(): void {
this.workingArea.innerHTML = ''; this.workingArea.innerHTML = '';
this.blocks.forEach((block) => block.call(BlockToolAPI.REMOVED));
this.blocks.length = 0; this.blocks.length = 0;
} }
@ -220,4 +244,21 @@ export default class Blocks {
public indexOf(block: Block): number { public indexOf(block: Block): number {
return this.blocks.indexOf(block); return this.blocks.indexOf(block);
} }
/**
* Insert new Block into DOM
*
* @param {Block} block - Block to insert
* @param {InsertPosition} position insert position (if set, will use insertAdjacentElement)
* @param {Block} target Block related to position
*/
private insertToDOM(block: Block, position?: InsertPosition, target?: Block): void {
if (position) {
target.holder.insertAdjacentElement(position, block.holder);
} else {
this.workingArea.appendChild(block.holder);
}
block.call(BlockToolAPI.RENDERED);
}
} }

View file

@ -1,7 +1,8 @@
import $ from './dom'; import $ from './dom';
import _ from './utils'; import * as _ from './utils';
import {EditorConfig, OutputData, SanitizerConfig} from '../../types'; import {EditorConfig, OutputData, SanitizerConfig} from '../../types';
import {EditorModules} from '../types-internal/editor-modules'; import {EditorModules} from '../types-internal/editor-modules';
import {LogLevels} from './utils';
/** /**
* @typedef {Core} Core - editor core class * @typedef {Core} Core - editor core class
@ -75,9 +76,11 @@ export default class Core {
await this.init(); await this.init();
await this.start(); await this.start();
_.log('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧', 'log', '', 'color: #E24A75'); _.logLabeled('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧', 'log', '', 'color: #E24A75');
setTimeout(async () => {
await this.render();
setTimeout(() => {
if ((this.configuration as EditorConfig).autofocus) { if ((this.configuration as EditorConfig).autofocus) {
const {BlockManager, Caret} = this.moduleInstances; const {BlockManager, Caret} = this.moduleInstances;
@ -142,6 +145,12 @@ export default class Core {
this.config.holder = 'editorjs'; this.config.holder = 'editorjs';
} }
if (!this.config.logLevel) {
this.config.logLevel = LogLevels.VERBOSE;
}
_.setLogLevel(this.config.logLevel);
/** /**
* If initial Block's Tool was not passed, use the Paragraph Tool * If initial Block's Tool was not passed, use the Paragraph Tool
*/ */
@ -268,7 +277,12 @@ export default class Core {
}), }),
Promise.resolve(), Promise.resolve(),
); );
}
/**
* Render initial data
*/
private render(): Promise<void> {
return this.moduleInstances.Renderer.render(this.config.data.blocks); return this.moduleInstances.Renderer.render(this.config.data.blocks);
} }
@ -276,8 +290,14 @@ export default class Core {
* Make modules instances and save it to the @property this.moduleInstances * Make modules instances and save it to the @property this.moduleInstances
*/ */
private constructModules(): void { private constructModules(): void {
modules.forEach( (Module) => { modules.forEach( (module) => {
/**
* If module has non-default exports, passed object contains them all and default export as 'default' property
*/
const Module = typeof module === 'function' ? module : module.default;
try { try {
/** /**
* We use class name provided by displayName property * We use class name provided by displayName property
* *

View file

@ -98,9 +98,12 @@ export default class Dom {
* Append one or several elements to the parent * Append one or several elements to the parent
* *
* @param {Element|DocumentFragment} parent - where to append * @param {Element|DocumentFragment} parent - where to append
* @param {Element|Element[]} elements - element or elements list * @param {Element|Element[]|Text|Text[]} elements - element or elements list
*/ */
public static append(parent: Element|DocumentFragment, elements: Element|Element[]|DocumentFragment): void { public static append(
parent: Element|DocumentFragment,
elements: Element|Element[]|DocumentFragment|Text|Text[],
): void {
if ( Array.isArray(elements) ) { if ( Array.isArray(elements) ) {
elements.forEach( (el) => parent.appendChild(el) ); elements.forEach( (el) => parent.appendChild(el) );
} else { } else {
@ -367,6 +370,11 @@ export default class Dom {
return this.isNodeEmpty(node); return this.isNodeEmpty(node);
} }
/**
* Normalize node to merge several text nodes to one to reduce tree walker iterations
*/
node.normalize();
treeWalker.push(node.firstChild); treeWalker.push(node.firstChild);
while ( treeWalker.length > 0 ) { while ( treeWalker.length > 0 ) {
@ -527,4 +535,16 @@ export default class Dom {
if (typeof element === 'string') { return document.getElementById(element); } if (typeof element === 'string') { return document.getElementById(element); }
return element; return element;
} }
/**
* Method checks passed Node if it is some extension Node
* @param {Node} node - any node
*/
public static isExtensionNode(node: Node): boolean {
const extensions = [
'GRAMMARLY-EXTENSION',
];
return node && extensions.includes(node.nodeName);
}
} }

View file

@ -96,6 +96,13 @@ export default class DomIterator {
* @return {Number} index of focused node * @return {Number} index of focused node
*/ */
private leafNodesAndReturnIndex(direction: string): number { private leafNodesAndReturnIndex(direction: string): number {
/**
* if items are empty then there is nothing to leaf
*/
if (this.items.length === 0) {
return this.cursor;
}
let focusedButtonIndex = this.cursor; let focusedButtonIndex = this.cursor;
/** /**

@ -0,0 +1 @@
Subproject commit 72a7c01b6589fbe5591f63d0501c898a47d22875

View file

@ -1,5 +1,5 @@
import DomIterator from './domIterator'; import DomIterator from './domIterator';
import _ from './utils'; import * as _ from './utils';
/** /**
* Flipper construction options * Flipper construction options
@ -96,9 +96,11 @@ export default class Flipper {
this.handleTabPress(event); this.handleTabPress(event);
break; break;
case _.keyCodes.LEFT: case _.keyCodes.LEFT:
case _.keyCodes.UP:
this.flipLeft(); this.flipLeft();
break; break;
case _.keyCodes.RIGHT: case _.keyCodes.RIGHT:
case _.keyCodes.DOWN:
this.flipRight(); this.flipRight();
break; break;
case _.keyCodes.ENTER: case _.keyCodes.ENTER:
@ -120,6 +122,8 @@ export default class Flipper {
_.keyCodes.LEFT, _.keyCodes.LEFT,
_.keyCodes.RIGHT, _.keyCodes.RIGHT,
_.keyCodes.ENTER, _.keyCodes.ENTER,
_.keyCodes.UP,
_.keyCodes.DOWN,
]; ];
} }
@ -183,6 +187,8 @@ export default class Flipper {
handlingKeyCodeList.push( handlingKeyCodeList.push(
_.keyCodes.LEFT, _.keyCodes.LEFT,
_.keyCodes.RIGHT, _.keyCodes.RIGHT,
_.keyCodes.UP,
_.keyCodes.DOWN,
); );
} }

View file

@ -17,6 +17,11 @@ export default class BoldInlineTool implements InlineTool {
*/ */
public static isInline = true; public static isInline = true;
/**
* Title for hover-tooltip
*/
public static title: string = 'Bold';
/** /**
* Sanitizer Rule * Sanitizer Rule
* Leave <b> tags * Leave <b> tags

View file

@ -17,6 +17,11 @@ export default class ItalicInlineTool implements InlineTool {
*/ */
public static isInline = true; public static isInline = true;
/**
* Title for hover-tooltip
*/
public static title: string = 'Italic';
/** /**
* Sanitizer Rule * Sanitizer Rule
* Leave <i> tags * Leave <i> tags
@ -56,7 +61,7 @@ export default class ItalicInlineTool implements InlineTool {
this.nodes.button = document.createElement('button') as HTMLButtonElement; this.nodes.button = document.createElement('button') as HTMLButtonElement;
this.nodes.button.type = 'button'; this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('italic', 34, 34)); this.nodes.button.appendChild($.svg('italic', 4, 11));
return this.nodes.button; return this.nodes.button;
} }

View file

@ -1,7 +1,7 @@
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
import $ from '../dom'; import $ from '../dom';
import _ from '../utils'; import * as _ from '../utils';
import {API, InlineTool, SanitizerConfig} from '../../../types'; import {API, InlineTool, SanitizerConfig} from '../../../types';
import {Notifier, Toolbar} from '../../../types/api'; import {Notifier, Toolbar} from '../../../types/api';
@ -21,6 +21,11 @@ export default class LinkInlineTool implements InlineTool {
*/ */
public static isInline = true; public static isInline = true;
/**
* Title for hover-tooltip
*/
public static title: string = 'Link';
/** /**
* Sanitizer Rule * Sanitizer Rule
* Leave <a> tags * Leave <a> tags
@ -112,8 +117,8 @@ export default class LinkInlineTool implements InlineTool {
this.nodes.button = document.createElement('button') as HTMLButtonElement; this.nodes.button = document.createElement('button') as HTMLButtonElement;
this.nodes.button.type = 'button'; this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('link', 34, 34)); this.nodes.button.appendChild($.svg('link', 14, 10));
this.nodes.button.appendChild($.svg('unlink', 16, 18)); this.nodes.button.appendChild($.svg('unlink', 15, 11));
return this.nodes.button; return this.nodes.button;
} }

View file

@ -2,7 +2,7 @@ import Module from '../../__module';
import {Blocks} from '../../../../types/api'; import {Blocks} from '../../../../types/api';
import {BlockToolData, OutputData, ToolConfig} from '../../../../types'; import {BlockToolData, OutputData, ToolConfig} from '../../../../types';
import _ from './../../utils'; import * as _ from './../../utils';
/** /**
* @class BlocksAPI * @class BlocksAPI
@ -87,13 +87,9 @@ export default class BlocksAPI extends Module {
} }
/** /**
* In case of deletion first block we need to set caret to the current Block * After Block deletion currentBlock is updated
*/ */
if (this.Editor.BlockManager.currentBlockIndex === 0) { this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock, this.Editor.Caret.positions.END);
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
} else {
this.Editor.Caret.navigatePrevious(true);
}
this.Editor.Toolbar.close(); this.Editor.Toolbar.close();
} }

View file

@ -25,6 +25,7 @@ export default class API extends Module {
styles: this.Editor.StylesAPI.classes, styles: this.Editor.StylesAPI.classes,
toolbar: this.Editor.ToolbarAPI.methods, toolbar: this.Editor.ToolbarAPI.methods,
inlineToolbar: this.Editor.InlineToolbarAPI.methods, inlineToolbar: this.Editor.InlineToolbarAPI.methods,
tooltip: this.Editor.TooltipAPI.methods,
} as APIInterfaces; } as APIInterfaces;
} }
} }

View file

@ -0,0 +1,55 @@
import Module from '../../__module';
import { Tooltip } from '../../../../types/api';
import {TooltipContent, TooltipOptions} from '../../external/codex.tooltips';
/**
* @class TooltipAPI
* @classdesc Tooltip API
*/
export default class TooltipAPI extends Module {
/**
* Available methods
*/
get methods(): Tooltip {
return {
show: (element: HTMLElement,
content: TooltipContent,
options?: TooltipOptions,
) => this.show(element, content, options),
hide: () => this.hide(),
onHover: (element: HTMLElement,
content: TooltipContent,
options?: TooltipOptions,
) => this.onHover(element, content, options),
};
}
/**
* Method show tooltip on element with passed HTML content
*
* @param {HTMLElement} element
* @param {TooltipContent} content
* @param {TooltipOptions} options
*/
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions) {
this.Editor.Tooltip.show(element, content, options);
}
/**
* Method hides tooltip on HTML page
*/
public hide() {
this.Editor.Tooltip.hide();
}
/**
* Decorator for showing Tooltip by mouseenter/mouseleave
*
* @param {HTMLElement} element
* @param {TooltipContent} content
* @param {TooltipOptions} options
*/
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions) {
this.Editor.Tooltip.onHover(element, content, options);
}
}

View file

@ -2,9 +2,9 @@
* Contains keyboard and mouse events binded on each Block by Block Manager * Contains keyboard and mouse events binded on each Block by Block Manager
*/ */
import Module from '../__module'; import Module from '../__module';
import _ from '../utils'; import * as _ from '../utils';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
import Flipper from "../flipper"; import Flipper from '../flipper';
export default class BlockEvents extends Module { export default class BlockEvents extends Module {
@ -102,63 +102,16 @@ export default class BlockEvents extends Module {
return; return;
} }
const { InlineToolbar, ConversionToolbar, UI, BlockManager, BlockSettings } = this.Editor;
const block = BlockManager.getBlock(event.target);
/**
* Conversion Toolbar will be opened when user selects 85% of plugins content
* that why we must with the length of pluginsContent
*/
if (SelectionUtils.almostAllSelected(block.pluginsContent.textContent)) {
InlineToolbar.close();
BlockSettings.close();
ConversionToolbar.tryToShow(block);
} else {
ConversionToolbar.close();
InlineToolbar.tryToShow(true);
}
/** /**
* Check if editor is empty on each keyup and add special css class to wrapper * Check if editor is empty on each keyup and add special css class to wrapper
*/ */
UI.checkEmptiness(); this.Editor.UI.checkEmptiness();
} }
/** /**
* Mouse up on Block: * Mouse up on Block:
* - shows Inline Toolbar if something selected
*/ */
public mouseUp(event): void { public mouseUp(): void {
const { InlineToolbar, ConversionToolbar, BlockManager, BlockSelection } = this.Editor;
const block = BlockManager.getBlock(event.target);
/**
* Timeout uses to wait if selection will cleared after mouse up (regular click on block)
*/
_.delay(() => {
/**
* 1) selected 85% of block - open Conversion Toolbar
* 2) select something inside block - open Inline Toolbar
* 3) nothing selected - close Toolbars
*/
if (SelectionUtils.almostAllSelected(block.pluginsContent.textContent)) {
InlineToolbar.close();
ConversionToolbar.tryToShow(block);
} else if (!SelectionUtils.isCollapsed) {
InlineToolbar.tryToShow();
ConversionToolbar.close();
} else {
InlineToolbar.close();
/**
* Don't close Conversion toolbar when Rectangle Selection ended with one block selected
* @see RectangleSelection#endSelection
*/
if (BlockSelection.selectedBlocks.length !== 1) {
ConversionToolbar.close();
}
}
}, 30)();
} }
/** /**
@ -223,10 +176,10 @@ export default class BlockEvents extends Module {
this.Editor.Toolbox.close(); this.Editor.Toolbox.close();
} else if (this.Editor.BlockSettings.opened) { } else if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close(); this.Editor.BlockSettings.close();
} else if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.close();
} else if (this.Editor.ConversionToolbar.opened) { } else if (this.Editor.ConversionToolbar.opened) {
this.Editor.ConversionToolbar.close(); this.Editor.ConversionToolbar.close();
} else if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.close();
} else { } else {
this.Editor.Toolbar.close(); this.Editor.Toolbar.close();
} }
@ -324,8 +277,9 @@ export default class BlockEvents extends Module {
/** /**
* Opened Toolbars uses Flipper with own Enter handling * Opened Toolbars uses Flipper with own Enter handling
* Allow split block when no one button in Flipper is focused
*/ */
if (UI.someToolbarOpened) { if (UI.someToolbarOpened && UI.someFlipperButtonFocused) {
return; return;
} }
@ -420,7 +374,10 @@ export default class BlockEvents extends Module {
} }
const isFirstBlock = BlockManager.currentBlockIndex === 0; const isFirstBlock = BlockManager.currentBlockIndex === 0;
const canMergeBlocks = Caret.isAtStart && currentBlock.currentInput === currentBlock.firstInput && !isFirstBlock; const canMergeBlocks = Caret.isAtStart &&
SelectionUtils.isCollapsed &&
currentBlock.currentInput === currentBlock.firstInput &&
!isFirstBlock;
if (canMergeBlocks) { if (canMergeBlocks) {
/** /**
@ -481,11 +438,14 @@ export default class BlockEvents extends Module {
* Handle right and down keyboard keys * Handle right and down keyboard keys
*/ */
private arrowRightAndDown(event: KeyboardEvent): void { private arrowRightAndDown(event: KeyboardEvent): void {
const isFlipperCombination = Flipper.usedKeys.includes(event.keyCode) &&
(!event.shiftKey || event.keyCode === _.keyCodes.TAB);
/** /**
* Arrows might be handled on toolbars by flipper * Arrows might be handled on toolbars by flipper
* Check for Flipper.usedKeys to allow navigate by DOWN and disallow by RIGHT * Check for Flipper.usedKeys to allow navigate by DOWN and disallow by RIGHT
*/ */
if (this.Editor.UI.someToolbarOpened && Flipper.usedKeys.includes(event.keyCode)) { if (this.Editor.UI.someToolbarOpened && isFlipperCombination) {
return; return;
} }
@ -533,8 +493,12 @@ export default class BlockEvents extends Module {
* Arrows might be handled on toolbars by flipper * Arrows might be handled on toolbars by flipper
* Check for Flipper.usedKeys to allow navigate by UP and disallow by LEFT * Check for Flipper.usedKeys to allow navigate by UP and disallow by LEFT
*/ */
if (this.Editor.UI.someToolbarOpened && Flipper.usedKeys.includes(event.keyCode)) { if (this.Editor.UI.someToolbarOpened) {
return; if (Flipper.usedKeys.includes(event.keyCode) && (!event.shiftKey || event.keyCode === _.keyCodes.TAB)) {
return;
}
this.Editor.UI.closeAllToolbars();
} }
/** /**

View file

@ -6,10 +6,10 @@
* *
* @version 2.0.0 * @version 2.0.0
*/ */
import Block from '../block'; import Block, {BlockToolAPI} from '../block';
import Module from '../__module'; import Module from '../__module';
import $ from '../dom'; import $ from '../dom';
import _ from '../utils'; import * as _ from '../utils';
import Blocks from '../blocks'; import Blocks from '../blocks';
import {BlockTool, BlockToolConstructable, BlockToolData, PasteEvent, ToolConfig} from '../../../types'; import {BlockTool, BlockToolConstructable, BlockToolData, PasteEvent, ToolConfig} from '../../../types';
@ -267,7 +267,7 @@ export default class BlockManager extends Module {
} }
try { try {
block.call('onPaste', pasteEvent); block.call(BlockToolAPI.ON_PASTE, pasteEvent);
} catch (e) { } catch (e) {
_.log(`${toolName}: onPaste callback call is failed`, 'error', e); _.log(`${toolName}: onPaste callback call is failed`, 'error', e);
} }
@ -598,7 +598,7 @@ export default class BlockManager extends Module {
const {BlockEvents, Listeners} = this.Editor; const {BlockEvents, Listeners} = this.Editor;
Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true); Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true);
Listeners.on(block.holder, 'mouseup', (event) => BlockEvents.mouseUp(event)); Listeners.on(block.holder, 'mouseup', (event) => BlockEvents.mouseUp());
Listeners.on(block.holder, 'mousedown', (event: MouseEvent) => BlockEvents.mouseDown(event)); Listeners.on(block.holder, 'mousedown', (event: MouseEvent) => BlockEvents.mouseDown(event));
Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event)); Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event));
Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent)); Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));

View file

@ -7,7 +7,7 @@
*/ */
import Module from '../__module'; import Module from '../__module';
import Block from '../block'; import Block from '../block';
import _ from '../utils'; import * as _ from '../utils';
import $ from '../dom'; import $ from '../dom';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
@ -251,6 +251,9 @@ export default class BlockSelection extends Module {
.removeAllRanges(); .removeAllRanges();
block.selected = true; block.selected = true;
/** close InlineToolbar when we selected any Block */
this.Editor.InlineToolbar.close();
} }
/** /**
@ -304,6 +307,14 @@ export default class BlockSelection extends Module {
*/ */
this.Editor.ConversionToolbar.close(); this.Editor.ConversionToolbar.close();
} else if (this.readyToBlockSelection) { } else if (this.readyToBlockSelection) {
/**
* prevent default selection when we use custom selection
*/
event.preventDefault();
/**
* select working Block
*/
this.selectBlockByIndex(); this.selectBlockByIndex();
/** /**
@ -331,5 +342,8 @@ export default class BlockSelection extends Module {
.removeAllRanges(); .removeAllRanges();
this.allBlocksSelected = true; this.allBlocksSelected = true;
/** close InlineToolbar if we selected all Blocks */
this.Editor.InlineToolbar.close();
} }
} }

View file

@ -13,7 +13,7 @@ import Selection from '../selection';
import Module from '../__module'; import Module from '../__module';
import Block from '../block'; import Block from '../block';
import $ from '../dom'; import $ from '../dom';
import _ from '../utils'; import * as _ from '../utils';
/** /**
* @typedef {Caret} Caret * @typedef {Caret} Caret

View file

@ -1,7 +1,7 @@
import Module from '../__module'; import Module from '../__module';
import Block from '../block'; import Block from '../block';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
import _ from '../utils'; import * as _ from '../utils';
export default class CrossBlockSelection extends Module { export default class CrossBlockSelection extends Module {
/** /**
@ -33,6 +33,14 @@ export default class CrossBlockSelection extends Module {
Listeners.on(document, 'mouseup', this.onMouseUp); Listeners.on(document, 'mouseup', this.onMouseUp);
} }
/**
* return boolean is cross block selection started
*/
public get isCrossBlockSelectionStarted(): boolean {
return !!this.firstSelectedBlock
&& !!this.lastSelectedBlock;
}
/** /**
* Change selection state of the next Block * Change selection state of the next Block
* Used for CBS via Shift + arrow keys * Used for CBS via Shift + arrow keys
@ -65,6 +73,9 @@ export default class CrossBlockSelection extends Module {
} }
this.lastSelectedBlock = nextBlock; this.lastSelectedBlock = nextBlock;
/** close InlineToolbar when Blocks selected */
this.Editor.InlineToolbar.close();
} }
/** /**
@ -151,6 +162,8 @@ export default class CrossBlockSelection extends Module {
return; return;
} }
this.Editor.InlineToolbar.close();
this.toggleBlocksSelectedState(relatedBlock, targetBlock); this.toggleBlocksSelectedState(relatedBlock, targetBlock);
this.lastSelectedBlock = targetBlock; this.lastSelectedBlock = targetBlock;
} }

View file

@ -36,6 +36,33 @@ export default class Events extends Module {
this.subscribers[eventName].push(callback); this.subscribers[eventName].push(callback);
} }
/**
* Subscribe any event on callback. Callback will be called once and be removed from subscribers array after call.
*
* @param {String} eventName - event name
* @param {Function} callback - subscriber
*/
public once(eventName: string, callback: (data: any) => any) {
if (!(eventName in this.subscribers)) {
this.subscribers[eventName] = [];
}
const wrappedCallback = (data: any) => {
const result = callback(data);
const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback);
if (indexOfHandler !== -1) {
this.subscribers[eventName].splice(indexOfHandler, 1);
}
return result;
};
// group by events
this.subscribers[eventName].push(wrappedCallback);
}
/** /**
* Emit callbacks with passed data * Emit callbacks with passed data
* *

View file

@ -145,7 +145,7 @@ export default class Listeners extends Module {
*/ */
public removeAll(): void { public removeAll(): void {
this.allListeners.map( (current) => { this.allListeners.map( (current) => {
current.element.removeEventListener(current.eventType, current.handler); current.element.removeEventListener(current.eventType, current.handler, current.options);
}); });
this.allListeners = []; this.allListeners = [];

View file

@ -6,7 +6,7 @@
*/ */
import Module from '../__module'; import Module from '../__module';
import _ from '../utils'; import * as _ from '../utils';
import Block from '../block'; import Block from '../block';
export default class ModificationsObserver extends Module { export default class ModificationsObserver extends Module {

View file

@ -1,6 +1,6 @@
import Module from '../__module'; import Module from '../__module';
import $ from '../dom'; import $ from '../dom';
import _ from '../utils'; import * as _ from '../utils';
import { import {
BlockTool, BlockTool,
BlockToolConstructable, BlockToolConstructable,

View file

@ -166,15 +166,6 @@ export default class RectangleSelection extends Module {
this.startX = 0; this.startX = 0;
this.startY = 0; this.startY = 0;
this.overlayRectangle.style.display = 'none'; this.overlayRectangle.style.display = 'none';
/**
* Show Conversion Toolbar when user select one Block
*/
const { selectedBlocks } = this.Editor.BlockSelection;
if (selectedBlocks.length === 1) {
this.Editor.ConversionToolbar.tryToShow(selectedBlocks[0]);
}
} }
/** /**

View file

@ -1,5 +1,6 @@
import Module from '../__module'; import Module from '../__module';
import _, {ChainData} from '../utils'; import * as _ from '../utils';
import {ChainData} from '../utils';
import {BlockToolData} from '../../../types'; import {BlockToolData} from '../../../types';
import {BlockToolConstructable} from '../../../types/tools'; import {BlockToolConstructable} from '../../../types/tools';

View file

@ -17,7 +17,7 @@
*/ */
import Module from '../__module'; import Module from '../__module';
import _ from '../utils'; import * as _ from '../utils';
/** /**
* @typedef {Object} SanitizerConfig * @typedef {Object} SanitizerConfig

View file

@ -9,6 +9,7 @@ import Module from '../__module';
import {OutputData} from '../../../types'; import {OutputData} from '../../../types';
import {ValidatedData} from '../../types-internal/block-data'; import {ValidatedData} from '../../types-internal/block-data';
import Block from '../block'; import Block from '../block';
import * as _ from '../utils';
declare const VERSION: string; declare const VERSION: string;
@ -67,7 +68,7 @@ export default class Saver extends Module {
let totalTime = 0; let totalTime = 0;
const blocks = []; const blocks = [];
console.groupCollapsed('[Editor.js saving]:'); _.log('[Editor.js saving]:', 'groupCollapsed');
allExtractedData.forEach(({tool, data, time, isValid}) => { allExtractedData.forEach(({tool, data, time, isValid}) => {
totalTime += time; totalTime += time;
@ -75,15 +76,15 @@ export default class Saver extends Module {
/** /**
* Capitalize Tool name * Capitalize Tool name
*/ */
console.group(`${tool.charAt(0).toUpperCase() + tool.slice(1)}`); _.log(`${tool.charAt(0).toUpperCase() + tool.slice(1)}`, 'group');
if (isValid) { if (isValid) {
/** Group process info */ /** Group process info */
console.log(data); _.log(data);
console.groupEnd(); _.log(undefined, 'groupEnd');
} else { } else {
console.log(`Block «${tool}» skipped because saved data is invalid`); _.log(`Block «${tool}» skipped because saved data is invalid`);
console.groupEnd(); _.log(undefined, 'groupEnd');
return; return;
} }
@ -99,8 +100,8 @@ export default class Saver extends Module {
}); });
}); });
console.log('Total', totalTime); _.log('Total', 'log', totalTime);
console.groupEnd(); _.log(undefined, 'groupEnd');
return { return {
time: +new Date(), time: +new Date(),

View file

@ -1,7 +1,7 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import Flipper, {FlipperOptions} from '../../flipper'; import Flipper, {FlipperOptions} from '../../flipper';
import _ from '../../utils'; import * as _ from '../../utils';
/** /**
* Block Settings * Block Settings

View file

@ -1,7 +1,7 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import {BlockToolConstructable} from '../../../../types'; import {BlockToolConstructable} from '../../../../types';
import _ from '../../utils'; import * as _ from '../../utils';
import {SavedData} from '../../../types-internal/block-data'; import {SavedData} from '../../../types-internal/block-data';
import Block from '../../block'; import Block from '../../block';
import Flipper from '../../flipper'; import Flipper from '../../flipper';
@ -18,7 +18,10 @@ export default class ConversionToolbar extends Module {
conversionToolbarWrapper: 'ce-conversion-toolbar', conversionToolbarWrapper: 'ce-conversion-toolbar',
conversionToolbarShowed: 'ce-conversion-toolbar--showed', conversionToolbarShowed: 'ce-conversion-toolbar--showed',
conversionToolbarTools: 'ce-conversion-toolbar__tools', conversionToolbarTools: 'ce-conversion-toolbar__tools',
conversionToolbarLabel: 'ce-conversion-toolbar__label',
conversionTool: 'ce-conversion-tool', conversionTool: 'ce-conversion-tool',
conversionToolHidden: 'ce-conversion-tool--hidden',
conversionToolIcon: 'ce-conversion-tool__icon',
conversionToolFocused : 'ce-conversion-tool--focused', conversionToolFocused : 'ce-conversion-tool--focused',
conversionToolActive : 'ce-conversion-tool--active', conversionToolActive : 'ce-conversion-tool--active',
@ -50,13 +53,22 @@ export default class ConversionToolbar extends Module {
*/ */
private flipper: Flipper = null; private flipper: Flipper = null;
/**
* Callback that fill be fired on open/close and accepts an opening state
*/
private togglingCallback = null;
/** /**
* Create UI of Conversion Toolbar * Create UI of Conversion Toolbar
*/ */
public make(): void { public make(): HTMLElement {
this.nodes.wrapper = $.make('div', ConversionToolbar.CSS.conversionToolbarWrapper); this.nodes.wrapper = $.make('div', ConversionToolbar.CSS.conversionToolbarWrapper);
this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools); this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);
const label = $.make('div', ConversionToolbar.CSS.conversionToolbarLabel, {
textContent: 'Convert to',
});
/** /**
* Add Tools that has 'import' method * Add Tools that has 'import' method
*/ */
@ -67,41 +79,53 @@ export default class ConversionToolbar extends Module {
*/ */
this.enableFlipper(); this.enableFlipper();
$.append(this.nodes.wrapper, label);
$.append(this.nodes.wrapper, this.nodes.tools); $.append(this.nodes.wrapper, this.nodes.tools);
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
return this.nodes.wrapper;
} }
/** /**
* Try to show Conversion Toolbar near passed Block * Toggle conversion dropdown visibility
* @param {Block} block - block to convert * @param {function} [togglingCallback] callback that will accept opening state
*/ */
public tryToShow(block: Block): void { public toggle(togglingCallback?: (openedState: boolean) => void): void {
const hasExportConfig = block.class.conversionConfig && block.class.conversionConfig.export;
if (!hasExportConfig) {
return;
}
this.move(block);
if (!this.opened) { if (!this.opened) {
this.open(); this.open();
} else {
this.close();
} }
/** if (typeof togglingCallback === 'function') {
* Mark current block's button with color this.togglingCallback = togglingCallback;
*/
this.highlightActiveTool(block.name); this.togglingCallback(this.opened);
}
} }
/** /**
* Shows Conversion Toolbar * Shows Conversion Toolbar
*/ */
public open(): void { public open(): void {
this.filterTools();
this.opened = true; this.opened = true;
this.flipper.activate(Object.values(this.tools));
this.flipper.focusFirst();
this.nodes.wrapper.classList.add(ConversionToolbar.CSS.conversionToolbarShowed); this.nodes.wrapper.classList.add(ConversionToolbar.CSS.conversionToolbarShowed);
/**
* We use timeout to prevent bubbling Enter keydown on first dropdown item
* Conversion flipper will be activated after dropdown will open
*/
setTimeout(() => {
this.flipper.activate(Object.values(this.tools).filter((button) => {
return !button.classList.contains(ConversionToolbar.CSS.conversionToolHidden);
}));
this.flipper.focusFirst();
if (typeof this.togglingCallback === 'function') {
this.togglingCallback(true);
}
}, 50);
} }
/** /**
@ -111,6 +135,10 @@ export default class ConversionToolbar extends Module {
this.opened = false; this.opened = false;
this.flipper.deactivate(); this.flipper.deactivate();
this.nodes.wrapper.classList.remove(ConversionToolbar.CSS.conversionToolbarShowed); this.nodes.wrapper.classList.remove(ConversionToolbar.CSS.conversionToolbarShowed);
if (typeof this.togglingCallback === 'function') {
this.togglingCallback(false);
}
} }
/** /**
@ -127,6 +155,7 @@ export default class ConversionToolbar extends Module {
const currentBlockClass = this.Editor.BlockManager.currentBlock.class; const currentBlockClass = this.Editor.BlockManager.currentBlock.class;
const currentBlockName = this.Editor.BlockManager.currentBlock.name; const currentBlockName = this.Editor.BlockManager.currentBlock.name;
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData; const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
const { INTERNAL_SETTINGS } = this.Editor.Tools;
const blockData = savedBlock.data; const blockData = savedBlock.data;
/** /**
@ -151,7 +180,7 @@ export default class ConversionToolbar extends Module {
* In both cases returning value must be a string * In both cases returning value must be a string
*/ */
let exportData: string = ''; let exportData: string = '';
const exportProp = currentBlockClass.conversionConfig.export; const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export;
if (typeof exportProp === 'function') { if (typeof exportProp === 'function') {
exportData = exportProp(blockData); exportData = exportProp(blockData);
@ -177,7 +206,7 @@ export default class ConversionToolbar extends Module {
* string the name of data field to import * string the name of data field to import
*/ */
let newBlockData = {}; let newBlockData = {};
const importProp = replacingTool.conversionConfig.import; const importProp = replacingTool[INTERNAL_SETTINGS.CONVERSION_CONFIG].import;
if (typeof importProp === 'function') { if (typeof importProp === 'function') {
newBlockData = importProp(cleaned); newBlockData = importProp(cleaned);
@ -193,28 +222,13 @@ export default class ConversionToolbar extends Module {
this.Editor.BlockSelection.clearSelection(); this.Editor.BlockSelection.clearSelection();
this.close(); this.close();
this.Editor.InlineToolbar.close();
_.delay(() => { _.delay(() => {
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock); this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
}, 10)(); }, 10)();
} }
/**
* Move Conversion Toolbar to the working Block
*/
private move(block: Block): void {
const blockRect = block.pluginsContent.getBoundingClientRect();
const wrapperRect = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
const newCoords = {
x: blockRect.left - wrapperRect.left,
y: blockRect.top + blockRect.height - wrapperRect.top,
};
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
}
/** /**
* Iterates existing Tools and inserts to the ConversionToolbar * Iterates existing Tools and inserts to the ConversionToolbar
* if tools have ability to import * if tools have ability to import
@ -246,18 +260,22 @@ export default class ConversionToolbar extends Module {
continue; continue;
} }
this.addTool(toolName, toolToolboxSettings.icon); this.addTool(toolName, toolToolboxSettings.icon, toolToolboxSettings.title);
} }
} }
/** /**
* Add tool to the Conversion Toolbar * Add tool to the Conversion Toolbar
*/ */
private addTool(toolName: string, toolIcon: string): void { private addTool(toolName: string, toolIcon: string, title: string): void {
const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]); const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);
const icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]);
tool.dataset.tool = toolName; tool.dataset.tool = toolName;
tool.innerHTML = toolIcon; icon.innerHTML = toolIcon;
$.append(tool, icon);
$.append(tool, $.text(title || _.capitalize(toolName)));
$.append(this.nodes.tools, tool); $.append(this.nodes.tools, tool);
this.tools[toolName] = tool; this.tools[toolName] = tool;
@ -268,21 +286,18 @@ export default class ConversionToolbar extends Module {
} }
/** /**
* Marks current Blocks button with highlighting color * Hide current Tool and show others
*/ */
private highlightActiveTool(toolName: string): void { private filterTools(): void {
if (!this.tools[toolName]) { const { currentBlock } = this.Editor.BlockManager;
return;
}
/** /**
* Drop previous active button * Show previously hided
*/ */
Object.values(this.tools).forEach((el) => { Object.entries(this.tools).forEach(([name, button]) => {
el.classList.remove(ConversionToolbar.CSS.conversionToolActive); button.hidden = false;
button.classList.toggle(ConversionToolbar.CSS.conversionToolHidden, name === currentBlock.name);
}); });
this.tools[toolName].classList.add(ConversionToolbar.CSS.conversionToolActive);
} }
/** /**

View file

@ -1,6 +1,6 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import _ from '../../utils'; import * as _ from '../../utils';
/** /**
* *
@ -85,6 +85,7 @@ export default class Toolbar extends Module {
// Content Zone // Content Zone
plusButton: 'ce-toolbar__plus', plusButton: 'ce-toolbar__plus',
plusButtonShortcut: 'ce-toolbar__plus-shortcut',
plusButtonHidden: 'ce-toolbar__plus--hidden', plusButtonHidden: 'ce-toolbar__plus--hidden',
// Actions Zone // Actions Zone
@ -113,33 +114,22 @@ export default class Toolbar extends Module {
* - Toolbox * - Toolbox
*/ */
this.nodes.plusButton = $.make('div', this.CSS.plusButton); this.nodes.plusButton = $.make('div', this.CSS.plusButton);
$.append(this.nodes.plusButton, $.svg('plus', 14, 14));
$.append(this.nodes.content, this.nodes.plusButton);
this.Editor.Listeners.on(this.nodes.plusButton, 'click', () => this.plusButtonClicked(), false);
/** /**
* Add events to show/hide tooltip for plus button * Add events to show/hide tooltip for plus button
*/ */
this.Editor.Listeners.on(this.nodes.plusButton, 'mouseenter', () => { const tooltipContent = $.make('div');
const tooltip = this.Editor.Toolbox.nodes.tooltip;
const fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode('Add')); tooltipContent.appendChild(document.createTextNode('Add'));
fragment.appendChild($.make('div', this.Editor.Toolbox.CSS.tooltipShortcut, { tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {
textContent: '⇥ Tab', textContent: '⇥ Tab',
})); }));
tooltip.style.left = '-17px'; this.Editor.Tooltip.onHover(this.nodes.plusButton, tooltipContent);
tooltip.innerHTML = '';
tooltip.appendChild(fragment);
tooltip.classList.add(this.Editor.Toolbox.CSS.tooltipShown);
});
this.Editor.Listeners.on(this.nodes.plusButton, 'mouseleave', () => {
this.Editor.Toolbox.hideTooltip();
});
$.append(this.nodes.plusButton, $.svg('plus', 14, 14));
$.append(this.nodes.content, this.nodes.plusButton);
this.Editor.Listeners.on(this.nodes.plusButton, 'click', () => this.plusButtonClicked(), false);
/** /**
* Make a Toolbox * Make a Toolbox
@ -160,6 +150,10 @@ export default class Toolbar extends Module {
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler); $.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons); $.append(this.nodes.actions, this.nodes.blockActionsButtons);
this.Editor.Tooltip.onHover(this.nodes.settingsToggler, 'Click to tune', {
placement: 'top',
});
/** /**
* Make and append Settings Panel * Make and append Settings Panel
*/ */

View file

@ -2,7 +2,7 @@ import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import SelectionUtils from '../../selection'; import SelectionUtils from '../../selection';
import _ from '../../utils'; import * as _ from '../../utils';
import {InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings} from '../../../../types'; import {InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings} from '../../../../types';
import Flipper from '../../flipper'; import Flipper from '../../flipper';
@ -23,12 +23,16 @@ export default class InlineToolbar extends Module {
inlineToolbarShowed: 'ce-inline-toolbar--showed', inlineToolbarShowed: 'ce-inline-toolbar--showed',
inlineToolbarLeftOriented: 'ce-inline-toolbar--left-oriented', inlineToolbarLeftOriented: 'ce-inline-toolbar--left-oriented',
inlineToolbarRightOriented: 'ce-inline-toolbar--right-oriented', inlineToolbarRightOriented: 'ce-inline-toolbar--right-oriented',
inlineToolbarShortcut: 'ce-inline-toolbar__shortcut',
buttonsWrapper: 'ce-inline-toolbar__buttons', buttonsWrapper: 'ce-inline-toolbar__buttons',
actionsWrapper: 'ce-inline-toolbar__actions', actionsWrapper: 'ce-inline-toolbar__actions',
inlineToolButton: 'ce-inline-tool', inlineToolButton: 'ce-inline-tool',
inlineToolButtonLast: 'ce-inline-tool--last', inlineToolButtonLast: 'ce-inline-tool--last',
inputField: 'cdx-input', inputField: 'cdx-input',
focusedButton: 'ce-inline-tool--focused', focusedButton: 'ce-inline-tool--focused',
conversionToggler: 'ce-inline-toolbar__dropdown',
conversionTogglerHidden: 'ce-inline-toolbar__dropdown--hidden',
conversionTogglerContent: 'ce-inline-toolbar__dropdown-content',
}; };
/** /**
@ -40,9 +44,17 @@ export default class InlineToolbar extends Module {
/** /**
* Inline Toolbar elements * Inline Toolbar elements
*/ */
private nodes: { wrapper: HTMLElement, buttons: HTMLElement, actions: HTMLElement } = { private nodes: {
wrapper: HTMLElement,
buttons: HTMLElement,
conversionToggler: HTMLElement,
conversionTogglerContent: HTMLElement,
actions: HTMLElement,
} = {
wrapper: null, wrapper: null,
buttons: null, buttons: null,
conversionToggler: null,
conversionTogglerContent: null,
/** /**
* Zone below the buttons where Tools can create additional actions by 'renderActions()' method * Zone below the buttons where Tools can create additional actions by 'renderActions()' method
* For example, input for the 'link' tool or textarea for the 'comment' tool * For example, input for the 'link' tool or textarea for the 'comment' tool
@ -53,7 +65,7 @@ export default class InlineToolbar extends Module {
/** /**
* Margin above/below the Toolbar * Margin above/below the Toolbar
*/ */
private readonly toolbarVerticalMargin: number = 20; private readonly toolbarVerticalMargin: number = 5;
/** /**
* Tools instances * Tools instances
@ -122,11 +134,18 @@ export default class InlineToolbar extends Module {
$.append(this.nodes.wrapper, [this.nodes.buttons, this.nodes.actions]); $.append(this.nodes.wrapper, [this.nodes.buttons, this.nodes.actions]);
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper); $.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
/**
* Add button that will allow switching block type
*/
this.addConversionToggler();
/** /**
* Append Inline Toolbar Tools * Append Inline Toolbar Tools
*/ */
this.addTools(); this.addTools();
this.prepareConversionToolbar();
/** /**
* Recalculate initial width with all buttons * Recalculate initial width with all buttons
*/ */
@ -163,9 +182,6 @@ export default class InlineToolbar extends Module {
/** Check Tools state for selected fragment */ /** Check Tools state for selected fragment */
this.checkToolsState(); this.checkToolsState();
/** Clear selection */
this.Editor.BlockSelection.clearSelection();
} }
/** /**
@ -228,6 +244,7 @@ export default class InlineToolbar extends Module {
this.opened = false; this.opened = false;
this.flipper.deactivate(); this.flipper.deactivate();
this.Editor.ConversionToolbar.close();
} }
/** /**
@ -256,13 +273,20 @@ export default class InlineToolbar extends Module {
this.buttonsList = this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`); this.buttonsList = this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`);
this.opened = true; this.opened = true;
/**
* Change Conversion Dropdown content for current tool
*/
this.setConversionTogglerContent();
/** /**
* Get currently visible buttons to pass it to the Flipper * Get currently visible buttons to pass it to the Flipper
*/ */
const visibleTools = Array.from(this.buttonsList) let visibleTools = Array.from(this.buttonsList);
.filter((tool) => !(tool as HTMLElement).hidden) as HTMLElement[];
this.flipper.activate(visibleTools); visibleTools.unshift(this.nodes.conversionToggler);
visibleTools = visibleTools.filter((tool) => !(tool as HTMLElement).hidden);
this.flipper.activate(visibleTools as HTMLElement[]);
} }
/** /**
@ -370,6 +394,77 @@ export default class InlineToolbar extends Module {
this.width = this.nodes.wrapper.offsetWidth; this.width = this.nodes.wrapper.offsetWidth;
} }
/**
* Create a toggler for Conversion Dropdown
* and prepend it to the buttons list
*/
private addConversionToggler(): void {
this.nodes.conversionToggler = $.make('div', this.CSS.conversionToggler);
this.nodes.conversionTogglerContent = $.make('div', this.CSS.conversionTogglerContent);
const icon = $.svg('toggler-down', 13, 13);
this.nodes.conversionToggler.appendChild(this.nodes.conversionTogglerContent);
this.nodes.conversionToggler.appendChild(icon);
this.nodes.buttons.appendChild(this.nodes.conversionToggler);
this.Editor.Listeners.on(this.nodes.conversionToggler, 'click', () => {
this.Editor.ConversionToolbar.toggle((conversionToolbarOpened) => {
if (conversionToolbarOpened) {
this.flipper.deactivate();
} else {
this.flipper.activate();
}
});
});
this.Editor.Tooltip.onHover(this.nodes.conversionToggler, 'Convert to', {
placement: 'top',
hidingDelay: 100,
});
}
/**
* Changes Conversion Dropdown content for current block's Tool
*/
private setConversionTogglerContent(): void {
const {BlockManager, Tools} = this.Editor;
const toolName = BlockManager.currentBlock.name;
/**
* If tool does not provide 'export' rule, hide conversion dropdown
*/
const conversionConfig = Tools.available[toolName][Tools.INTERNAL_SETTINGS.CONVERSION_CONFIG] || {};
const exportRuleDefined = conversionConfig && conversionConfig.export;
this.nodes.conversionToggler.hidden = !exportRuleDefined;
this.nodes.conversionToggler.classList.toggle(this.CSS.conversionTogglerHidden, !exportRuleDefined);
/**
* Get icon or title for dropdown
*/
const toolSettings = Tools.getToolSettings(toolName);
const toolboxSettings = Tools.available[toolName][Tools.INTERNAL_SETTINGS.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {};
this.nodes.conversionTogglerContent.innerHTML =
userToolboxSettings.icon
|| toolboxSettings.icon
|| userToolboxSettings.title
|| toolboxSettings.title
|| _.capitalize(toolName);
}
/**
* Makes the Conversion Dropdown
*/
private prepareConversionToolbar(): void {
const ct = this.Editor.ConversionToolbar.make();
$.append(this.nodes.wrapper, ct);
}
/** /**
* Working with Tools * Working with Tools
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -391,6 +486,7 @@ export default class InlineToolbar extends Module {
const { const {
Listeners, Listeners,
Tools, Tools,
Tooltip,
} = this.Editor; } = this.Editor;
const button = tool.render(); const button = tool.render();
@ -448,6 +544,26 @@ export default class InlineToolbar extends Module {
if (shortcut) { if (shortcut) {
this.enableShortcuts(tool, shortcut); this.enableShortcuts(tool, shortcut);
} }
/**
* Enable tooltip module on button
*/
const tooltipContent = $.make('div');
const toolTitle = Tools.toolsClasses[toolName][Tools.INTERNAL_SETTINGS.TITLE] || _.capitalize(toolName);
tooltipContent.appendChild($.text(toolTitle));
if (shortcut) {
tooltipContent.appendChild($.make('div', this.CSS.inlineToolbarShortcut, {
textContent: _.beautifyShortcut(shortcut),
}));
}
Tooltip.onHover(button, tooltipContent, {
placement: 'top',
hidingDelay: 100,
});
} }
/** /**

View file

@ -1,8 +1,9 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import _ from '../../utils'; import * as _ from '../../utils';
import {BlockToolConstructable} from '../../../../types'; import {BlockToolConstructable} from '../../../../types';
import Flipper from '../../flipper'; import Flipper from '../../flipper';
import {BlockToolAPI} from '../../block';
/** /**
* @class Toolbox * @class Toolbox
@ -27,10 +28,10 @@ export default class Toolbox extends Module {
toolboxButton: 'ce-toolbox__button', toolboxButton: 'ce-toolbox__button',
toolboxButtonActive : 'ce-toolbox__button--active', toolboxButtonActive : 'ce-toolbox__button--active',
toolboxOpened: 'ce-toolbox--opened', toolboxOpened: 'ce-toolbox--opened',
tooltip: 'ce-toolbox__tooltip',
tooltipShown: 'ce-toolbox__tooltip--shown',
tooltipShortcut: 'ce-toolbox__tooltip-shortcut',
openedToolbarHolderModifier: 'codex-editor--toolbox-opened', openedToolbarHolderModifier: 'codex-editor--toolbox-opened',
buttonTooltip: 'ce-toolbox-button-tooltip',
buttonShortcut: 'ce-toolbox-button-tooltip__shortcut',
}; };
} }
@ -53,11 +54,9 @@ export default class Toolbox extends Module {
*/ */
public nodes: { public nodes: {
toolbox: HTMLElement, toolbox: HTMLElement,
tooltip: HTMLElement,
buttons: HTMLElement[], buttons: HTMLElement[],
} = { } = {
toolbox: null, toolbox: null,
tooltip: null,
buttons: [], buttons: [],
}; };
@ -81,7 +80,6 @@ export default class Toolbox extends Module {
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox); $.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
this.addTools(); this.addTools();
this.addTooltip();
this.enableFlipper(); this.enableFlipper();
} }
@ -116,8 +114,6 @@ export default class Toolbox extends Module {
* Close Toolbox * Close Toolbox
*/ */
public close(): void { public close(): void {
this.hideTooltip();
this.nodes.toolbox.classList.remove(this.CSS.toolboxOpened); this.nodes.toolbox.classList.remove(this.CSS.toolboxOpened);
this.Editor.UI.nodes.wrapper.classList.remove(this.CSS.openedToolbarHolderModifier); this.Editor.UI.nodes.wrapper.classList.remove(this.CSS.openedToolbarHolderModifier);
@ -136,13 +132,6 @@ export default class Toolbox extends Module {
} }
} }
/**
* Hide toolbox tooltip
*/
public hideTooltip(): void {
this.nodes.tooltip.classList.remove(this.CSS.tooltipShown);
}
/** /**
* Iterates available tools and appends them to the Toolbox * Iterates available tools and appends them to the Toolbox
*/ */
@ -210,12 +199,11 @@ export default class Toolbox extends Module {
/** /**
* Add listeners to show/hide toolbox tooltip * Add listeners to show/hide toolbox tooltip
*/ */
this.Editor.Listeners.on(button, 'mouseenter', () => { const tooltipContent = this.drawTooltip(toolName);
this.showTooltip(button, toolName);
});
this.Editor.Listeners.on(button, 'mouseleave', () => { this.Editor.Tooltip.onHover(button, tooltipContent, {
this.hideTooltip(); placement: 'bottom',
hidingDelay: 200,
}); });
/** /**
@ -232,22 +220,12 @@ export default class Toolbox extends Module {
} }
/** /**
* Add toolbox tooltip to page * Draw tooltip for toolbox tools
*
* @param {String} toolName - toolbox tool name
* @return { HTMLElement }
*/ */
private addTooltip(): void { private drawTooltip(toolName: string): HTMLElement {
this.nodes.tooltip = $.make('div', this.CSS.tooltip, {
innerHTML: '',
});
$.append(this.Editor.Toolbar.nodes.content, this.nodes.tooltip);
}
/**
* Show tooltip for toolbox button
* @param {HTMLElement} button
* @param {string} toolName
*/
private showTooltip(button: HTMLElement, toolName: string): void {
const toolSettings = this.Editor.Tools.getToolSettings(toolName); const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {}; const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {}; const userToolboxSettings = toolSettings.toolbox || {};
@ -255,48 +233,20 @@ export default class Toolbox extends Module {
let shortcut = toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT]; let shortcut = toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT];
const fragment = document.createDocumentFragment(); const tooltip = $.make('div', this.CSS.buttonTooltip);
const hint = document.createTextNode(_.capitalize(name)); const hint = document.createTextNode(_.capitalize(name));
fragment.appendChild(hint); tooltip.appendChild(hint);
if (shortcut) { if (shortcut) {
const OS = _.getUserOS(); shortcut = _.beautifyShortcut(shortcut);
shortcut = shortcut tooltip.appendChild($.make('div', this.CSS.buttonShortcut, {
.replace(/shift/gi, '⇧')
.replace(/backspace/gi, '⌫')
.replace(/enter/gi, '⏎')
.replace(/up/gi, '↑')
.replace(/left/gi, '→')
.replace(/down/gi, '↓')
.replace(/right/gi, '←')
.replace(/escape/gi, '⎋')
.replace(/insert/gi, 'Ins')
.replace(/delete/gi, '␡')
.replace(/\+/gi, ' + ');
if (OS.mac) {
shortcut = shortcut.replace(/ctrl|cmd/gi, '⌘').replace(/alt/gi, '⌥');
} else {
shortcut = shortcut.replace(/cmd/gi, 'Ctrl').replace(/windows/gi, 'WIN');
}
fragment.appendChild($.make('div', this.CSS.tooltipShortcut, {
textContent: shortcut, textContent: shortcut,
})); }));
} }
const leftOffset = 16; return tooltip;
const coordinate = button.offsetLeft;
const topOffset = Math.floor(this.Editor.BlockManager.currentBlock.holder.offsetHeight / 2);
this.nodes.tooltip.innerHTML = '';
this.nodes.tooltip.appendChild(fragment);
this.nodes.tooltip.style.left = `${coordinate + leftOffset}px`;
this.nodes.tooltip.style.transform = `translate3d(-50%, ${topOffset}px, 0)`;
this.nodes.tooltip.classList.add(this.CSS.tooltipShown);
} }
/** /**
@ -351,7 +301,7 @@ export default class Toolbox extends Module {
/** /**
* Apply callback before inserting html * Apply callback before inserting html
*/ */
newBlock.call('appendCallback', {}); newBlock.call(BlockToolAPI.APPEND_CALLBACK);
this.Editor.Caret.setToBlock(newBlock); this.Editor.Caret.setToBlock(newBlock);

View file

@ -1,6 +1,6 @@
import Paragraph from '../tools/paragraph/dist/bundle'; import Paragraph from '../tools/paragraph/dist/bundle';
import Module from '../__module'; import Module from '../__module';
import _ from '../utils'; import * as _ from '../utils';
import { import {
BlockToolConstructable, BlockToolConstructable,
InlineTool, InlineTool,
@ -130,6 +130,7 @@ export default class Tools extends Module {
return { return {
IS_ENABLED_LINE_BREAKS: 'enableLineBreaks', IS_ENABLED_LINE_BREAKS: 'enableLineBreaks',
IS_INLINE: 'isInline', IS_INLINE: 'isInline',
TITLE: 'title', // for Inline Tools. Block Tools can pass title along with icon through the 'toolbox' static prop.
SHORTCUT: 'shortcut', SHORTCUT: 'shortcut',
TOOLBOX: 'toolbox', TOOLBOX: 'toolbox',
SANITIZE_CONFIG: 'sanitize', SANITIZE_CONFIG: 'sanitize',

View file

@ -0,0 +1,58 @@
import Module from '../__module';
/**
* Use external module CodeX Tooltip
*/
import CodeXTooltips, { TooltipContent, TooltipOptions } from '../external/codex.tooltips';
import {ModuleConfig} from '../../types-internal/module-config';
/**
* Tooltip
*
* Decorates any tooltip module like adapter
*/
export default class Tooltip extends Module {
/**
* Tooltips lib: CodeX Tooltips
* @see https://github.com/codex-team/codex.tooltips
*/
private lib: CodeXTooltips = new CodeXTooltips();
/**
* @constructor
* @param {EditorConfig}
*/
constructor({config}: ModuleConfig) {
super({config});
}
/**
* Shows tooltip on element with passed HTML content
*
* @param {HTMLElement} element - any HTML element in DOM
* @param {TooltipContent} content - tooltip's content
* @param {TooltipOptions} options - showing settings
*/
public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
this.lib.show(element, content, options);
}
/**
* Hides tooltip
*/
public hide(): void {
this.lib.hide();
}
/**
* Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip
*
* @param {HTMLElement} element - any HTML element in DOM
* @param {TooltipContent} content - tooltip's content
* @param {TooltipOptions} options - showing settings
*/
public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {
this.lib.onHover(element, content, options);
}
}

View file

@ -10,10 +10,11 @@ import sprite from '../../../dist/sprite.svg';
*/ */
import Module from '../__module'; import Module from '../__module';
import $ from '../dom'; import $ from '../dom';
import _ from '../utils'; import * as _ from '../utils';
import Selection from '../selection'; import Selection from '../selection';
import Block from '../block'; import Block from '../block';
import Flipper from '../flipper';
/** /**
* @class * @class
@ -161,11 +162,6 @@ export default class UI extends Module {
*/ */
await this.Editor.InlineToolbar.make(); await this.Editor.InlineToolbar.make();
/**
* Make the Converter tool holder
*/
await this.Editor.ConversionToolbar.make();
/** /**
* Load and append CSS * Load and append CSS
*/ */
@ -191,12 +187,23 @@ export default class UI extends Module {
* Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar * Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar
* @return {boolean} * @return {boolean}
*/ */
public get someToolbarOpened() { public get someToolbarOpened(): boolean {
const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor; const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
return BlockSettings.opened || InlineToolbar.opened || ConversionToolbar.opened || Toolbox.opened; return BlockSettings.opened || InlineToolbar.opened || ConversionToolbar.opened || Toolbox.opened;
} }
/**
* Check for some Flipper-buttons is under focus
*/
public get someFlipperButtonFocused(): boolean {
return Object.entries(this.Editor).filter(([moduleName, moduleClass]) => {
return moduleClass.flipper instanceof Flipper;
}).some(([moduleName, moduleClass]) => {
return moduleClass.flipper.currentItem;
});
}
/** /**
* Clean editor`s UI * Clean editor`s UI
*/ */
@ -204,6 +211,18 @@ export default class UI extends Module {
this.nodes.holder.innerHTML = ''; this.nodes.holder.innerHTML = '';
} }
/**
* Close all Editor's toolbars
*/
public closeAllToolbars(): void {
const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
BlockSettings.close();
InlineToolbar.close();
ConversionToolbar.close();
Toolbox.close();
}
/** /**
* Check for mobile mode and cache a result * Check for mobile mode and cache a result
*/ */
@ -277,17 +296,26 @@ export default class UI extends Module {
(event) => this.redactorClicked(event as MouseEvent), (event) => this.redactorClicked(event as MouseEvent),
false, false,
); );
this.Editor.Listeners.on(this.nodes.redactor,
'mousedown',
(event) => this.documentTouched(event as MouseEvent),
true,
);
this.Editor.Listeners.on(this.nodes.redactor,
'touchstart',
(event) => this.documentTouched(event as MouseEvent),
true,
);
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true); this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true);
this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), true); this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), true);
/** /**
* Handle selection change on mobile devices for the Inline Toolbar support * Handle selection change to manipulate Inline Toolbar appearance
*/ */
if (_.isTouchSupported()) { this.Editor.Listeners.on(document, 'selectionchange', (event: Event) => {
this.Editor.Listeners.on(document, 'selectionchange', (event) => { this.selectionChanged(event);
this.selectionChanged(event as Event); }, true);
}, true);
}
this.Editor.Listeners.on(window, 'resize', () => { this.Editor.Listeners.on(window, 'resize', () => {
this.resizeDebouncer(); this.resizeDebouncer();
@ -374,6 +402,7 @@ export default class UI extends Module {
* Manipulation with BlockSelections is handled in global backspacePress because they may occur * Manipulation with BlockSelections is handled in global backspacePress because they may occur
* with CMD+A or RectangleSelection and they can be handled on document event * with CMD+A or RectangleSelection and they can be handled on document event
*/ */
event.preventDefault();
event.stopPropagation(); event.stopPropagation();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
} }
@ -464,7 +493,6 @@ export default class UI extends Module {
this.Editor.BlockManager.dropPointer(); this.Editor.BlockManager.dropPointer();
this.Editor.InlineToolbar.close(); this.Editor.InlineToolbar.close();
this.Editor.Toolbar.close(); this.Editor.Toolbar.close();
this.Editor.BlockSelection.clearSelection(event);
this.Editor.ConversionToolbar.close(); this.Editor.ConversionToolbar.close();
} }
@ -479,44 +507,35 @@ export default class UI extends Module {
this.Editor.BlockManager.setCurrentBlockByChildNode(Selection.anchorNode); this.Editor.BlockManager.setCurrentBlockByChildNode(Selection.anchorNode);
} }
} }
/**
* Clear Selection if user clicked somewhere
*/
if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted) {
this.Editor.BlockSelection.clearSelection(event);
}
} }
/** /**
* All clicks on the redactor zone * First touch on editor
* * Fired before click
* @param {MouseEvent} event
*
* @description
* 1. Save clicked Block as a current {@link BlockManager#currentNode}
* it uses for the following:
* - add CSS modifier for the selected Block
* - on Enter press, we make a new Block under that
*
* 2. Move and show the Toolbar
*
* 3. Set a Caret
*
* 4. By clicks on the Editor's bottom zone:
* - if last Block is empty, set a Caret to this
* - otherwise, add a new empty Block and set a Caret to that
*
* 5. Hide the Inline Toolbar
*
* @see selectClickedBlock
* *
* Used to change current block we need to do it before 'selectionChange' event.
* Also:
* - Move and show the Toolbar
* - Set a Caret
*/ */
private redactorClicked(event: MouseEvent): void { private documentTouched(event: MouseEvent | TouchEvent): void {
if (!Selection.isCollapsed) {
return;
}
let clickedNode = event.target as HTMLElement; let clickedNode = event.target as HTMLElement;
/** /**
* If click was fired is on Editor`s wrapper, try to get clicked node by elementFromPoint method * If click was fired is on Editor`s wrapper, try to get clicked node by elementFromPoint method
*/ */
if (clickedNode === this.nodes.redactor) { if (clickedNode === this.nodes.redactor) {
clickedNode = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement; const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
clickedNode = document.elementFromPoint(clientX, clientY) as HTMLElement;
} }
/** /**
@ -541,9 +560,6 @@ export default class UI extends Module {
} }
} }
event.stopImmediatePropagation();
event.stopPropagation();
/** /**
* Move and open toolbar * Move and open toolbar
*/ */
@ -553,6 +569,25 @@ export default class UI extends Module {
* Hide the Plus Button * Hide the Plus Button
*/ */
this.Editor.Toolbar.plusButton.hide(); this.Editor.Toolbar.plusButton.hide();
}
/**
* All clicks on the redactor zone
*
* @param {MouseEvent} event
*
* @description
* - By clicks on the Editor's bottom zone:
* - if last Block is empty, set a Caret to this
* - otherwise, add a new empty Block and set a Caret to that
*/
private redactorClicked(event: MouseEvent): void {
if (!Selection.isCollapsed) {
return;
}
event.stopImmediatePropagation();
event.stopPropagation();
if (!this.Editor.BlockManager.currentBlock) { if (!this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.insert(); this.Editor.BlockManager.insert();
@ -593,7 +628,7 @@ export default class UI extends Module {
return; return;
} }
this.Editor.InlineToolbar.tryToShow(); this.Editor.InlineToolbar.tryToShow(true);
} }
/** /**

View file

@ -1,7 +1,7 @@
/** /**
* TextRange interface fot IE9- * TextRange interface fot IE9-
*/ */
import _ from './utils'; import * as _ from './utils';
import $ from './dom'; import $ from './dom';
interface TextRange { interface TextRange {
@ -101,24 +101,6 @@ export default class SelectionUtils {
return selection ? selection.isCollapsed : null; return selection ? selection.isCollapsed : null;
} }
/**
* Returns true if 85% of text content is selected
* @return {boolean}
*/
public static almostAllSelected(targetText: string): boolean {
const range = SelectionUtils.range;
if (!range) {
return false;
}
const copiedFragment = range.cloneContents();
const lengthOfWholeText = targetText.length;
const lengthOfCopiedText = copiedFragment.textContent.length;
return lengthOfCopiedText / lengthOfWholeText > 0.85;
}
/** /**
* Check current selection if it is at Editor's zone * Check current selection if it is at Editor's zone
* @return {boolean} * @return {boolean}
@ -194,6 +176,10 @@ export default class SelectionUtils {
return rect; return rect;
} }
if (sel.rangeCount === 0) {
return rect;
}
range = sel.getRangeAt(0).cloneRange() as Range; range = sel.getRangeAt(0).cloneRange() as Range;
if (range.getBoundingClientRect) { if (range.getBoundingClientRect) {

@ -1 +1 @@
Subproject commit 69f0c1a24cdfa443dcda9dca71015472709ae2fb Subproject commit 306ed49135905d56ebb7d55f90f26fcd603ca7f1

View file

@ -4,6 +4,16 @@
import Dom from './dom'; import Dom from './dom';
/**
* Possible log levels
*/
export enum LogLevels {
VERBOSE = 'VERBOSE',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
}
/** /**
* Allow to use global VERSION, that will be overwritten by Webpack * Allow to use global VERSION, that will be overwritten by Webpack
*/ */
@ -22,23 +32,90 @@ export interface ChainData {
/** /**
* Editor.js utils * Editor.js utils
*/ */
export default class Util {
/**
* Custom logger
*
* @param {string} msg - message
* @param {string} type - logging type 'log'|'warn'|'error'|'info'
* @param {*} [args] - argument to log with a message
* @param {string} style - additional styling to message
*/
public static log(msg: string, type: string = 'log', args?: any, style: string = 'color: inherit'): void {
if ( !('console' in window) || !window.console[ type ] ) { /**
return; * Returns basic keycodes as constants
} * @return {{}}
*/
export const keyCodes = {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
SHIFT: 16,
CTRL: 17,
ALT: 18,
ESC: 27,
SPACE: 32,
LEFT: 37,
UP: 38,
DOWN: 40,
RIGHT: 39,
DELETE: 46,
META: 91,
};
const editorLabelText = `Editor.js ${VERSION}`; /**
const editorLabelStyle = `line-height: 1em; * Return mouse buttons codes
*/
export const mouseButtons = {
LEFT: 0,
WHEEL: 1,
RIGHT: 2,
BACKWARD: 3,
FORWARD: 4,
};
/**
* Custom logger
*
* @param {boolean} labeled if true, Editor.js label is shown
* @param {string} msg - message
* @param {string} type - logging type 'log'|'warn'|'error'|'info'
* @param {*} [args] - argument to log with a message
* @param {string} style - additional styling to message
* @param labeled
*/
function _log(
labeled: boolean,
msg: string,
type: string = 'log',
args?: any,
style: string = 'color: inherit',
): void {
if ( !('console' in window) || !window.console[ type ] ) {
return;
}
const isSimpleType = ['info', 'log', 'warn', 'error'].includes(type);
const argsToPass = [];
switch (_log.logLevel) {
case LogLevels.ERROR:
if (type !== 'error') {
return;
}
break;
case LogLevels.WARN:
if (!['error', 'warn'].includes(type)) {
return;
}
break;
case LogLevels.INFO:
if (!isSimpleType || labeled) {
return;
}
break;
}
if (args) {
argsToPass.push(args);
}
const editorLabelText = `Editor.js ${VERSION}`;
const editorLabelStyle = `line-height: 1em;
color: #006FEA; color: #006FEA;
display: inline-block; display: inline-block;
font-size: 11px; font-size: 11px;
@ -49,341 +126,364 @@ export default class Util {
border: 1px solid rgba(56, 138, 229, 0.16); border: 1px solid rgba(56, 138, 229, 0.16);
margin: 4px 5px 4px 0;`; margin: 4px 5px 4px 0;`;
try { if (labeled) {
if (['time', 'timeEnd'].includes(type)) { if (isSimpleType) {
console[type](`( ${editorLabelText} ) ${msg}`); argsToPass.unshift(editorLabelStyle, style);
} else if (args) { msg = `%c${editorLabelText}%c ${msg}`;
console[type](`%c${editorLabelText}%c ${msg} %o`, editorLabelStyle, style, args); } else {
} else { msg = `( ${editorLabelText} )${msg}`;
console[type](`%c${editorLabelText}%c ${msg}`, editorLabelStyle, style); }
}
} catch (ignored) {}
} }
/** try {
* Returns basic keycodes as constants if (!isSimpleType) {
* @return {{}} console[type](msg);
*/ } else if (args) {
static get keyCodes() { console[type](`${msg} %o`, ...argsToPass);
return { } else {
BACKSPACE: 8, console[type](msg, ...argsToPass);
TAB: 9, }
ENTER: 13, } catch (ignored) {}
SHIFT: 16, }
CTRL: 17,
ALT: 18,
ESC: 27,
SPACE: 32,
LEFT: 37,
UP: 38,
DOWN: 40,
RIGHT: 39,
DELETE: 46,
META: 91,
};
}
/** /**
* Return mouse buttons codes * Current log level
*/ */
static get mouseButtons() { _log.logLevel = LogLevels.VERBOSE;
return {
LEFT: 0,
WHEEL: 1,
RIGHT: 2,
BACKWARD: 3,
FORWARD: 4,
};
}
/** /**
* Returns true if passed key code is printable (a-Z, 0-9, etc) character. * Set current log level
* @param {number} keyCode *
* @return {boolean} * @param {LogLevels} logLevel - log level to set
*/ */
public static isPrintableKey( keyCode: number ): boolean { export function setLogLevel(logLevel: LogLevels) {
return (keyCode > 47 && keyCode < 58) || // number keys _log.logLevel = logLevel;
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s) }
(keyCode > 64 && keyCode < 91) || // letter keys
(keyCode > 95 && keyCode < 112) || // Numpad keys
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
(keyCode > 218 && keyCode < 223); // [\]' (in order)
}
/**
* _log method proxy without Editor.js label
*/
export const log = _log.bind(window, false);
/**
* _log method proxy with Editor.js label
*/
export const logLabeled = _log.bind(window, true);
/**
* Returns true if passed key code is printable (a-Z, 0-9, etc) character.
* @param {number} keyCode
* @return {boolean}
*/
export function isPrintableKey( keyCode: number ): boolean {
return (keyCode > 47 && keyCode < 58) || // number keys
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)
(keyCode > 64 && keyCode < 91) || // letter keys
(keyCode > 95 && keyCode < 112) || // Numpad keys
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
(keyCode > 218 && keyCode < 223); // [\]' (in order)
}
/**
* Fires a promise sequence asyncronically
*
* @param {ChainData[]} chains - list or ChainData's
* @param {Function} success - success callback
* @param {Function} fallback - callback that fires in case of errors
*
* @return {Promise}
*/
export async function sequence(
chains: ChainData[],
success: (data: any) => void = () => {},
fallback: (data: any) => void = () => {},
): Promise<void> {
/** /**
* Fires a promise sequence asyncronically * Decorator
* *
* @param {ChainData[]} chains - list or ChainData's * @param {ChainData} chainData
* @param {Function} success - success callback *
* @param {Function} fallback - callback that fires in case of errors * @param {Function} successCallback
* @param {Function} fallbackCallback
* *
* @return {Promise} * @return {Promise}
*/ */
public static async sequence( async function waitNextBlock(
chains: ChainData[], chainData: ChainData,
success: (data: any) => void = () => {}, successCallback: (data: any) => void,
fallback: (data: any) => void = () => {}, fallbackCallback: (data: any) => void,
): Promise<void> { ): Promise<void> {
/** try {
* Decorator await chainData.function(chainData.data);
* await successCallback(typeof chainData.data !== 'undefined' ? chainData.data : {});
* @param {ChainData} chainData } catch (e) {
* fallbackCallback(typeof chainData.data !== 'undefined' ? chainData.data : {});
* @param {Function} successCallback
* @param {Function} fallbackCallback
*
* @return {Promise}
*/
async function waitNextBlock(
chainData: ChainData,
successCallback: (data: any) => void,
fallbackCallback: (data: any) => void,
): Promise<void> {
try {
await chainData.function(chainData.data);
await successCallback(typeof chainData.data !== 'undefined' ? chainData.data : {});
} catch (e) {
fallbackCallback(typeof chainData.data !== 'undefined' ? chainData.data : {});
}
} }
/**
* pluck each element from queue
* First, send resolved Promise as previous value
* Each plugins "prepare" method returns a Promise, that's why
* reduce current element will not be able to continue while can't get
* a resolved Promise
*/
return await chains.reduce(async (previousValue, currentValue) => {
await previousValue;
return waitNextBlock(currentValue, success, fallback);
}, Promise.resolve());
} }
/** /**
* Make array from array-like collection * pluck each element from queue
* * First, send resolved Promise as previous value
* @param {ArrayLike} collection * Each plugins "prepare" method returns a Promise, that's why
* * reduce current element will not be able to continue while can't get
* @return {Array} * a resolved Promise
*/ */
public static array(collection: ArrayLike<any>): any[] { return await chains.reduce(async (previousValue, currentValue) => {
return Array.prototype.slice.call(collection); await previousValue;
return waitNextBlock(currentValue, success, fallback);
}, Promise.resolve());
}
/**
* Make array from array-like collection
*
* @param {ArrayLike} collection
*
* @return {Array}
*/
export function array(collection: ArrayLike<any>): any[] {
return Array.prototype.slice.call(collection);
}
/**
* Check if passed variable is a function
* @param {*} fn
* @return {boolean}
*/
export function isFunction(fn: any): boolean {
return typeof fn === 'function';
}
/**
* Check if passed function is a class
* @param {function} fn
* @return {boolean}
*/
export function isClass(fn: any): boolean {
return typeof fn === 'function' && /^\s*class\s+/.test(fn.toString());
}
/**
* Checks if object is empty
*
* @param {Object} object
* @return {boolean}
*/
export function isEmpty(object: object): boolean {
if (!object) {
return true;
} }
/** return Object.keys(object).length === 0 && object.constructor === Object;
* Check if passed variable is a function }
* @param {*} fn
* @return {boolean}
*/
public static isFunction(fn: any): boolean {
return typeof fn === 'function';
}
/** /**
* Check if passed function is a class * Check if passed object is a Promise
* @param {function} fn * @param {*} object - object to check
* @return {boolean} * @return {Boolean}
*/ */
public static isClass(fn: any): boolean { export function isPromise(object: any): boolean {
return typeof fn === 'function' && /^\s*class\s+/.test(fn.toString()); return Promise.resolve(object) === object;
} }
/** /**
* Checks if object is empty * Delays method execution
* *
* @param {Object} object * @param {Function} method
* @return {boolean} * @param {Number} timeout
*/ */
public static isEmpty(object: object): boolean { export function delay(method: (...args: any[]) => any, timeout: number) {
if (!object) { return function() {
return true; const context = this,
} args = arguments;
return Object.keys(object).length === 0 && object.constructor === Object; window.setTimeout(() => method.apply(context, args), timeout);
} };
}
/** /**
* Check if passed object is a Promise * Get file extension
* @param {*} object - object to check *
* @return {Boolean} * @param {File} file
*/ * @return string
public static isPromise(object: any): boolean { */
return Promise.resolve(object) === object; export function getFileExtension(file: File): string {
} return file.name.split('.').pop();
}
/** /**
* Delays method execution * Check if string is MIME type
* *
* @param {Function} method * @param {string} type
* @param {Number} timeout * @return boolean
*/ */
public static delay(method: (...args: any[]) => any, timeout: number) { export function isValidMimeType(type: string): boolean {
return function() { return /^[-\w]+\/([-+\w]+|\*)$/.test(type);
const context = this, }
args = arguments;
window.setTimeout(() => method.apply(context, args), timeout); /**
}; * Debouncing method
} * Call method after passed time
*
* Note that this method returns Function and declared variable need to be called
*
* @param {Function} func - function that we're throttling
* @param {Number} wait - time in milliseconds
* @param {Boolean} immediate - call now
* @return {Function}
*/
export function debounce(func: () => void, wait?: number , immediate?: boolean): () => void {
let timeout;
/** return () => {
* Get file extension const context = this,
* args = arguments;
* @param {File} file
* @return string
*/
public static getFileExtension(file: File): string {
return file.name.split('.').pop();
}
/** const later = () => {
* Check if string is MIME type timeout = null;
* if (!immediate) {
* @param {string} type
* @return boolean
*/
public static isValidMimeType(type: string): boolean {
return /^[-\w]+\/([-+\w]+|\*)$/.test(type);
}
/**
* Debouncing method
* Call method after passed time
*
* Note that this method returns Function and declared variable need to be called
*
* @param {Function} func - function that we're throttling
* @param {Number} wait - time in milliseconds
* @param {Boolean} immediate - call now
* @return {Function}
*/
public static debounce(func: () => void, wait?: number , immediate?: boolean): () => void {
let timeout;
return () => {
const context = this,
args = arguments;
const later = () => {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
const callNow = immediate && !timeout;
window.clearTimeout(timeout);
timeout = window.setTimeout(later, wait);
if (callNow) {
func.apply(context, args); func.apply(context, args);
} }
}; };
}
/** const callNow = immediate && !timeout;
* Copies passed text to the clipboard
* @param text
*/
public static copyTextToClipboard(text) {
const el = Dom.make('div', 'codex-editor-clipboard', {
innerHTML: text,
});
document.body.appendChild(el); window.clearTimeout(timeout);
timeout = window.setTimeout(later, wait);
const selection = window.getSelection(); if (callNow) {
const range = document.createRange(); func.apply(context, args);
range.selectNode(el);
window.getSelection().removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
document.body.removeChild(el);
}
/**
* Returns object with os name as key and boolean as value. Shows current user OS
*
* @return {[key: string]: boolean}
*/
public static getUserOS(): {[key: string]: boolean} {
const OS = {
win: false,
mac: false,
x11: false,
linux: false,
};
const userOS = Object.keys(OS).find((os: string) => navigator.appVersion.toLowerCase().indexOf(os) !== -1);
if (userOS) {
OS[userOS] = true;
return OS;
} }
};
}
/**
* Copies passed text to the clipboard
* @param text
*/
export function copyTextToClipboard(text) {
const el = Dom.make('div', 'codex-editor-clipboard', {
innerHTML: text,
});
document.body.appendChild(el);
const selection = window.getSelection();
const range = document.createRange();
range.selectNode(el);
window.getSelection().removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
document.body.removeChild(el);
}
/**
* Returns object with os name as key and boolean as value. Shows current user OS
*
* @return {[key: string]: boolean}
*/
export function getUserOS(): {[key: string]: boolean} {
const OS = {
win: false,
mac: false,
x11: false,
linux: false,
};
const userOS = Object.keys(OS).find((os: string) => navigator.appVersion.toLowerCase().indexOf(os) !== -1);
if (userOS) {
OS[userOS] = true;
return OS; return OS;
} }
/** return OS;
* Capitalizes first letter of the string }
* @param {string} text
* @return {string}
*/
public static capitalize(text: string): string {
return text[0].toUpperCase() + text.slice(1);
}
/** /**
* Merge to objects recursively * Capitalizes first letter of the string
* @param {object} target * @param {string} text
* @param {object[]} sources * @return {string}
* @return {object} */
*/ export function capitalize(text: string): string {
public static deepMerge(target, ...sources) { return text[0].toUpperCase() + text.slice(1);
const isObject = (item) => item && Util.typeof(item) === 'object'; }
if (!sources.length) { return target; } /**
const source = sources.shift(); * Merge to objects recursively
* @param {object} target
* @param {object[]} sources
* @return {object}
*/
export function deepMerge(target, ...sources) {
const isObject = (item) => item && typeOf(item) === 'object';
if (isObject(target) && isObject(source)) { if (!sources.length) { return target; }
for (const key in source) { const source = sources.shift();
if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
}
Util.deepMerge(target[key], source[key]); if (isObject(target) && isObject(source)) {
} else { for (const key in source) {
Object.assign(target, { [key]: source[key] }); if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
} }
deepMerge(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
} }
} }
return Util.deepMerge(target, ...sources);
} }
/** return deepMerge(target, ...sources);
* Return true if current device supports touch events }
*
* Note! This is a simple solution, it can give false-positive results. /**
* To detect touch devices more carefully, use 'touchstart' event listener * Return true if current device supports touch events
* @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/ *
* * Note! This is a simple solution, it can give false-positive results.
* @return {boolean} * To detect touch devices more carefully, use 'touchstart' event listener
*/ * @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
public static isTouchSupported(): boolean { *
return 'ontouchstart' in document.documentElement; * @return {boolean}
} */
export const isTouchSupported: boolean = 'ontouchstart' in document.documentElement;
/**
* Return string representation of the object type /**
* * Return string representation of the object type
* @param {any} object *
*/ * @param {any} object
public static typeof(object: any): string { */
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); export function typeOf(object: any): string {
} return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
/**
* Make shortcut command more human-readable
* @param {string} shortcut string like 'CMD+B'
*/
export function beautifyShortcut(shortcut: string): string {
const OS = getUserOS();
shortcut = shortcut
.replace(/shift/gi, '⇧')
.replace(/backspace/gi, '⌫')
.replace(/enter/gi, '⏎')
.replace(/up/gi, '↑')
.replace(/left/gi, '→')
.replace(/down/gi, '↓')
.replace(/right/gi, '←')
.replace(/escape/gi, '⎋')
.replace(/insert/gi, 'Ins')
.replace(/delete/gi, '␡')
.replace(/\+/gi, ' + ');
if (OS.mac) {
shortcut = shortcut.replace(/ctrl|cmd/gi, '⌘').replace(/alt/gi, '⌥');
} else {
shortcut = shortcut.replace(/cmd/gi, 'Ctrl').replace(/windows/gi, 'WIN');
}
return shortcut;
} }

View file

@ -1,19 +1,15 @@
.ce-conversion-toolbar { .ce-conversion-toolbar {
@apply --overlay-pane; @apply --overlay-pane;
padding: 3px;
box-shadow: 0 6px 12px -6px rgba(131, 147, 173, 0.46),
5px -12px 34px -13px rgba(97, 105, 134, 0.6),
0 26px 52px 3px rgba(147, 165, 186, 0.24);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
will-change: transform, opacity; will-change: transform, opacity;
transition: transform 150ms ease, opacity 250ms ease; transition: transform 100ms ease, opacity 100ms ease;
transform: translateY(8px) scale(0.9); transform: translateY(-8px);
left: -1px;
&::before { width: 150px;
left: 20px; margin-top: 5px;
} box-sizing: content-box;
&--showed { &--showed {
opacity: 1; opacity: 1;
@ -28,21 +24,65 @@
&__buttons { &__buttons {
display: flex; display: flex;
} }
&__label {
color: var(--grayText);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.33px;
padding: 10px 10px 5px;
text-transform: uppercase;
}
} }
.ce-conversion-tool { .ce-conversion-tool {
@apply --toolbar-button; display: flex;
line-height: normal; padding: 5px 10px;
font-size: 14px;
line-height: 20px;
font-weight: 500;
cursor: pointer;
align-items: center;
&:not(:last-of-type) { &--hidden {
margin-right: 2px; display: none;
}
&--focused {
box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08);
background: rgba(34, 186, 255, 0.08) !important;
&-animated {
animation-name: buttonClicked;
animation-duration: 250ms;
}
}
&:hover {
background: var(--bg-light);
}
&__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;
}
} }
&--last { &--last {
margin-right: 0 !important; margin-right: 0 !important;
} }
&:hover,
&--active { &--active {
color: var(--color-active-icon) !important; color: var(--color-active-icon) !important;
} }

View file

@ -2,7 +2,7 @@
* Block Tool wrapper * Block Tool wrapper
*/ */
.cdx-block { .cdx-block {
padding: 0.7em 0; padding: 0.4em 0;
} }
/** /**

View file

@ -1,10 +1,6 @@
.ce-inline-toolbar { .ce-inline-toolbar {
@apply --overlay-pane; @apply --overlay-pane;
padding: 3px;
transform: translateX(-50%) translateY(8px) scale(0.9); transform: translateX(-50%) translateY(8px) scale(0.9);
box-shadow: 0 6px 12px -6px rgba(131, 147, 173, 0.46),
5px -12px 34px -13px rgba(97, 105, 134, 0.6),
0 26px 52px 3px rgba(147, 165, 186, 0.24);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: transform 150ms ease, opacity 250ms ease; transition: transform 150ms ease, opacity 250ms ease;
@ -39,17 +35,67 @@
&__buttons { &__buttons {
display: flex; display: flex;
padding: 0 6px;
}
&__actions {
}
&__dropdown {
display: inline-flex;
height: var(--toolbar-buttons-size);
padding: 0 9px 0 10px;
margin: 0 6px 0 -6px;
align-items: center;
cursor: pointer;
border-right: 1px solid var(--color-gray-border);
&:hover {
background: var(--bg-light);
}
&--hidden {
display: none;
}
&-content{
display: flex;
font-weight: 500;
font-size: 14px;
svg {
height: 12px;
}
}
.icon--toggler-down {
margin-left: 4px;
}
}
&__shortcut {
opacity: 0.6;
word-spacing: -3px;
margin-top: 3px;
} }
} }
.ce-inline-tool { .ce-inline-tool {
@apply --toolbar-button; @apply --toolbar-button;
border-radius: 0;
line-height: normal; line-height: normal;
width: auto;
padding: 0 5px !important;
min-width: 24px;
&:not(:last-of-type) { &:not(:last-of-type) {
margin-right: 2px; margin-right: 2px;
} }
.icon {
height: 12px;
}
&--last { &--last {
margin-right: 0 !important; margin-right: 0 !important;
} }
@ -66,20 +112,22 @@
} }
.icon--unlink { .icon--unlink {
display: inline-block; display: inline-block;
margin-bottom: -1px;
} }
} }
&-input { &-input {
background-color: var(--bg-light);
outline: none; outline: none;
border: 0; border: 0;
border-radius: 3px; border-radius: 0 0 4px 4px;
margin: 3px 0 0; margin: 0;
font-size: 13px; font-size: 13px;
padding: 8px; padding: 10px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
display: none; display: none;
font-weight: 500;
border-top: 1px solid rgba(201,201,204,.48);
&::placeholder { &::placeholder {
color: var(--grayText); color: var(--grayText);

View file

@ -43,6 +43,12 @@
left: calc(var(--toolbox-buttons-size) * -1); left: calc(var(--toolbox-buttons-size) * -1);
flex-shrink: 0; flex-shrink: 0;
&-shortcut {
opacity: 0.6;
word-spacing: -2px;
margin-top: 5px;
}
&--hidden { &--hidden {
display: none; display: none;
} }

View file

@ -22,55 +22,15 @@
@apply --toolbox-button; @apply --toolbox-button;
flex-shrink: 0; flex-shrink: 0;
} }
&__tooltip {
position: absolute;
top: 25px;
padding: 6px 10px;
border-radius: 5px;
opacity: 0;
background: var(--bg-light);
box-shadow: 0 10px 12px -9px rgba(26, 39, 54, 0.32), 0 3px 2px -2px rgba(33, 48, 73, 0.05);
color: #5C6174;
font-size: 12px;
text-align: center;
user-select: none;
pointer-events: none;
transition: opacity 150ms ease-in, left 0.1s linear;
will-change: opacity, left;
letter-spacing: 0.02em;
line-height: 1em;
@media (--mobile) {
display: none;
}
&-shortcut {
color: rgba(100, 105, 122, 0.6);
word-spacing: -2px;
margin-top: 5px;
}
&--shown {
opacity: 1;
transition-delay: 0.1s, 0s;
}
&::before {
content: '';
width: 10px;
height: 10px;
position: absolute;
top: -5px;
left: 50%;
margin-left: -5px;
transform: rotate(-45deg);
background-color: var(--bg-light);
z-index: -1;
}
}
} }
.ce-toolbox-button-tooltip {
&__shortcut {
opacity: 0.6;
word-spacing: -3px;
margin-top: 3px;
}
}
/** /**
* Styles for Narrow mode * Styles for Narrow mode

View file

@ -57,25 +57,14 @@
--overlay-pane: { --overlay-pane: {
position: absolute; position: absolute;
background-color: #FFFFFF; background-color: #FFFFFF;
box-shadow: 0 8px 23px -6px rgba(21,40,54,0.31), 22px -14px 34px -18px rgba(33,48,73,0.26); border: 1px solid #EAEAEA;
box-shadow: 0 3px 15px -3px rgba(13,20,33,0.13);
border-radius: 4px; border-radius: 4px;
z-index: 2; z-index: 2;
@media (--mobile){ @media (--mobile){
box-shadow: 0 5px 9px -5px rgba(21, 40, 54, 0.49),6px 15px 34px -6px rgba(33, 48, 73, 0.54); box-shadow: 0 13px 7px -5px rgba(26, 38, 49, 0.09),6px 15px 34px -6px rgba(33, 48, 73, 0.29);
} border-bottom-color: #d5d7db;
&::before {
content: '';
width: 15px;
height: 15px;
position: absolute;
top: -7px;
left: 50%;
margin-left: -7px;
transform: rotate(-45deg);
background-color: #fff;
z-index: -1;
} }
&--left-oriented { &--left-oriented {
@ -134,7 +123,8 @@
outline: none; outline: none;
background-color: transparent; background-color: transparent;
vertical-align: bottom; vertical-align: bottom;
color: var(--grayText); color: #000;
margin: 0;
&:hover { &:hover {
background-color: var(--bg-light); background-color: var(--bg-light);

View file

@ -9,6 +9,7 @@ import Events from '../components/modules/events';
import Shortcuts from '../components/modules/shortcuts'; import Shortcuts from '../components/modules/shortcuts';
import Paste from '../components/modules/paste'; import Paste from '../components/modules/paste';
import Notifier from '../components/modules/notifier'; import Notifier from '../components/modules/notifier';
import Tooltip from '../components/modules/tooltip';
import DragNDrop from '../components/modules/dragNDrop'; import DragNDrop from '../components/modules/dragNDrop';
import ModificationsObserver from '../components/modules/modificationsObserver'; import ModificationsObserver from '../components/modules/modificationsObserver';
import Renderer from '../components/modules/renderer'; import Renderer from '../components/modules/renderer';
@ -33,6 +34,7 @@ import RectangleSelection from '../components/modules/RectangleSelection';
import InlineToolbarAPI from '../components/modules/api/inlineToolbar'; import InlineToolbarAPI from '../components/modules/api/inlineToolbar';
import CrossBlockSelection from '../components/modules/crossBlockSelection'; import CrossBlockSelection from '../components/modules/crossBlockSelection';
import ConversionToolbar from '../components/modules/toolbar/conversion'; import ConversionToolbar from '../components/modules/toolbar/conversion';
import TooltipAPI from '../components/modules/api/tooltip';
export interface EditorModules { export interface EditorModules {
UI: UI; UI: UI;
@ -57,6 +59,7 @@ export interface EditorModules {
Caret: Caret; Caret: Caret;
Saver: Saver; Saver: Saver;
Notifier: Notifier; Notifier: Notifier;
Tooltip: Tooltip;
BlockManager: BlockManager; BlockManager: BlockManager;
BlocksAPI: BlocksAPI; BlocksAPI: BlocksAPI;
CaretAPI: CaretAPI; CaretAPI: CaretAPI;
@ -70,4 +73,5 @@ export interface EditorModules {
InlineToolbarAPI: InlineToolbarAPI; InlineToolbarAPI: InlineToolbarAPI;
CrossBlockSelection: CrossBlockSelection; CrossBlockSelection: CrossBlockSelection;
NotifierAPI: NotifierAPI; NotifierAPI: NotifierAPI;
TooltipAPI: TooltipAPI;
} }

View file

@ -8,4 +8,5 @@ export * from './styles';
export * from './caret'; export * from './caret';
export * from './toolbar'; export * from './toolbar';
export * from './notifier'; export * from './notifier';
export * from './inline-toolbar' export * from './tooltip';
export * from './inline-toolbar';

30
types/api/tooltip.d.ts vendored Normal file
View file

@ -0,0 +1,30 @@
/**
* Tooltip API
*/
import {TooltipContent, TooltipOptions} from '../../src/components/external/codex.tooltips';
export interface Tooltip {
/**
* Show tooltip
*
* @param {HTMLElement} element
* @param {TooltipContent} content
* @param {TooltipOptions} options
*/
show: (element: HTMLElement, content: TooltipContent, options?: TooltipOptions) => void;
/**
* Hides tooltip
*/
hide: () => void;
/**
* Decorator for showing Tooltip by mouseenter/mouseleave
*
* @param {HTMLElement} element
* @param {TooltipContent} content
* @param {TooltipOptions} options
*/
onHover: (element: HTMLElement, content: TooltipContent, options?: TooltipOptions) => void;
}

View file

@ -1,5 +1,5 @@
import {ToolConstructable, ToolSettings} from '../tools'; import {ToolConstructable, ToolSettings} from '../tools';
import {OutputData} from '../index'; import {LogLevels, OutputData} from '../index';
import {SanitizerConfig} from './sanitizer-config'; import {SanitizerConfig} from './sanitizer-config';
export interface EditorConfig { export interface EditorConfig {
@ -57,6 +57,11 @@ export interface EditorConfig {
*/ */
minHeight?: number; minHeight?: number;
/**
* Editors log level (how many logs you want to see)
*/
logLevel?: LogLevels;
/** /**
* Fires when Editor is ready to work * Fires when Editor is ready to work
*/ */

View file

@ -2,3 +2,4 @@ export * from './editor-config';
export * from './sanitizer-config'; export * from './sanitizer-config';
export * from './paste-config'; export * from './paste-config';
export * from './conversion-config'; export * from './conversion-config';
export * from './log-levels';

9
types/configs/log-levels.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
/**
* Available log levels
*/
export enum LogLevels {
VERBOSE = 'VERBOSE',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
}

34
types/index.d.ts vendored
View file

@ -5,8 +5,21 @@
*/ */
import {EditorConfig} from './configs'; import {EditorConfig} from './configs';
import {Blocks, Caret, Events, Listeners, Notifier, Sanitizer, Saver, Selection, Styles, Toolbar, InlineToolbar} from './api'; import {
import {OutputData} from "./data-formats/output-data"; Blocks,
Caret,
Events,
InlineToolbar,
Listeners,
Notifier,
Sanitizer,
Saver,
Selection,
Styles,
Toolbar,
Tooltip,
} from './api';
import {OutputData} from './data-formats/output-data';
/** /**
* Interfaces used for development * Interfaces used for development
@ -34,7 +47,7 @@ export {
FilePasteEventDetail, FilePasteEventDetail,
} from './tools'; } from './tools';
export {BlockTune, BlockTuneConstructable} from './block-tunes'; export {BlockTune, BlockTuneConstructable} from './block-tunes';
export {EditorConfig, SanitizerConfig, PasteConfig} from './configs'; export {EditorConfig, SanitizerConfig, PasteConfig, LogLevels, ConversionConfig} from './configs';
export {OutputData} from './data-formats/output-data'; export {OutputData} from './data-formats/output-data';
/** /**
@ -53,6 +66,7 @@ export interface API {
styles: Styles; styles: Styles;
toolbar: Toolbar; toolbar: Toolbar;
inlineToolbar: InlineToolbar; inlineToolbar: InlineToolbar;
tooltip: Tooltip;
} }
/** /**
@ -82,37 +96,37 @@ declare class EditorJS {
/** /**
* @see Saver.save * @see Saver.save
*/ */
save(): Promise<OutputData>; public save(): Promise<OutputData>;
/** /**
* @see Blocks.clear * @see Blocks.clear
*/ */
clear(): void; public clear(): void;
/** /**
* @see Blocks.render * @see Blocks.render
*/ */
render(data: OutputData): Promise<void>; public render(data: OutputData): Promise<void>;
/** /**
* @see Caret.focus * @see Caret.focus
*/ */
focus(atEnd?: boolean): boolean; public focus(atEnd?: boolean): boolean;
/** /**
* @see Events.on * @see Events.on
*/ */
on(eventName: string, callback: (data?: any) => void): void; public on(eventName: string, callback: (data?: any) => void): void;
/** /**
* @see Events.off * @see Events.off
*/ */
off(eventName: string, callback: (data?: any) => void): void; public off(eventName: string, callback: (data?: any) => void): void;
/** /**
* @see Events.emit * @see Events.emit
*/ */
emit(eventName: string, data: any): void; public emit(eventName: string, data: any): void;
/** /**
* Destroy Editor instance and related DOM elements * Destroy Editor instance and related DOM elements

View file

@ -46,6 +46,25 @@ export interface BlockTool extends BaseTool {
* @param {PasteEvent} event * @param {PasteEvent} event
*/ */
onPaste?(event: PasteEvent): void; onPaste?(event: PasteEvent): void;
/**
* Lifecycle hooks
*/
/**
* Called after block content added to the page
*/
rendered?(): void;
/**
* Called each time block content is updated
*/
updated?(): void;
/**
* Called after block removed from the page but before instance is deleted
*/
removed?(): void;
} }
export interface BlockToolConstructable extends BaseToolConstructable { export interface BlockToolConstructable extends BaseToolConstructable {

View file

@ -15,7 +15,6 @@ export interface BaseTool {
} }
export interface BaseToolConstructable { export interface BaseToolConstructable {
/** /**
* Define Tool type as Inline * Define Tool type as Inline
*/ */
@ -26,6 +25,11 @@ export interface BaseToolConstructable {
*/ */
sanitize?: SanitizerConfig; sanitize?: SanitizerConfig;
/**
* Title of Inline Tool
*/
title?: string;
/** /**
* Describe constructor parameters * Describe constructor parameters
*/ */

3795
yarn.lock

File diff suppressed because it is too large Load diff