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"]
path = example/tools/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
Copyright © 2015-2018 CodeX
Copyright © 2015-present CodeX
Licensed under the Apache License, Version 2.0 (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 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
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 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
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.
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
MIT
Copyright (c) 2014-2018 Denis Pushkarev
@ -379,3 +390,24 @@ Apache License
regenerator-runtime
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"/>
</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 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"/>
@ -22,11 +22,13 @@
</g>
</symbol>
<symbol id="italic">
<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"/>
<symbol id="italic" viewBox="0 0 4 11">
<path d="M3.289 4.17L2.164 9.713c-.078.384-.238.674-.48.87-.243.198-.52.296-.831.296-.312 0-.545-.1-.699-.302-.153-.202-.192-.49-.116-.864L1.15 4.225c.077-.38.232-.665.466-.857a1.25 1.25 0 01.818-.288c.312 0 .55.096.713.288.163.192.21.46.141.801zm-.667-2.09c-.295 0-.53-.09-.706-.273-.176-.181-.233-.439-.173-.77.055-.302.207-.55.457-.745C2.45.097 2.716 0 3 0c.273 0 .5.088.68.265.179.176.238.434.177.771-.06.327-.21.583-.45.767-.24.185-.502.277-.785.277z"/>
</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 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"/>
@ -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"/>
</symbol>
<symbol id="unlink" viewBox="0 0 16 18">
<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"/>
<symbol id="toggler-down">
<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>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,5 +1,24 @@
# 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
- `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.
### 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.
@ -179,3 +227,4 @@ const editor = EditorJS();
editor.focus();
editor.save();
```

View File

@ -44,7 +44,7 @@ Then require this script.
### 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
<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)
### 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',
data: {
url: 'https://codex.so/upload/redactor_images/o_e48549d1855c7fc1807308dd14990126.jpg',
url: 'https://capella.pics/bbe6896c-ca1f-439e-8cb6-ebfda0d397d6.jpg',
caption: '',
stretched: false,
withBorder: true,

View File

@ -254,7 +254,7 @@
{
type: 'image',
data: {
url: 'https://codex.so/upload/redactor_images/o_e48549d1855c7fc1807308dd14990126.jpg',
url: 'https://capella.pics/bbe6896c-ca1f-439e-8cb6-ebfda0d397d6.jpg',
caption: '',
stretched: false,
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",
"version": "2.15.1",
"version": "2.16.0",
"description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js",
"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">
<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"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 11">
<path d="M3.289 4.17L2.164 9.713c-.078.384-.238.674-.48.87-.243.198-.52.296-.831.296-.312 0-.545-.1-.699-.302-.153-.202-.192-.49-.116-.864L1.15 4.225c.077-.38.232-.665.466-.857a1.25 1.25 0 01.818-.288c.312 0 .55.096.713.288.163.192.21.46.141.801zm-.667-2.09c-.295 0-.53-.09-.706-.273-.176-.181-.233-.439-.173-.77.055-.302.207-.55.457-.745C2.45.097 2.716 0 3 0c.273 0 .5.088.68.265.179.176.238.434.177.771-.06.327-.21.583-.45.767-.24.185-.502.277-.785.277z"/>
</svg>

Before

Width:  |  Height:  |  Size: 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">
<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"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 11">
<path d="M13.073 2.099l-1.448 1.448A3 3 0 009 2H8V0h1c1.68 0 3.166.828 4.073 2.099zM6.929 4l-.879.879L7.172 6H5a1 1 0 110-2h1.929zM6 0v2H5a3 3 0 100 6h1v2H5A5 5 0 115 0h1zm6.414 7l2.122 2.121-1.415 1.415L11 8.414l-2.121 2.122L7.464 9.12 9.586 7 7.464 4.879 8.88 3.464 11 5.586l2.121-2.122 1.415 1.415L12.414 7z"/>
</svg>

Before

Width:  |  Height:  |  Size: 892 B

After

Width:  |  Height:  |  Size: 384 B

View File

@ -17,10 +17,8 @@ export default class DeleteTune implements BlockTune {
/**
* Styles
* @type {{wrapper: string}}
*/
private CSS = {
wrapper: 'ass',
button: 'ce-settings__button',
buttonDelete: 'ce-settings__button--delete',
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.appendChild($.svg('cross', 12, 12));
this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false);
/**
* Enable tooltip module
*/
this.api.tooltip.onHover(this.nodes.button, 'Delete');
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.blocks.delete();
this.api.toolbar.close();
this.api.tooltip.hide();
/**
* 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),
false,
);
/**
* Enable tooltip module on button
*/
this.api.tooltip.onHover(moveDownButton, 'Move down');
return moveDownButton;
}
@ -88,5 +94,7 @@ export default class MoveDownTune implements BlockTune {
/** Change blocks positions */
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),
false,
);
/**
* Enable tooltip module on button
*/
this.api.tooltip.onHover(moveUpButton, 'Move up');
return moveUpButton;
}
@ -94,5 +100,8 @@ export default class MoveUpTune implements BlockTune {
/** Change blocks positions */
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 $ from './dom';
import _ from './utils';
import * as _ from './utils';
/**
* @class Block
@ -28,6 +28,21 @@ import DeleteTune from './block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down';
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
*
@ -165,10 +180,20 @@ export default class Block {
* @return {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) {
return pluginsContent.childNodes[0] as HTMLElement;
if (blockContentNodes && blockContentNodes.childNodes.length) {
/**
* 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;
@ -337,6 +362,29 @@ export default class Block {
*/
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
* @param {String} toolName - Tool name that passed on initialization
@ -375,12 +423,16 @@ export default class Block {
* @param {String} methodName
* @param {Object} params
*/
public call(methodName: string, params: object) {
public call(methodName: string, params?: object) {
/**
* call Tool's method with the instance context
*/
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
*/
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();
}
/**
* 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
* @returns {HTMLDivElement}

View File

@ -1,6 +1,6 @@
import _ from './utils';
import * as _ from './utils';
import $ from './dom';
import Block from './block';
import Block, {BlockToolAPI} from './block';
/**
* @class Blocks
@ -12,7 +12,6 @@ import Block from './block';
*
*/
export default class Blocks {
/**
* Get length of Block instances array
*
@ -47,16 +46,27 @@ export default class Blocks {
* blocks[0] = new Block(...)
*
* @param {Blocks} instance Blocks instance
* @param {Number|String} index block index
* @param {Block} block Block to set
* @param {Number|String} property block index or any Blocks class property to set
* @param {Block} value value to set
* @returns {Boolean}
*/
public static set(instance: Blocks, index: number, block: Block) {
if (isNaN(Number(index))) {
return false;
public static set(instance: Blocks, property: number | string, value: Block | any) {
/**
* 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;
}
@ -65,15 +75,22 @@ export default class Blocks {
* Proxy trap to implement array-like getter
*
* @param {Blocks} instance Blocks instance
* @param {Number|String} index Block index
* @param {Number|String} property Blocks class property
* @returns {Block|*}
*/
public static get(instance: Blocks, index: number) {
if (isNaN(Number(index))) {
return instance[index];
public static get(instance: Blocks, property: any | number) {
/**
* 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 {
this.blocks.push(block);
this.workingArea.appendChild(block.holder);
this.insertToDOM(block);
}
/**
@ -145,6 +162,7 @@ export default class Blocks {
if (replace) {
this.blocks[index].holder.remove();
this.blocks[index].call(BlockToolAPI.REMOVED);
}
const deleteCount = replace ? 1 : 0;
@ -154,14 +172,14 @@ export default class Blocks {
if (index > 0) {
const previousBlock = this.blocks[index - 1];
previousBlock.holder.insertAdjacentElement('afterend', block.holder);
this.insertToDOM(block, 'afterend', previousBlock);
} else {
const nextBlock = this.blocks[index + 1];
if (nextBlock) {
nextBlock.holder.insertAdjacentElement('beforebegin', block.holder);
this.insertToDOM(block, 'beforebegin', nextBlock);
} 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].call(BlockToolAPI.REMOVED);
this.blocks.splice(index, 1);
}
@ -184,6 +205,9 @@ export default class Blocks {
*/
public removeAll(): void {
this.workingArea.innerHTML = '';
this.blocks.forEach((block) => block.call(BlockToolAPI.REMOVED));
this.blocks.length = 0;
}
@ -220,4 +244,21 @@ export default class Blocks {
public indexOf(block: Block): number {
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 './utils';
import * as _ from './utils';
import {EditorConfig, OutputData, SanitizerConfig} from '../../types';
import {EditorModules} from '../types-internal/editor-modules';
import {LogLevels} from './utils';
/**
* @typedef {Core} Core - editor core class
@ -75,9 +76,11 @@ export default class Core {
await this.init();
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) {
const {BlockManager, Caret} = this.moduleInstances;
@ -142,6 +145,12 @@ export default class Core {
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
*/
@ -268,7 +277,12 @@ export default class Core {
}),
Promise.resolve(),
);
}
/**
* Render initial data
*/
private render(): Promise<void> {
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
*/
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 {
/**
* 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
*
* @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) ) {
elements.forEach( (el) => parent.appendChild(el) );
} else {
@ -367,6 +370,11 @@ export default class Dom {
return this.isNodeEmpty(node);
}
/**
* Normalize node to merge several text nodes to one to reduce tree walker iterations
*/
node.normalize();
treeWalker.push(node.firstChild);
while ( treeWalker.length > 0 ) {
@ -527,4 +535,16 @@ export default class Dom {
if (typeof element === 'string') { return document.getElementById(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
*/
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;
/**

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import Module from '../../__module';
import {Blocks} from '../../../../types/api';
import {BlockToolData, OutputData, ToolConfig} from '../../../../types';
import _ from './../../utils';
import * as _ from './../../utils';
/**
* @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);
} else {
this.Editor.Caret.navigatePrevious(true);
}
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock, this.Editor.Caret.positions.END);
this.Editor.Toolbar.close();
}

View File

@ -25,6 +25,7 @@ export default class API extends Module {
styles: this.Editor.StylesAPI.classes,
toolbar: this.Editor.ToolbarAPI.methods,
inlineToolbar: this.Editor.InlineToolbarAPI.methods,
tooltip: this.Editor.TooltipAPI.methods,
} 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
*/
import Module from '../__module';
import _ from '../utils';
import * as _ from '../utils';
import SelectionUtils from '../selection';
import Flipper from "../flipper";
import Flipper from '../flipper';
export default class BlockEvents extends Module {
@ -102,63 +102,16 @@ export default class BlockEvents extends Module {
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
*/
UI.checkEmptiness();
this.Editor.UI.checkEmptiness();
}
/**
* Mouse up on Block:
* - shows Inline Toolbar if something selected
*/
public mouseUp(event): 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)();
public mouseUp(): void {
}
/**
@ -223,10 +176,10 @@ export default class BlockEvents extends Module {
this.Editor.Toolbox.close();
} else if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close();
} else if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.close();
} else if (this.Editor.ConversionToolbar.opened) {
this.Editor.ConversionToolbar.close();
} else if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.close();
} else {
this.Editor.Toolbar.close();
}
@ -324,8 +277,9 @@ export default class BlockEvents extends Module {
/**
* 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;
}
@ -420,7 +374,10 @@ export default class BlockEvents extends Module {
}
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) {
/**
@ -481,11 +438,14 @@ export default class BlockEvents extends Module {
* Handle right and down keyboard keys
*/
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
* 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;
}
@ -533,8 +493,12 @@ export default class BlockEvents extends Module {
* Arrows might be handled on toolbars by flipper
* Check for Flipper.usedKeys to allow navigate by UP and disallow by LEFT
*/
if (this.Editor.UI.someToolbarOpened && Flipper.usedKeys.includes(event.keyCode)) {
return;
if (this.Editor.UI.someToolbarOpened) {
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
*/
import Block from '../block';
import Block, {BlockToolAPI} from '../block';
import Module from '../__module';
import $ from '../dom';
import _ from '../utils';
import * as _ from '../utils';
import Blocks from '../blocks';
import {BlockTool, BlockToolConstructable, BlockToolData, PasteEvent, ToolConfig} from '../../../types';
@ -267,7 +267,7 @@ export default class BlockManager extends Module {
}
try {
block.call('onPaste', pasteEvent);
block.call(BlockToolAPI.ON_PASTE, pasteEvent);
} catch (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;
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, 'keyup', (event) => BlockEvents.keyup(event));
Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));

View File

@ -7,7 +7,7 @@
*/
import Module from '../__module';
import Block from '../block';
import _ from '../utils';
import * as _ from '../utils';
import $ from '../dom';
import SelectionUtils from '../selection';
@ -251,6 +251,9 @@ export default class BlockSelection extends Module {
.removeAllRanges();
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();
} else if (this.readyToBlockSelection) {
/**
* prevent default selection when we use custom selection
*/
event.preventDefault();
/**
* select working Block
*/
this.selectBlockByIndex();
/**
@ -331,5 +342,8 @@ export default class BlockSelection extends Module {
.removeAllRanges();
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 Block from '../block';
import $ from '../dom';
import _ from '../utils';
import * as _ from '../utils';
/**
* @typedef {Caret} Caret

View File

@ -1,7 +1,7 @@
import Module from '../__module';
import Block from '../block';
import SelectionUtils from '../selection';
import _ from '../utils';
import * as _ from '../utils';
export default class CrossBlockSelection extends Module {
/**
@ -33,6 +33,14 @@ export default class CrossBlockSelection extends Module {
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
* Used for CBS via Shift + arrow keys
@ -65,6 +73,9 @@ export default class CrossBlockSelection extends Module {
}
this.lastSelectedBlock = nextBlock;
/** close InlineToolbar when Blocks selected */
this.Editor.InlineToolbar.close();
}
/**
@ -151,6 +162,8 @@ export default class CrossBlockSelection extends Module {
return;
}
this.Editor.InlineToolbar.close();
this.toggleBlocksSelectedState(relatedBlock, targetBlock);
this.lastSelectedBlock = targetBlock;
}

View File

@ -36,6 +36,33 @@ export default class Events extends Module {
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
*

View File

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

View File

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

View File

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

View File

@ -166,15 +166,6 @@ export default class RectangleSelection extends Module {
this.startX = 0;
this.startY = 0;
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 _, {ChainData} from '../utils';
import * as _ from '../utils';
import {ChainData} from '../utils';
import {BlockToolData} from '../../../types';
import {BlockToolConstructable} from '../../../types/tools';

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import Module from '../../__module';
import $ from '../../dom';
import {BlockToolConstructable} from '../../../../types';
import _ from '../../utils';
import * as _ from '../../utils';
import {SavedData} from '../../../types-internal/block-data';
import Block from '../../block';
import Flipper from '../../flipper';
@ -18,7 +18,10 @@ export default class ConversionToolbar extends Module {
conversionToolbarWrapper: 'ce-conversion-toolbar',
conversionToolbarShowed: 'ce-conversion-toolbar--showed',
conversionToolbarTools: 'ce-conversion-toolbar__tools',
conversionToolbarLabel: 'ce-conversion-toolbar__label',
conversionTool: 'ce-conversion-tool',
conversionToolHidden: 'ce-conversion-tool--hidden',
conversionToolIcon: 'ce-conversion-tool__icon',
conversionToolFocused : 'ce-conversion-tool--focused',
conversionToolActive : 'ce-conversion-tool--active',
@ -50,13 +53,22 @@ export default class ConversionToolbar extends Module {
*/
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
*/
public make(): void {
public make(): HTMLElement {
this.nodes.wrapper = $.make('div', ConversionToolbar.CSS.conversionToolbarWrapper);
this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);
const label = $.make('div', ConversionToolbar.CSS.conversionToolbarLabel, {
textContent: 'Convert to',
});
/**
* Add Tools that has 'import' method
*/
@ -67,41 +79,53 @@ export default class ConversionToolbar extends Module {
*/
this.enableFlipper();
$.append(this.nodes.wrapper, label);
$.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
* @param {Block} block - block to convert
* Toggle conversion dropdown visibility
* @param {function} [togglingCallback] callback that will accept opening state
*/
public tryToShow(block: Block): void {
const hasExportConfig = block.class.conversionConfig && block.class.conversionConfig.export;
if (!hasExportConfig) {
return;
}
this.move(block);
public toggle(togglingCallback?: (openedState: boolean) => void): void {
if (!this.opened) {
this.open();
} else {
this.close();
}
/**
* Mark current block's button with color
*/
this.highlightActiveTool(block.name);
if (typeof togglingCallback === 'function') {
this.togglingCallback = togglingCallback;
this.togglingCallback(this.opened);
}
}
/**
* Shows Conversion Toolbar
*/
public open(): void {
this.filterTools();
this.opened = true;
this.flipper.activate(Object.values(this.tools));
this.flipper.focusFirst();
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.flipper.deactivate();
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 currentBlockName = this.Editor.BlockManager.currentBlock.name;
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
const { INTERNAL_SETTINGS } = this.Editor.Tools;
const blockData = savedBlock.data;
/**
@ -151,7 +180,7 @@ export default class ConversionToolbar extends Module {
* In both cases returning value must be a string
*/
let exportData: string = '';
const exportProp = currentBlockClass.conversionConfig.export;
const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export;
if (typeof exportProp === 'function') {
exportData = exportProp(blockData);
@ -177,7 +206,7 @@ export default class ConversionToolbar extends Module {
* string the name of data field to import
*/
let newBlockData = {};
const importProp = replacingTool.conversionConfig.import;
const importProp = replacingTool[INTERNAL_SETTINGS.CONVERSION_CONFIG].import;
if (typeof importProp === 'function') {
newBlockData = importProp(cleaned);
@ -193,28 +222,13 @@ export default class ConversionToolbar extends Module {
this.Editor.BlockSelection.clearSelection();
this.close();
this.Editor.InlineToolbar.close();
_.delay(() => {
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
}, 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
* if tools have ability to import
@ -246,18 +260,22 @@ export default class ConversionToolbar extends Module {
continue;
}
this.addTool(toolName, toolToolboxSettings.icon);
this.addTool(toolName, toolToolboxSettings.icon, toolToolboxSettings.title);
}
}
/**
* 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 icon = $.make('div', [ ConversionToolbar.CSS.conversionToolIcon ]);
tool.dataset.tool = toolName;
tool.innerHTML = toolIcon;
icon.innerHTML = toolIcon;
$.append(tool, icon);
$.append(tool, $.text(title || _.capitalize(toolName)));
$.append(this.nodes.tools, 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 {
if (!this.tools[toolName]) {
return;
}
private filterTools(): void {
const { currentBlock } = this.Editor.BlockManager;
/**
* Drop previous active button
* Show previously hided
*/
Object.values(this.tools).forEach((el) => {
el.classList.remove(ConversionToolbar.CSS.conversionToolActive);
Object.entries(this.tools).forEach(([name, button]) => {
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 $ from '../../dom';
import _ from '../../utils';
import * as _ from '../../utils';
/**
*
@ -85,6 +85,7 @@ export default class Toolbar extends Module {
// Content Zone
plusButton: 'ce-toolbar__plus',
plusButtonShortcut: 'ce-toolbar__plus-shortcut',
plusButtonHidden: 'ce-toolbar__plus--hidden',
// Actions Zone
@ -113,33 +114,22 @@ export default class Toolbar extends Module {
* - Toolbox
*/
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
*/
this.Editor.Listeners.on(this.nodes.plusButton, 'mouseenter', () => {
const tooltip = this.Editor.Toolbox.nodes.tooltip;
const fragment = document.createDocumentFragment();
const tooltipContent = $.make('div');
fragment.appendChild(document.createTextNode('Add'));
fragment.appendChild($.make('div', this.Editor.Toolbox.CSS.tooltipShortcut, {
textContent: '⇥ Tab',
}));
tooltipContent.appendChild(document.createTextNode('Add'));
tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {
textContent: '⇥ Tab',
}));
tooltip.style.left = '-17px';
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);
this.Editor.Tooltip.onHover(this.nodes.plusButton, tooltipContent);
/**
* Make a Toolbox
@ -160,6 +150,10 @@ export default class Toolbar extends Module {
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
this.Editor.Tooltip.onHover(this.nodes.settingsToggler, 'Click to tune', {
placement: 'top',
});
/**
* Make and append Settings Panel
*/

View File

@ -2,7 +2,7 @@ import Module from '../../__module';
import $ from '../../dom';
import SelectionUtils from '../../selection';
import _ from '../../utils';
import * as _ from '../../utils';
import {InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings} from '../../../../types';
import Flipper from '../../flipper';
@ -23,12 +23,16 @@ export default class InlineToolbar extends Module {
inlineToolbarShowed: 'ce-inline-toolbar--showed',
inlineToolbarLeftOriented: 'ce-inline-toolbar--left-oriented',
inlineToolbarRightOriented: 'ce-inline-toolbar--right-oriented',
inlineToolbarShortcut: 'ce-inline-toolbar__shortcut',
buttonsWrapper: 'ce-inline-toolbar__buttons',
actionsWrapper: 'ce-inline-toolbar__actions',
inlineToolButton: 'ce-inline-tool',
inlineToolButtonLast: 'ce-inline-tool--last',
inputField: 'cdx-input',
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
*/
private nodes: { wrapper: HTMLElement, buttons: HTMLElement, actions: HTMLElement } = {
private nodes: {
wrapper: HTMLElement,
buttons: HTMLElement,
conversionToggler: HTMLElement,
conversionTogglerContent: HTMLElement,
actions: HTMLElement,
} = {
wrapper: null,
buttons: null,
conversionToggler: null,
conversionTogglerContent: null,
/**
* 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
@ -53,7 +65,7 @@ export default class InlineToolbar extends Module {
/**
* Margin above/below the Toolbar
*/
private readonly toolbarVerticalMargin: number = 20;
private readonly toolbarVerticalMargin: number = 5;
/**
* Tools instances
@ -122,11 +134,18 @@ export default class InlineToolbar extends Module {
$.append(this.nodes.wrapper, [this.nodes.buttons, this.nodes.actions]);
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
/**
* Add button that will allow switching block type
*/
this.addConversionToggler();
/**
* Append Inline Toolbar Tools
*/
this.addTools();
this.prepareConversionToolbar();
/**
* Recalculate initial width with all buttons
*/
@ -163,9 +182,6 @@ export default class InlineToolbar extends Module {
/** Check Tools state for selected fragment */
this.checkToolsState();
/** Clear selection */
this.Editor.BlockSelection.clearSelection();
}
/**
@ -228,6 +244,7 @@ export default class InlineToolbar extends Module {
this.opened = false;
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.opened = true;
/**
* Change Conversion Dropdown content for current tool
*/
this.setConversionTogglerContent();
/**
* Get currently visible buttons to pass it to the Flipper
*/
const visibleTools = Array.from(this.buttonsList)
.filter((tool) => !(tool as HTMLElement).hidden) as HTMLElement[];
let visibleTools = Array.from(this.buttonsList);
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;
}
/**
* 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
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -391,6 +486,7 @@ export default class InlineToolbar extends Module {
const {
Listeners,
Tools,
Tooltip,
} = this.Editor;
const button = tool.render();
@ -448,6 +544,26 @@ export default class InlineToolbar extends Module {
if (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 $ from '../../dom';
import _ from '../../utils';
import * as _ from '../../utils';
import {BlockToolConstructable} from '../../../../types';
import Flipper from '../../flipper';
import {BlockToolAPI} from '../../block';
/**
* @class Toolbox
@ -27,10 +28,10 @@ export default class Toolbox extends Module {
toolboxButton: 'ce-toolbox__button',
toolboxButtonActive : 'ce-toolbox__button--active',
toolboxOpened: 'ce-toolbox--opened',
tooltip: 'ce-toolbox__tooltip',
tooltipShown: 'ce-toolbox__tooltip--shown',
tooltipShortcut: 'ce-toolbox__tooltip-shortcut',
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: {
toolbox: HTMLElement,
tooltip: HTMLElement,
buttons: HTMLElement[],
} = {
toolbox: null,
tooltip: null,
buttons: [],
};
@ -81,7 +80,6 @@ export default class Toolbox extends Module {
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
this.addTools();
this.addTooltip();
this.enableFlipper();
}
@ -116,8 +114,6 @@ export default class Toolbox extends Module {
* Close Toolbox
*/
public close(): void {
this.hideTooltip();
this.nodes.toolbox.classList.remove(this.CSS.toolboxOpened);
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
*/
@ -210,12 +199,11 @@ export default class Toolbox extends Module {
/**
* Add listeners to show/hide toolbox tooltip
*/
this.Editor.Listeners.on(button, 'mouseenter', () => {
this.showTooltip(button, toolName);
});
const tooltipContent = this.drawTooltip(toolName);
this.Editor.Listeners.on(button, 'mouseleave', () => {
this.hideTooltip();
this.Editor.Tooltip.onHover(button, tooltipContent, {
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 {
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 {
private drawTooltip(toolName: string): HTMLElement {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {};
@ -255,48 +233,20 @@ export default class Toolbox extends Module {
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));
fragment.appendChild(hint);
tooltip.appendChild(hint);
if (shortcut) {
const OS = _.getUserOS();
shortcut = _.beautifyShortcut(shortcut);
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');
}
fragment.appendChild($.make('div', this.CSS.tooltipShortcut, {
tooltip.appendChild($.make('div', this.CSS.buttonShortcut, {
textContent: shortcut,
}));
}
const leftOffset = 16;
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);
return tooltip;
}
/**
@ -351,7 +301,7 @@ export default class Toolbox extends Module {
/**
* Apply callback before inserting html
*/
newBlock.call('appendCallback', {});
newBlock.call(BlockToolAPI.APPEND_CALLBACK);
this.Editor.Caret.setToBlock(newBlock);

View File

@ -1,6 +1,6 @@
import Paragraph from '../tools/paragraph/dist/bundle';
import Module from '../__module';
import _ from '../utils';
import * as _ from '../utils';
import {
BlockToolConstructable,
InlineTool,
@ -130,6 +130,7 @@ export default class Tools extends Module {
return {
IS_ENABLED_LINE_BREAKS: 'enableLineBreaks',
IS_INLINE: 'isInline',
TITLE: 'title', // for Inline Tools. Block Tools can pass title along with icon through the 'toolbox' static prop.
SHORTCUT: 'shortcut',
TOOLBOX: 'toolbox',
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 $ from '../dom';
import _ from '../utils';
import * as _ from '../utils';
import Selection from '../selection';
import Block from '../block';
import Flipper from '../flipper';
/**
* @class
@ -161,11 +162,6 @@ export default class UI extends Module {
*/
await this.Editor.InlineToolbar.make();
/**
* Make the Converter tool holder
*/
await this.Editor.ConversionToolbar.make();
/**
* 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
* @return {boolean}
*/
public get someToolbarOpened() {
public get someToolbarOpened(): boolean {
const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
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
*/
@ -204,6 +211,18 @@ export default class UI extends Module {
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
*/
@ -277,17 +296,26 @@ export default class UI extends Module {
(event) => this.redactorClicked(event as MouseEvent),
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, '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) => {
this.selectionChanged(event as Event);
}, true);
}
this.Editor.Listeners.on(document, 'selectionchange', (event: Event) => {
this.selectionChanged(event);
}, true);
this.Editor.Listeners.on(window, 'resize', () => {
this.resizeDebouncer();
@ -374,6 +402,7 @@ export default class UI extends Module {
* 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
*/
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
@ -464,7 +493,6 @@ export default class UI extends Module {
this.Editor.BlockManager.dropPointer();
this.Editor.InlineToolbar.close();
this.Editor.Toolbar.close();
this.Editor.BlockSelection.clearSelection(event);
this.Editor.ConversionToolbar.close();
}
@ -479,44 +507,35 @@ export default class UI extends Module {
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
*
* @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
* First touch on editor
* Fired before click
*
* 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 {
if (!Selection.isCollapsed) {
return;
}
private documentTouched(event: MouseEvent | TouchEvent): void {
let clickedNode = event.target as HTMLElement;
/**
* If click was fired is on Editor`s wrapper, try to get clicked node by elementFromPoint method
*/
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
*/
@ -553,6 +569,25 @@ export default class UI extends Module {
* Hide the Plus Button
*/
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) {
this.Editor.BlockManager.insert();
@ -593,7 +628,7 @@ export default class UI extends Module {
return;
}
this.Editor.InlineToolbar.tryToShow();
this.Editor.InlineToolbar.tryToShow(true);
}
/**

View File

@ -1,7 +1,7 @@
/**
* TextRange interface fot IE9-
*/
import _ from './utils';
import * as _ from './utils';
import $ from './dom';
interface TextRange {
@ -101,24 +101,6 @@ export default class SelectionUtils {
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
* @return {boolean}
@ -194,6 +176,10 @@ export default class SelectionUtils {
return rect;
}
if (sel.rangeCount === 0) {
return rect;
}
range = sel.getRangeAt(0).cloneRange() as Range;
if (range.getBoundingClientRect) {

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

View File

@ -4,6 +4,16 @@
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
*/
@ -22,23 +32,90 @@ export interface ChainData {
/**
* 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;
display: inline-block;
font-size: 11px;
@ -49,341 +126,364 @@ export default class Util {
border: 1px solid rgba(56, 138, 229, 0.16);
margin: 4px 5px 4px 0;`;
try {
if (['time', 'timeEnd'].includes(type)) {
console[type](`( ${editorLabelText} ) ${msg}`);
} else if (args) {
console[type](`%c${editorLabelText}%c ${msg} %o`, editorLabelStyle, style, args);
} else {
console[type](`%c${editorLabelText}%c ${msg}`, editorLabelStyle, style);
}
} catch (ignored) {}
if (labeled) {
if (isSimpleType) {
argsToPass.unshift(editorLabelStyle, style);
msg = `%c${editorLabelText}%c ${msg}`;
} else {
msg = `( ${editorLabelText} )${msg}`;
}
}
/**
* Returns basic keycodes as constants
* @return {{}}
*/
static get keyCodes() {
return {
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,
};
}
try {
if (!isSimpleType) {
console[type](msg);
} else if (args) {
console[type](`${msg} %o`, ...argsToPass);
} else {
console[type](msg, ...argsToPass);
}
} catch (ignored) {}
}
/**
* Return mouse buttons codes
*/
static get mouseButtons() {
return {
LEFT: 0,
WHEEL: 1,
RIGHT: 2,
BACKWARD: 3,
FORWARD: 4,
};
}
/**
* Current log level
*/
_log.logLevel = LogLevels.VERBOSE;
/**
* Returns true if passed key code is printable (a-Z, 0-9, etc) character.
* @param {number} keyCode
* @return {boolean}
*/
public static 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)
}
/**
* Set current log level
*
* @param {LogLevels} logLevel - log level to set
*/
export function setLogLevel(logLevel: LogLevels) {
_log.logLevel = logLevel;
}
/**
* _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 {Function} success - success callback
* @param {Function} fallback - callback that fires in case of errors
* @param {ChainData} chainData
*
* @param {Function} successCallback
* @param {Function} fallbackCallback
*
* @return {Promise}
*/
public static async sequence(
chains: ChainData[],
success: (data: any) => void = () => {},
fallback: (data: any) => void = () => {},
async function waitNextBlock(
chainData: ChainData,
successCallback: (data: any) => void,
fallbackCallback: (data: any) => void,
): Promise<void> {
/**
* Decorator
*
* @param {ChainData} chainData
*
* @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 : {});
}
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
*
* @param {ArrayLike} collection
*
* @return {Array}
* 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
*/
public static array(collection: ArrayLike<any>): any[] {
return Array.prototype.slice.call(collection);
return await chains.reduce(async (previousValue, currentValue) => {
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;
}
/**
* Check if passed variable is a function
* @param {*} fn
* @return {boolean}
*/
public static isFunction(fn: any): boolean {
return typeof fn === 'function';
}
return Object.keys(object).length === 0 && object.constructor === Object;
}
/**
* Check if passed function is a class
* @param {function} fn
* @return {boolean}
*/
public static isClass(fn: any): boolean {
return typeof fn === 'function' && /^\s*class\s+/.test(fn.toString());
}
/**
* Check if passed object is a Promise
* @param {*} object - object to check
* @return {Boolean}
*/
export function isPromise(object: any): boolean {
return Promise.resolve(object) === object;
}
/**
* Checks if object is empty
*
* @param {Object} object
* @return {boolean}
*/
public static isEmpty(object: object): boolean {
if (!object) {
return true;
}
/**
* Delays method execution
*
* @param {Function} method
* @param {Number} timeout
*/
export function delay(method: (...args: any[]) => any, timeout: number) {
return function() {
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
* @param {*} object - object to check
* @return {Boolean}
*/
public static isPromise(object: any): boolean {
return Promise.resolve(object) === object;
}
/**
* Get file extension
*
* @param {File} file
* @return string
*/
export function getFileExtension(file: File): string {
return file.name.split('.').pop();
}
/**
* Delays method execution
*
* @param {Function} method
* @param {Number} timeout
*/
public static delay(method: (...args: any[]) => any, timeout: number) {
return function() {
const context = this,
args = arguments;
/**
* Check if string is MIME type
*
* @param {string} type
* @return boolean
*/
export function isValidMimeType(type: string): boolean {
return /^[-\w]+\/([-+\w]+|\*)$/.test(type);
}
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;
/**
* Get file extension
*
* @param {File} file
* @return string
*/
public static getFileExtension(file: File): string {
return file.name.split('.').pop();
}
return () => {
const context = this,
args = arguments;
/**
* Check if string is MIME type
*
* @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) {
const later = () => {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
}
/**
* Copies passed text to the clipboard
* @param text
*/
public static copyTextToClipboard(text) {
const el = Dom.make('div', 'codex-editor-clipboard', {
innerHTML: text,
});
const callNow = immediate && !timeout;
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}
*/
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;
window.clearTimeout(timeout);
timeout = window.setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
/**
* 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;
}
/**
* Capitalizes first letter of the string
* @param {string} text
* @return {string}
*/
public static capitalize(text: string): string {
return text[0].toUpperCase() + text.slice(1);
}
return OS;
}
/**
* Merge to objects recursively
* @param {object} target
* @param {object[]} sources
* @return {object}
*/
public static deepMerge(target, ...sources) {
const isObject = (item) => item && Util.typeof(item) === 'object';
/**
* Capitalizes first letter of the string
* @param {string} text
* @return {string}
*/
export function capitalize(text: string): string {
return text[0].toUpperCase() + text.slice(1);
}
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)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
}
if (!sources.length) { return target; }
const source = sources.shift();
Util.deepMerge(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
if (isObject(target) && isObject(source)) {
for (const key in source) {
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 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
* @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
*
* @return {boolean}
*/
public static isTouchSupported(): boolean {
return 'ontouchstart' in document.documentElement;
}
/**
* Return string representation of the object type
*
* @param {any} object
*/
public static typeof(object: any): string {
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
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
* @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
*
* @return {boolean}
*/
export const isTouchSupported: boolean = 'ontouchstart' in document.documentElement;
/**
* Return string representation of the object type
*
* @param {any} object
*/
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 {
@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;
visibility: hidden;
will-change: transform, opacity;
transition: transform 150ms ease, opacity 250ms ease;
transform: translateY(8px) scale(0.9);
&::before {
left: 20px;
}
transition: transform 100ms ease, opacity 100ms ease;
transform: translateY(-8px);
left: -1px;
width: 150px;
margin-top: 5px;
box-sizing: content-box;
&--showed {
opacity: 1;
@ -28,21 +24,65 @@
&__buttons {
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 {
@apply --toolbar-button;
line-height: normal;
display: flex;
padding: 5px 10px;
font-size: 14px;
line-height: 20px;
font-weight: 500;
cursor: pointer;
align-items: center;
&:not(:last-of-type) {
margin-right: 2px;
&--hidden {
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 {
margin-right: 0 !important;
}
&:hover,
&--active {
color: var(--color-active-icon) !important;
}

View File

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

View File

@ -1,10 +1,6 @@
.ce-inline-toolbar {
@apply --overlay-pane;
padding: 3px;
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;
visibility: hidden;
transition: transform 150ms ease, opacity 250ms ease;
@ -39,17 +35,67 @@
&__buttons {
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 {
@apply --toolbar-button;
border-radius: 0;
line-height: normal;
width: auto;
padding: 0 5px !important;
min-width: 24px;
&:not(:last-of-type) {
margin-right: 2px;
}
.icon {
height: 12px;
}
&--last {
margin-right: 0 !important;
}
@ -66,20 +112,22 @@
}
.icon--unlink {
display: inline-block;
margin-bottom: -1px;
}
}
&-input {
background-color: var(--bg-light);
outline: none;
border: 0;
border-radius: 3px;
margin: 3px 0 0;
border-radius: 0 0 4px 4px;
margin: 0;
font-size: 13px;
padding: 8px;
padding: 10px;
width: 100%;
box-sizing: border-box;
display: none;
font-weight: 500;
border-top: 1px solid rgba(201,201,204,.48);
&::placeholder {
color: var(--grayText);

View File

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

View File

@ -22,55 +22,15 @@
@apply --toolbox-button;
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

View File

@ -57,25 +57,14 @@
--overlay-pane: {
position: absolute;
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;
z-index: 2;
@media (--mobile){
box-shadow: 0 5px 9px -5px rgba(21, 40, 54, 0.49),6px 15px 34px -6px rgba(33, 48, 73, 0.54);
}
&::before {
content: '';
width: 15px;
height: 15px;
position: absolute;
top: -7px;
left: 50%;
margin-left: -7px;
transform: rotate(-45deg);
background-color: #fff;
z-index: -1;
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;
}
&--left-oriented {
@ -134,7 +123,8 @@
outline: none;
background-color: transparent;
vertical-align: bottom;
color: var(--grayText);
color: #000;
margin: 0;
&:hover {
background-color: var(--bg-light);

View File

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

View File

@ -8,4 +8,5 @@ export * from './styles';
export * from './caret';
export * from './toolbar';
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 {OutputData} from '../index';
import {LogLevels, OutputData} from '../index';
import {SanitizerConfig} from './sanitizer-config';
export interface EditorConfig {
@ -57,6 +57,11 @@ export interface EditorConfig {
*/
minHeight?: number;
/**
* Editors log level (how many logs you want to see)
*/
logLevel?: LogLevels;
/**
* Fires when Editor is ready to work
*/

View File

@ -2,3 +2,4 @@ export * from './editor-config';
export * from './sanitizer-config';
export * from './paste-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 {Blocks, Caret, Events, Listeners, Notifier, Sanitizer, Saver, Selection, Styles, Toolbar, InlineToolbar} from './api';
import {OutputData} from "./data-formats/output-data";
import {
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
@ -34,7 +47,7 @@ export {
FilePasteEventDetail,
} from './tools';
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';
/**
@ -53,6 +66,7 @@ export interface API {
styles: Styles;
toolbar: Toolbar;
inlineToolbar: InlineToolbar;
tooltip: Tooltip;
}
/**
@ -82,37 +96,37 @@ declare class EditorJS {
/**
* @see Saver.save
*/
save(): Promise<OutputData>;
public save(): Promise<OutputData>;
/**
* @see Blocks.clear
*/
clear(): void;
public clear(): void;
/**
* @see Blocks.render
*/
render(data: OutputData): Promise<void>;
public render(data: OutputData): Promise<void>;
/**
* @see Caret.focus
*/
focus(atEnd?: boolean): boolean;
public focus(atEnd?: boolean): boolean;
/**
* @see Events.on
*/
on(eventName: string, callback: (data?: any) => void): void;
public on(eventName: string, callback: (data?: any) => void): void;
/**
* @see Events.off
*/
off(eventName: string, callback: (data?: any) => void): void;
public off(eventName: string, callback: (data?: any) => void): void;
/**
* @see Events.emit
*/
emit(eventName: string, data: any): void;
public emit(eventName: string, data: any): void;
/**
* Destroy Editor instance and related DOM elements

View File

@ -46,6 +46,25 @@ export interface BlockTool extends BaseTool {
* @param {PasteEvent} event
*/
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 {

View File

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

3795
yarn.lock

File diff suppressed because it is too large Load Diff