Release: 2.19 (#1364)

* typo fixed (#1235)

* Improvements: more translations added to the i18n example (#1250)

* Return the result of block.call (#1205)

* [Improvements] ESLint action (#1099)

* TSLint -> ESLint, GitHub Action

* Update eslint.yml

* Autofix

* more autofix

* fix

* manually fix some issues

* Update CHANGELOG.md

* [Refactor] ESLint fixed (#1100)

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

* [Feature] i18n (#1106)

* i18n first steps

* i18n internal, toolbox, api for tools

* namespaced api

* tn, t

* tn in block tunes

* join toolbox and inlineTools under toolNames

* translations

* make enum toolTypes

* Update block.ts

* Update src/components/core.ts

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* add more types

* rm tn

* export i18n types

* upd bundle

* fix tabulation

* Add type-safe namespaces

* upd

* Improve example

* Update toolbox.ts

* improve examplle

* upd

* fix typo

* Add comments for complex types

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>

* Remove unused submodule

* Fixed: icon centering in Firefox

* Do not load styles twice (#1112)

* Do not load styles twice

* Add changelog

* Fix issue link

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

* Show warning if Block to delete is not found (#1111)

Resolves #1102

* Save Tools' order in the Toolbox (#1113)

Resolves #1073

* fix $.isEmpty performance (#1096)

* fix $.isEmpty performance

* add changelog

* upd bundle

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

* Add issue templates (#1114)

* Update issue templates (#1121)

* Update issue templates

* Apply suggestions from code review

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* upd texts

* Update feature_request.md

* Update .github/ISSUE_TEMPLATE/discussion.md

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>

* Allowing deleting block by block id (#1108)

* Allowing deleting block by block id

* Fixed no argument error

* Making index value optional for delete operation

* Added to changelog

* Making index value optional for delete operation

* Added parameter description

* Update docs/CHANGELOG.md

* Update types/api/blocks.d.ts

* Update editor.js

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

* Allow navigate next from last non-initial block (#1110)

Resolves #1103

* Create CODE_OF_CONDUCT.md (#1171)

* Create CODE_OF_CONDUCT.md

* Update changelog file

* Update dependencies (#1122)

* Update dependencies

* upd codex.tooltip

* Update editor.js.LICENSE.txt

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

* Feature/disable tab event config (#1164)

* Highlight first block on autofocus (#1127)

* Fix shortcut for external tools (#1141)

* fix/shortcut-for-external-tools

* Check inline tools property for shortcut

Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* Hotfix/issue1133 selection shortcut removed on editor destroy (#1140)

* Removed shortcut CMD+A on editor destroy #1133

* Removed patch version and made code cleaner #1133

* lint error fixes #1133

Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* [Feature] BlockAPI Interface (#1075)

* Fix BlockManager.insert method (#1172)

* Fix BlockManager.insert method

* upd

* Explicitly check for undefined

* Update tools master branches (#1180)

* Update master branches

* Update image

* Update CHANGELOG.md

* Fix behaviour of inputs editing in block settings (#1123)

* lint code

* Update CHANGELOG.md

* Return the result of block.call

This change allows blocks to return the result of `call` methods, thus allowing them to expose arbitrary data as needed.

My particular use case is I am using Vue to mount components inside of the larger editorjs framework. One of the components that we are developing can be thought of as a nested agenda, where labels need to be in an order like:

```
I. Top level
  a. second level
    i. third level
```

My plan is to have an orchestrator query all blocks, filter those that need labels prepended, and then programmatically tell each block (with another `call` method) to set its depth to the desired level. At that point, Vue can reactively update any labels, etc. that are needed.

I believe this change will allow for other such uses, and I imagine it should not break any existing code since it was returning `null` before.

* Disable ESLint for call method return value

Because we are returning the value of an arbitrary function, the return value can be anything (hence, the return type must be `any`). However, to reduce noise in ESLint output, we disable ESLint checking the line with the `any` type return.

* Change any type to unknown and add to CHANGELOG.md

Change any type of the call method to unknown but eslint shows error
saying the unknown type is undefined, Also, add the chnage to
CHANGELOG.md as an improvement with the link to the PR itself as no
issue was assigned with it.

* Add unknown to eslint globals

* upd

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
Co-authored-by: tasuku-s <tasuku@freemind.co.jp>
Co-authored-by: Athul Anil Kumar <athul7744@outlook.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
Co-authored-by: flaming-cl <51183663+flaming-cl@users.noreply.github.com>
Co-authored-by: Nguyen Ngoc Son <sonnn.se@gmail.com>
Co-authored-by: Sisir Das K <37764463+sis-dk@users.noreply.github.com>
Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: ranemihir <mihirrane171@gmail.com>

* <fix> toolbar--opened overlap with certain text [issue 1196] (#1201)

* [Improvements] ESLint action (#1099)

* TSLint -> ESLint, GitHub Action

* Update eslint.yml

* Autofix

* more autofix

* fix

* manually fix some issues

* Update CHANGELOG.md

* [Refactor] ESLint fixed (#1100)

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

* [Feature] i18n (#1106)

* i18n first steps

* i18n internal, toolbox, api for tools

* namespaced api

* tn, t

* tn in block tunes

* join toolbox and inlineTools under toolNames

* translations

* make enum toolTypes

* Update block.ts

* Update src/components/core.ts

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* add more types

* rm tn

* export i18n types

* upd bundle

* fix tabulation

* Add type-safe namespaces

* upd

* Improve example

* Update toolbox.ts

* improve examplle

* upd

* fix typo

* Add comments for complex types

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>

* Remove unused submodule

* Fixed: icon centering in Firefox

* Do not load styles twice (#1112)

* Do not load styles twice

* Add changelog

* Fix issue link

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

* Show warning if Block to delete is not found (#1111)

Resolves #1102

* Save Tools' order in the Toolbox (#1113)

Resolves #1073

* fix $.isEmpty performance (#1096)

* fix $.isEmpty performance

* add changelog

* upd bundle

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

* Add issue templates (#1114)

* Update issue templates (#1121)

* Update issue templates

* Apply suggestions from code review

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* upd texts

* Update feature_request.md

* Update .github/ISSUE_TEMPLATE/discussion.md

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>

* Allowing deleting block by block id (#1108)

* Allowing deleting block by block id

* Fixed no argument error

* Making index value optional for delete operation

* Added to changelog

* Making index value optional for delete operation

* Added parameter description

* Update docs/CHANGELOG.md

* Update types/api/blocks.d.ts

* Update editor.js

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

* Allow navigate next from last non-initial block (#1110)

Resolves #1103

* Create CODE_OF_CONDUCT.md (#1171)

* Create CODE_OF_CONDUCT.md

* Update changelog file

* Update dependencies (#1122)

* Update dependencies

* upd codex.tooltip

* Update editor.js.LICENSE.txt

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

* Feature/disable tab event config (#1164)

* Highlight first block on autofocus (#1127)

* Fix shortcut for external tools (#1141)

* fix/shortcut-for-external-tools

* Check inline tools property for shortcut

Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* Hotfix/issue1133 selection shortcut removed on editor destroy (#1140)

* Removed shortcut CMD+A on editor destroy #1133

* Removed patch version and made code cleaner #1133

* lint error fixes #1133

Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* [Feature] BlockAPI Interface (#1075)

* Fix BlockManager.insert method (#1172)

* Fix BlockManager.insert method

* upd

* Explicitly check for undefined

* Update tools master branches (#1180)

* Update master branches

* Update image

* Update CHANGELOG.md

* Fix behaviour of inputs editing in block settings (#1123)

* lint code

* Update CHANGELOG.md

* <fix> toolbar overlap with text

* Add Fix in CHANGELOG.md

* Update editor.js

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
Co-authored-by: tasuku-s <tasuku@freemind.co.jp>
Co-authored-by: Athul Anil Kumar <athul7744@outlook.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
Co-authored-by: Nguyen Ngoc Son <sonnn.se@gmail.com>
Co-authored-by: Sisir Das K <37764463+sis-dk@users.noreply.github.com>
Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: ranemihir <mihirrane171@gmail.com>

* Rename initialBlock to defaultBlock (#1209)

* [Improvements] ESLint action (#1099)

* TSLint -> ESLint, GitHub Action

* Update eslint.yml

* Autofix

* more autofix

* fix

* manually fix some issues

* Update CHANGELOG.md

* [Refactor] ESLint fixed (#1100)

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

* [Feature] i18n (#1106)

* i18n first steps

* i18n internal, toolbox, api for tools

* namespaced api

* tn, t

* tn in block tunes

* join toolbox and inlineTools under toolNames

* translations

* make enum toolTypes

* Update block.ts

* Update src/components/core.ts

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* add more types

* rm tn

* export i18n types

* upd bundle

* fix tabulation

* Add type-safe namespaces

* upd

* Improve example

* Update toolbox.ts

* improve examplle

* upd

* fix typo

* Add comments for complex types

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>

* Remove unused submodule

* Fixed: icon centering in Firefox

* Do not load styles twice (#1112)

* Do not load styles twice

* Add changelog

* Fix issue link

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

* Show warning if Block to delete is not found (#1111)

Resolves #1102

* Save Tools' order in the Toolbox (#1113)

Resolves #1073

* fix $.isEmpty performance (#1096)

* fix $.isEmpty performance

* add changelog

* upd bundle

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

* Add issue templates (#1114)

* Update issue templates (#1121)

* Update issue templates

* Apply suggestions from code review

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* upd texts

* Update feature_request.md

* Update .github/ISSUE_TEMPLATE/discussion.md

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>

* Allowing deleting block by block id (#1108)

* Allowing deleting block by block id

* Fixed no argument error

* Making index value optional for delete operation

* Added to changelog

* Making index value optional for delete operation

* Added parameter description

* Update docs/CHANGELOG.md

* Update types/api/blocks.d.ts

* Update editor.js

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

* Allow navigate next from last non-initial block (#1110)

Resolves #1103

* Create CODE_OF_CONDUCT.md (#1171)

* Create CODE_OF_CONDUCT.md

* Update changelog file

* Update dependencies (#1122)

* Update dependencies

* upd codex.tooltip

* Update editor.js.LICENSE.txt

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

* Feature/disable tab event config (#1164)

* Highlight first block on autofocus (#1127)

* Fix shortcut for external tools (#1141)

* fix/shortcut-for-external-tools

* Check inline tools property for shortcut

Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* Hotfix/issue1133 selection shortcut removed on editor destroy (#1140)

* Removed shortcut CMD+A on editor destroy #1133

* Removed patch version and made code cleaner #1133

* lint error fixes #1133

Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* [Feature] BlockAPI Interface (#1075)

* Fix BlockManager.insert method (#1172)

* Fix BlockManager.insert method

* upd

* Explicitly check for undefined

* Update tools master branches (#1180)

* Update master branches

* Update image

* Update CHANGELOG.md

* Fix behaviour of inputs editing in block settings (#1123)

* lint code

* Update CHANGELOG.md

* Rename initialBlock to defaultBlock
Closes #993

The initialBlock property is renamed to defaultBlock.

* Change keyword 'InitialBlock' to 'DefaultBlock' in all methods
Fixes #993

All the methods using the keyword 'Initial' or 'initial' for initial block
are replace with 'Default' or 'default'.
For example, the Tools.isIntitial() method is changed to Tools.isDefault().

* Keep initialBlock and defaultBlock both.

initialBlock property is still kept but it will deprecated in the
next major release.

* Change defaultBlock in example.html and rebuild.

* Remove package-lock.json file.

* Update docs/tools.md

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

* Update example/example-dev.html

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

* Update example/example.html

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

* Update example/example-dev.html

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

* Update example/example.html

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

* Update src/components/utils.ts

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

* Update src/components/utils.ts

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

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

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

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

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

* Update src/components/utils.ts

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

* Fix needAddDefaultBlock to needToAddDefaultBlock

* Add as an Improvement to CHANGELOG.md

* Delete editor.js.map

* fix log, rename some more places

* Update example.html

* Update blockManager.ts

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
Co-authored-by: tasuku-s <tasuku@freemind.co.jp>
Co-authored-by: Athul Anil Kumar <athul7744@outlook.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
Co-authored-by: flaming-cl <51183663+flaming-cl@users.noreply.github.com>
Co-authored-by: Nguyen Ngoc Son <sonnn.se@gmail.com>
Co-authored-by: Sisir Das K <37764463+sis-dk@users.noreply.github.com>
Co-authored-by: Sisir <sisir@hellosivi.com>

* Fix blocks.delete with undefined index (#1182) (#1218)

* [Improvements] ESLint action (#1099)

* TSLint -> ESLint, GitHub Action

* Update eslint.yml

* Autofix

* more autofix

* fix

* manually fix some issues

* Update CHANGELOG.md

* [Refactor] ESLint fixed (#1100)

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

* [Feature] i18n (#1106)

* i18n first steps

* i18n internal, toolbox, api for tools

* namespaced api

* tn, t

* tn in block tunes

* join toolbox and inlineTools under toolNames

* translations

* make enum toolTypes

* Update block.ts

* Update src/components/core.ts

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* add more types

* rm tn

* export i18n types

* upd bundle

* fix tabulation

* Add type-safe namespaces

* upd

* Improve example

* Update toolbox.ts

* improve examplle

* upd

* fix typo

* Add comments for complex types

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>

* Remove unused submodule

* Fixed: icon centering in Firefox

* Do not load styles twice (#1112)

* Do not load styles twice

* Add changelog

* Fix issue link

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

* Show warning if Block to delete is not found (#1111)

Resolves #1102

* Save Tools' order in the Toolbox (#1113)

Resolves #1073

* fix $.isEmpty performance (#1096)

* fix $.isEmpty performance

* add changelog

* upd bundle

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

* Add issue templates (#1114)

* Update issue templates (#1121)

* Update issue templates

* Apply suggestions from code review

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* upd texts

* Update feature_request.md

* Update .github/ISSUE_TEMPLATE/discussion.md

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>

* Allowing deleting block by block id (#1108)

* Allowing deleting block by block id

* Fixed no argument error

* Making index value optional for delete operation

* Added to changelog

* Making index value optional for delete operation

* Added parameter description

* Update docs/CHANGELOG.md

* Update types/api/blocks.d.ts

* Update editor.js

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

* Allow navigate next from last non-initial block (#1110)

Resolves #1103

* Create CODE_OF_CONDUCT.md (#1171)

* Create CODE_OF_CONDUCT.md

* Update changelog file

* Update dependencies (#1122)

* Update dependencies

* upd codex.tooltip

* Update editor.js.LICENSE.txt

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

* Feature/disable tab event config (#1164)

* Highlight first block on autofocus (#1127)

* Fix shortcut for external tools (#1141)

* fix/shortcut-for-external-tools

* Check inline tools property for shortcut

Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* Hotfix/issue1133 selection shortcut removed on editor destroy (#1140)

* Removed shortcut CMD+A on editor destroy #1133

* Removed patch version and made code cleaner #1133

* lint error fixes #1133

Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* [Feature] BlockAPI Interface (#1075)

* Fix BlockManager.insert method (#1172)

* Fix BlockManager.insert method

* upd

* Explicitly check for undefined

* Update tools master branches (#1180)

* Update master branches

* Update image

* Update CHANGELOG.md

* Fix behaviour of inputs editing in block settings (#1123)

* lint code

* Update CHANGELOG.md

* fix: blocks.delete with undefined index (#1182)

* Add as a Fix in CHANGELOG.md.

* Update editor.js

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
Co-authored-by: tasuku-s <tasuku@freemind.co.jp>
Co-authored-by: Athul Anil Kumar <athul7744@outlook.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
Co-authored-by: flaming-cl <51183663+flaming-cl@users.noreply.github.com>
Co-authored-by: Nguyen Ngoc Son <sonnn.se@gmail.com>
Co-authored-by: Sisir Das K <37764463+sis-dk@users.noreply.github.com>
Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: ranemihir <mihirrane171@gmail.com>

* Fix spam clicking the tune button in Firefox  (#1285)

* Fix spam cliclikng tune in Firefox #1273

* build

* Disabled unwanted I18n messages (#1282)

* The unwanted I18n messages from console is disabled

* Update docs/CHANGELOG.md

Improved Change log

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

* Remove import statement

import * as _ from '../utils';
removed

* Apply suggestions from code review

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

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

* Move SavedData and ValidatedData interfaces from internal types (#1251)

* Move SavedData and ValidatedData interfaces from internal types

* Add changelog

* Upd submodules (#1287)

* upd modules

* Revert "upd modules"

This reverts commit e2ff850d9d.

* upd modules

* Tools destroy called when the editor is destroyed (#1264)

* Tools destroy called when the editor is destroyed

When the editor instance is destroyed, it calls the destroy of the blockManager. blockManager inturn calls destroy of all the blocks that it manages.

* Fixed lint errors

* Use Prmoise.all and add as a Fix in CHANGELOG.md

* Fix commit

* Fix CHANGELOG.md

* Add call of Tools reset methods

* Update tools

* Update changelog

* Update docs/CHANGELOG.md

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

* upd all

* bundle

* upd tools

Co-authored-by: ranemihir <mihirrane171@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Bump elliptic from 6.5.2 to 6.5.3 (#1257)

Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix for input and textarea bug (#1214)

* [Improvements] ESLint action (#1099)

* TSLint -> ESLint, GitHub Action

* Update eslint.yml

* Autofix

* more autofix

* fix

* manually fix some issues

* Update CHANGELOG.md

* [Refactor] ESLint fixed (#1100)

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

* [Feature] i18n (#1106)

* i18n first steps

* i18n internal, toolbox, api for tools

* namespaced api

* tn, t

* tn in block tunes

* join toolbox and inlineTools under toolNames

* translations

* make enum toolTypes

* Update block.ts

* Update src/components/core.ts

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* add more types

* rm tn

* export i18n types

* upd bundle

* fix tabulation

* Add type-safe namespaces

* upd

* Improve example

* Update toolbox.ts

* improve examplle

* upd

* fix typo

* Add comments for complex types

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>

* Remove unused submodule

* Fixed: icon centering in Firefox

* Do not load styles twice (#1112)

* Do not load styles twice

* Add changelog

* Fix issue link

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

* Show warning if Block to delete is not found (#1111)

Resolves #1102

* Save Tools' order in the Toolbox (#1113)

Resolves #1073

* fix $.isEmpty performance (#1096)

* fix $.isEmpty performance

* add changelog

* upd bundle

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

* Add issue templates (#1114)

* Update issue templates (#1121)

* Update issue templates

* Apply suggestions from code review

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* upd texts

* Update feature_request.md

* Update .github/ISSUE_TEMPLATE/discussion.md

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>

* Allowing deleting block by block id (#1108)

* Allowing deleting block by block id

* Fixed no argument error

* Making index value optional for delete operation

* Added to changelog

* Making index value optional for delete operation

* Added parameter description

* Update docs/CHANGELOG.md

* Update types/api/blocks.d.ts

* Update editor.js

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

* Allow navigate next from last non-initial block (#1110)

Resolves #1103

* Create CODE_OF_CONDUCT.md (#1171)

* Create CODE_OF_CONDUCT.md

* Update changelog file

* Update dependencies (#1122)

* Update dependencies

* upd codex.tooltip

* Update editor.js.LICENSE.txt

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

* Feature/disable tab event config (#1164)

* Highlight first block on autofocus (#1127)

* Fix shortcut for external tools (#1141)

* fix/shortcut-for-external-tools

* Check inline tools property for shortcut

Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* Hotfix/issue1133 selection shortcut removed on editor destroy (#1140)

* Removed shortcut CMD+A on editor destroy #1133

* Removed patch version and made code cleaner #1133

* lint error fixes #1133

Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* [Feature] BlockAPI Interface (#1075)

* Fix BlockManager.insert method (#1172)

* Fix BlockManager.insert method

* upd

* Explicitly check for undefined

* Update tools master branches (#1180)

* Update master branches

* Update image

* Update CHANGELOG.md

* Fix behaviour of inputs editing in block settings (#1123)

* lint code

* Update CHANGELOG.md

* added handling of inputs and textareas in custom plugins

* Upd tools

* Add changelog

* Upd submodules

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
Co-authored-by: tasuku-s <tasuku@freemind.co.jp>
Co-authored-by: Athul Anil Kumar <athul7744@outlook.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
Co-authored-by: flaming-cl <51183663+flaming-cl@users.noreply.github.com>
Co-authored-by: Nguyen Ngoc Son <sonnn.se@gmail.com>
Co-authored-by: Sisir Das K <37764463+sis-dk@users.noreply.github.com>
Co-authored-by: Sisir <sisir@hellosivi.com>

* Typos changes required to be fixed on website when using the import concept (#1260)

* Typos changes.

Required to fix them too on the official documentation website

* Update README.md

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>

* Use only import

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* Add hidden option to toolbox (#1220)

* Add hidden option to toolbox

* Use false in order to hide toolbox

* Add comment what false means

* Add issue #1221 to changelog

Co-authored-by: t.hata <hata@impact-blue.co.jp>

* Add RTL support (#1248)

* [Improvements] ESLint action (#1099)

* TSLint -> ESLint, GitHub Action

* Update eslint.yml

* Autofix

* more autofix

* fix

* manually fix some issues

* Update CHANGELOG.md

* [Refactor] ESLint fixed (#1100)

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

* [Feature] i18n (#1106)

* i18n first steps

* i18n internal, toolbox, api for tools

* namespaced api

* tn, t

* tn in block tunes

* join toolbox and inlineTools under toolNames

* translations

* make enum toolTypes

* Update block.ts

* Update src/components/core.ts

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* add more types

* rm tn

* export i18n types

* upd bundle

* fix tabulation

* Add type-safe namespaces

* upd

* Improve example

* Update toolbox.ts

* improve examplle

* upd

* fix typo

* Add comments for complex types

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>

* Remove unused submodule

* Fixed: icon centering in Firefox

* Do not load styles twice (#1112)

* Do not load styles twice

* Add changelog

* Fix issue link

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

* Show warning if Block to delete is not found (#1111)

Resolves #1102

* Save Tools' order in the Toolbox (#1113)

Resolves #1073

* fix $.isEmpty performance (#1096)

* fix $.isEmpty performance

* add changelog

* upd bundle

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

* Add issue templates (#1114)

* Update issue templates (#1121)

* Update issue templates

* Apply suggestions from code review

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* upd texts

* Update feature_request.md

* Update .github/ISSUE_TEMPLATE/discussion.md

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>

* Allowing deleting block by block id (#1108)

* Allowing deleting block by block id

* Fixed no argument error

* Making index value optional for delete operation

* Added to changelog

* Making index value optional for delete operation

* Added parameter description

* Update docs/CHANGELOG.md

* Update types/api/blocks.d.ts

* Update editor.js

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

* Allow navigate next from last non-initial block (#1110)

Resolves #1103

* Create CODE_OF_CONDUCT.md (#1171)

* Create CODE_OF_CONDUCT.md

* Update changelog file

* Update dependencies (#1122)

* Update dependencies

* upd codex.tooltip

* Update editor.js.LICENSE.txt

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

* Feature/disable tab event config (#1164)

* Highlight first block on autofocus (#1127)

* Fix shortcut for external tools (#1141)

* fix/shortcut-for-external-tools

* Check inline tools property for shortcut

Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* Hotfix/issue1133 selection shortcut removed on editor destroy (#1140)

* Removed shortcut CMD+A on editor destroy #1133

* Removed patch version and made code cleaner #1133

* lint error fixes #1133

Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>

* [Feature] BlockAPI Interface (#1075)

* Fix BlockManager.insert method (#1172)

* Fix BlockManager.insert method

* upd

* Explicitly check for undefined

* Update tools master branches (#1180)

* Update master branches

* Update image

* Update CHANGELOG.md

* Fix behaviour of inputs editing in block settings (#1123)

* lint code

* Update CHANGELOG.md

* Added RTL support

* Fixed code style

* Fixed icons positioning in the toolbar in the RTL mode

* Renamed example-dev-rtl.html to example-rtl.html

* Moved 'direction' option to 'i18n' section

* Fixed an issue with arrow navigation between blocks

* Renamed rtl-fix to codex-editor--rtl

* Fixed icons positioning in the narrow mode for RTL

* Replaced 'isRtl' method with getter

* Fixed bug with the editor initialization when 'i18n' option is not set

* narrow mode improved

* Changelog added

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
Co-authored-by: tasuku-s <tasuku@freemind.co.jp>
Co-authored-by: Athul Anil Kumar <athul7744@outlook.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
Co-authored-by: flaming-cl <51183663+flaming-cl@users.noreply.github.com>
Co-authored-by: Nguyen Ngoc Son <sonnn.se@gmail.com>
Co-authored-by: Sisir Das K <37764463+sis-dk@users.noreply.github.com>
Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: ImangazalievM <mahach.miangazaliev@gmail.com>

* Fix i18n default configuration (#1290)

* Fix i18n default configuration

* update bundle

* Fixing Bug #1270 and resolve all yarn lint warning. (#1292)

* Fixing Bug #1270 and resolve all yarn lint warning.

* Update CHANGELOG.md

* Change the Log type from Error to Warn

* upd types

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

* Stop click propagation only if click cause action (#1252)

* Fixing: #843 problem with onchange callback (#1310)

* Fixing: #843 problem with onchange callback

* Update docs/CHANGELOG.md

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

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

* The read-only mode (#1035)

(ノ◕ヮ◕)ノ*:・゚✧

* Update submodules (#1335)

* Add inlineToolbar property (#1293)

* Add inlineToolbar property

* Fix lint errors

* Fix comments

Co-authored-by: Murod Khaydarov <murod.haydarov@inbox.ru>

* Sort Tools Working, Can be optimized further

* Fix dataset error and use children

* Fix lint errors

* Add as improvement to CHNAGELOG.md

* Fix comments

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

* Add comments and small fixes

* Fix lint errors

* Fix sortTools() and check inlineToolbar property

* Fix lint errors

* Fix conditions and property names

* Separate block toggler from buttons list in ui

* Fix lint errors

* Fix condition names in allowedToShow()

* Minor bug fixes

* Fix linter warnings

* Update docs/CHANGELOG.md

Co-authored-by: Murod Khaydarov <murod.haydarov@inbox.ru>

* create inlineToolbarSettings() method

* Minor fixes

* Clearify boolean casting

* upd bundle

* fix getInlineToolbarSettings

* refactor & create new instance every showing

* remove unused codee

Co-authored-by: Murod Khaydarov <murod.haydarov@inbox.ru>
Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Throw error only if read-only is enabled from the start (#1337)

* Throw error only if read-only is enabled from the start

* update modules

* Fixed the 1302 bug and improve the tab key behaviour (#1342)

* Fixed the 1302 bug and improve the tab key behaviour

* yarn lint:fixed based improvements

* Update docs/CHANGELOG.md

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

* Update src/components/modules/ui.ts

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

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

* Fix caret behaviour (#1343)

* Fix caret behaviour

* Fix current input update

* Toggle readonly on start (#1344)

* Toggle readonly on start

* Do not render block twice on start

* Bugfix/fix modification observer disable (#1340)

* Enable modification observer when onChange callback throws an error

* Build

* Update src/components/modules/modificationsObserver.ts

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>

* Update CHANGELOG

Co-authored-by: t.hata <hata@impact-blue.co.jp>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>

* Improve the changelog and read-only toggler (#1347)

* Use activeElement if anchorNode is undefined (#1350)

* FIx errors on enter press when several blocks selected (#1349)

* FIx errors on enter press when several blocks selected

* Fix for safari

* Fix blocks copy in read-only (#1351)

* Fixes for 2.19 (#1356)

* Fixes

* Update docs/CHANGELOG.md

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

* Fix RTL

* Optimize tune down

* Add explanation on focus events listeners

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

* Fixes due code review (#1365)

* Fixes for release: (#1366)

* Fixes for release

* Apply suggestions from code review

Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>

* Update toolbox.ts

Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>

Co-authored-by: Qays <whosqays@gmail.com>
Co-authored-by: Jacob Smith <jacob.wesley.smith@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
Co-authored-by: tasuku-s <tasuku@freemind.co.jp>
Co-authored-by: Athul Anil Kumar <athul7744@outlook.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
Co-authored-by: flaming-cl <51183663+flaming-cl@users.noreply.github.com>
Co-authored-by: Nguyen Ngoc Son <sonnn.se@gmail.com>
Co-authored-by: Sisir Das K <37764463+sis-dk@users.noreply.github.com>
Co-authored-by: Sisir <sisir@hellosivi.com>
Co-authored-by: ranemihir <mihirrane171@gmail.com>
Co-authored-by: Mihir Rane <66768874+ranemihir@users.noreply.github.com>
Co-authored-by: Stephen Richard <stephen.richard44@gmail.com>
Co-authored-by: Umang G. Patel <23169768+robonetphy@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikola Pavlovic <47178050+PavlovicWorkCo@users.noreply.github.com>
Co-authored-by: Cyber_Ninja <49983428+Gicehajunior@users.noreply.github.com>
Co-authored-by: Tomoyuki Hata <hato6502@gmail.com>
Co-authored-by: t.hata <hata@impact-blue.co.jp>
Co-authored-by: Mahach Imangazaliev <mahach.imangazaliev@mail.ru>
Co-authored-by: ImangazalievM <mahach.miangazaliev@gmail.com>
Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>
Co-authored-by: Hugh Boylan <bluehugh2@gmail.com>
Co-authored-by: Murod Khaydarov <murod.haydarov@inbox.ru>
This commit is contained in:
Peter Savchenko 2020-10-13 20:50:46 +03:00 committed by GitHub
parent b223d63c59
commit 29db87525b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 2387 additions and 801 deletions

View file

@ -40,6 +40,7 @@
"DataTransfer": true,
"DOMRect": true,
"ClientRect": true,
"ArrayLike": true
"ArrayLike": true,
"unknown": true
}
}

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 "example/tools/underline"]
path = example/tools/underline
url = https://github.com/editor-js/underline

View file

@ -4,7 +4,7 @@
[![](https://flat.badgen.net/bundlephobia/min/@editorjs/editorjs?color=cyan)](https://www.npmjs.com/package/@editorjs/editorjs)
[![](https://flat.badgen.net/bundlephobia/minzip/@editorjs/editorjs?color=green)](https://www.npmjs.com/package/@editorjs/editorjs)
[![Backers on Open Collective](https://opencollective.com/editorjs/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/editorjs/sponsors/badge.svg)](#sponsors)
[![Sponsors on Open Collective](https://opencollective.com/editorjs/sponsors/badge.svg)](#sponsors)
[![](https://flat.badgen.net/npm/license/@editorjs/editorjs)](https://www.npmjs.com/package/@editorjs/editorjs)
[![Join the chat at https://gitter.im/codex-team/editor.js](https://badges.gitter.im/codex-team/editor.js.svg)](https://gitter.im/codex-team/editor.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@ -17,24 +17,24 @@
If you like Editor.js you can support project improvements and development of new features with a donation to our collective.
👉 [https://opencollective.com/editorjs](https://opencollective.com/editorjs)
### Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/editorjs#sponsor)]
<a href="https://opencollective.com/editorjs/sponsor/0/website" target="_blank"><img src="https://opencollective.com/editorjs/sponsor/0/avatar.svg"></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/editorjs#backer)]
<a href="https://opencollective.com/editorjs#backers" target="_blank"><img src="https://opencollective.com/editorjs/backers.svg?width=890"></a>
### Contributors
This project exists thanks to all the people who contribute. <img src="https://opencollective.com/editorjs/contributors.svg?width=890&button=false" />
We really welcome new contributors. If you want to make some code with us, please take a look at the [Good First Tasks](https://github.com/codex-team/editor.js/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+task%22). You can write to us on `team@codex.so` or via special [Telegram chat](https://t.me/editorjsdev), or any other way.
We really welcome new contributors. If you want to make some code with us, please take a look at the [Good First Tasks](https://github.com/codex-team/editor.js/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+task%22). You can write to us on `team@codex.so` or via special [Telegram chat](https://t.me/editorjsdev), or any other way.
## Documentation
@ -142,7 +142,7 @@ npm i @editorjs/editorjs
Include module in your application
```javascript
const EditorJS = require('@editorjs/editorjs');
import EditorJS from '@editorjs/editorjs';
```
##### Option B. Use a CDN
@ -240,11 +240,11 @@ Take a look at the [example.html](example/example.html) to view more detailed ex
## Credits and references
- We use [HTMLJanitor](https://github.com/guardian/html-janitor) module in our Sanitizer module.
- We use [HTMLJanitor](https://github.com/guardian/html-janitor) module in our Sanitizer module.
## About team
We are CodeX and we build products for developers and makers.
We are CodeX and we build products for developers and makers.
Follow us on Twitter: [twitter.com/codex_team](https://twitter.com/codex_team)

2
dist/editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,29 @@
# Changelog
### 2.19
- `New` - Read-only mode 🥳 [#837](https://github.com/codex-team/editor.js/issues/837)
- `New` - RTL mode added [#670](https://github.com/codex-team/editor.js/issues/670)
- `New` - Allows users to provide common `inlineToolbar` property which will be used for all tools whose `inlineToolbar` property is set to `true`. It can be overridden by the tool's own `inlineToolbar` property. Also, inline tools will be ordered according to the order of the inline tools in array provided in the `inlineToolbar` property. [#1056](https://github.com/codex-team/editor.js/issues/1056)
- `New` - Tool's `reset` static method added to the API to clean up any data added by Tool on initialization
- `Improvements` - The `initialBlock` property of Editor config is deprecated. Use the `defaultBlock` instead. [#993](https://github.com/codex-team/editor.js/issues/993)
- `Improvements` - BlockAPI `call()` method now returns the result of calling method, thus allowing it to expose arbitrary data as needed [#1205](https://github.com/codex-team/editor.js/pull/1205)
- `Improvements` - Unuseful log about missed i18n section has been removed [#1269](https://github.com/codex-team/editor.js/issues/1269)
- `Improvements` - Allowed to set `false` as `toolbox` config in order to hide Toolbox button [#1221](https://github.com/codex-team/editor.js/issues/1221)
- `Fix` — Fix problem with types usage [#1183](https://github.com/codex-team/editor.js/issues/1183)
- `Fix` - Fixed issue with Spam clicking the "Click to tune" button duplicates the icons on FireFox. [#1273](https://github.com/codex-team/editor.js/issues/1273)
- `Fix` - Fixed issue with `editor.blocks.delete(index)` method which throws an error when Editor.js is not focused, even after providing a valid index. [#1182](https://github.com/codex-team/editor.js/issues/1182)
- `Fix` - Fixed the issue of toolbar not disappearing on entering input in Chinese, Hindi and some other languages. [#1196](https://github.com/codex-team/editor.js/issues/1196)
- `Fix` - Do not stop events propagation if not needed (essential for React synthetic events) [#1051](https://github.com/codex-team/editor.js/issues/1051) [#946](https://github.com/codex-team/editor.js/issues/946)
- `Fix` - Tool's `destroy` method is not invoked when `editor.destroy()` is called. [#1047](https://github.com/codex-team/editor.js/issues/1047)
- `Fix` - Fixed issue with enter key in inputs and textareas [#920](https://github.com/codex-team/editor.js/issues/920)
- `Fix` - blocks.getBlockByIndex() API method now returns void for indexes out of range [#1270](https://github.com/codex-team/editor.js/issues/1270)
- `Fix` - Fixed the `Tab` key behavior when the caret is not set inside contenteditable element, but the block is selected [#1302](https://github.com/codex-team/editor.js/issues/1302).
- `Fix` - Fixed the `onChange` callback issue. This method didn't be called for native inputs before some contentedtable element changed [#843](https://github.com/codex-team/editor.js/issues/843)
- `Fix` - Fixed the `onChange` callback issue. This method didn't be called after the callback throws an exception [#1339](https://github.com/codex-team/editor.js/issues/1339)
- `Fix` - The internal `shortcut` getter of Tools classes will work now.
- `Deprecated` — The Inline Tool `clear()` method is deprecated because the new instance of Inline Tools will be created on every showing of the Inline Toolbar
### 2.18
- `New` *I18n API* — Ability to provide internalization for Editor.js core and tools. [#751](https://github.com/codex-team/editor.js/issues/751)
@ -24,7 +48,6 @@
> *Breaking changes* `blocks.getBlockByIndex` method now returns BlockAPI object. To access old value, use BlockAPI.holder property
### 2.17
- `Improvements` - Editor's [onchange callback](https://editorjs.io/configuration#editor-modifications-callback) now accepts an API as a parameter

View file

@ -24,10 +24,10 @@ Install the package via NPM or Yarn
npm i @editorjs/editorjs
```
Include module at your application
Include module at your application
```javascript
const EditorJS = require('@editorjs/editorjs');
import EditorJS from '@editorjs/editorjs';
```
### Use from CDN
@ -68,7 +68,7 @@ Check [Editor.js's community](https://github.com/editor-js/) to see Tools exampl
## Create Editor instance
Create an instance of Editor.js and pass [Configuration Object](../src/types-internal/editor-config.ts).
Create an instance of Editor.js and pass [Configuration Object](../src/types-internal/editor-config.ts).
At least the `holderId` option is required.
```html
@ -119,7 +119,7 @@ Editor.js needs a bit of time to initialize. It is an asynchronous action so it
If you need to know when the editor instance is ready you can use one of the following ways:
##### Pass `onReady` property to the configuration object.
##### Pass `onReady` property to the configuration object.
It must be a function:
@ -189,7 +189,7 @@ var editor = new EditorJS({
* onReady callback
*/
onReady: () => {console.log('Editor.js is ready to work!')},
/**
* onChange callback
*/

View file

@ -78,7 +78,7 @@ var editor = new EditorJS({
},
header: Header
},
initialBlock : 'text',
defaultBlock : 'text',
});
```
@ -89,6 +89,36 @@ There are few options available by Editor.js.
| `inlineToolbar` | _Boolean/Array_ | `false` | Pass `true` to enable the Inline Toolbar with all Tools, or pass an array with specified Tools list |
| `config` | _Object_ | `null` | User's configuration for Plugin.
## Tool prepare and reset
If you need to prepare some data for Tool (eg. load external script, create HTML nodes in the document, etc) you can use static prepare method.
It accepts tools config passed on Editor's initialization as an argument:
```javascript
class Tool {
static prepare(config) {
loadScript();
insertNodes();
...
}
}
```
On Editor destroy you can use an opposite method `reset` to clean up all prepared data:
```javascript
class Tool {
static reset() {
cleanUpScripts();
deleteNodes();
...
}
}
```
Both methods might be async.
## Paste handling
Editor.js handles paste on Blocks and provides API for Tools to process the pasted data.
@ -118,7 +148,7 @@ To handle pasted HTML elements object returned from `pasteConfig` getter should
| -- | -- | -- |
| `tags` | `String[]` | _Optional_. Should contain all tag names you want to be extracted from pasted data and processed by your `onPaste` method |
For correct work you MUST provide `onPaste` handler at least for `initialBlock` Tool.
For correct work you MUST provide `onPaste` handler at least for `defaultBlock` Tool.
> Example
@ -144,7 +174,7 @@ Your Tool can analyze text by RegExp patterns to substitute pasted string with d
**Note** Editor will check pattern's full match, so don't forget to handle all available chars in there.
Pattern will be processed only if paste was on `initialBlock` Tool and pasted string length is less than 450 characters.
Pattern will be processed only if paste was on `defaultBlock` Tool and pasted string length is less than 450 characters.
> Example

View file

@ -120,6 +120,32 @@ body {
text-decoration: none;
}
.ce-example__statusbar {
position: fixed;
bottom: 10px;
right: 10px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
font-size: 12px;
padding: 8px 15px;
z-index: 1;
}
.ce-example__statusbar-button {
display: inline-flex;
margin-left: 10px;
background: #4A9DF8;
padding: 6px 12px;
box-shadow: 0 7px 8px -4px rgba(137, 207, 255, 0.77);
transition: all 150ms ease;
cursor: pointer;
border-radius: 31px;
color: #fff;
font-family: 'PT Mono', Menlo, Monaco, Consolas, Courier New, monospace;
text-align: center;
}
@media all and (max-width: 730px){
.ce-example__header,
.ce-example__content{

View file

@ -34,6 +34,15 @@
<div class="ce-example__button" id="saveButton">
editor.save()
</div>
<div class="ce-example__statusbar">
Readonly:
<b id="readonly-state">
Off
</b>
<div class="ce-example__statusbar-button" id="toggleReadOnlyButton">
toggle
</div>
</div>
</div>
<div class="ce-example__output">
<pre class="ce-example__output-content" id="output"></pre>
@ -74,21 +83,29 @@
<!-- Initialization -->
<script>
/**
* Saving button
*/
const saveButton = document.getElementById('saveButton');
/**
* To initialize the Editor, create a new instance with configuration object
* @see docs/installation.md for mode details
*/
var editor = new EditorJS({
/**
* Enable/Disable the read only mode
*/
readOnly: false,
/**
* Wrapper of Editor
*/
holder: 'editorjs',
/**
* Common Inline Toolbar settings
* - if true (or not specified), the order from 'tool' property will be used
* - if an array of tool names, this order will be used
*/
// inlineToolbar: ['link', 'marker', 'bold', 'italic'],
// inlineToolbar: true,
/**
* Tools list
*/
@ -98,7 +115,7 @@
*/
header: {
class: Header,
inlineToolbar: ['link'],
inlineToolbar: ['marker', 'link'],
config: {
placeholder: 'Header'
},
@ -167,7 +184,7 @@
/**
* This Tool will be used as default
*/
// initialBlock: 'paragraph',
// defaultBlock: 'paragraph',
/**
* Initial Editor data
@ -279,14 +296,37 @@
},
});
/**
* Saving button
*/
const saveButton = document.getElementById('saveButton');
/**
* Toggle read-only button
*/
const toggleReadOnlyButton = document.getElementById('toggleReadOnlyButton');
const readOnlyIndicator = document.getElementById('readonly-state');
/**
* Saving example
*/
saveButton.addEventListener('click', function () {
editor.save().then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
});
editor.save()
.then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
})
.catch((error) => {
console.error('Saving error', error);
});
});
/**
* Toggle read-only example
*/
toggleReadOnlyButton.addEventListener('click', async () => {
const readOnlyState = await editor.readOnly.toggle();
readOnlyIndicator.textContent = readOnlyState ? 'On' : 'Off';
});
</script>
</body>

View file

@ -44,7 +44,7 @@
<!-- Load Tools -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@latest"></script><!-- Image -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script><!-- List -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script><!-- Checklist -->
@ -81,6 +81,11 @@
* Tools list
*/
tools: {
paragraph: {
config: {
placeholder: "Enter something"
}
},
/**
* Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
*/
@ -96,7 +101,7 @@
/**
* Or pass class directly without any configuration
*/
image: SimpleImage,
image: ImageTool,
list: {
class: List,
@ -209,6 +214,7 @@
"Bold": "Полужирный",
"Italic": "Курсив",
"InlineCode": "Моноширинный",
"Image": "Картинка"
},
/**
@ -235,6 +241,32 @@
*/
"stub": {
'The block can not be displayed correctly.': 'Блок не может быть отображен'
},
"image": {
"Caption": "Подпись",
"Select an Image": "Выберите файл",
"With border": "Добавить рамку",
"Stretch image": "Растянуть",
"With background": "Добавить подложку",
},
"code": {
"Enter a code": "Код",
},
"linkTool": {
"Link": "Ссылка",
"Couldn't fetch the link data": "Не удалось получить данные",
"Couldn't get this link data, try the other one": "Не удалось получить данные по ссылке, попробуйте другую",
"Wrong response format from the server": "Неполадки на сервере",
},
"header": {
"Header": "Заголовок",
},
"paragraph": {
"Enter something": "Введите текст"
},
"list": {
"Ordered": "Нумерованный",
"Unordered": "Маркированный",
}
},
@ -354,7 +386,9 @@
{
type: 'image',
data: {
url: 'assets/codex2x.png',
file : {
url: 'assets/codex2x.png',
},
caption: '',
stretched: false,
withBorder: true,

235
example/example-rtl.html Normal file
View file

@ -0,0 +1,235 @@
<!--
Use this page for RTL mode debugging.
Editor Tools are loaded as git-submodules.
You can pull modules by running `yarn pull_tools` and start experimenting.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Editor.js RTL example</title>
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
<link href="assets/demo.css" rel="stylesheet">
<script src="assets/json-preview.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
<div class="ce-example">
<div class="ce-example__header">
<a class="ce-example__header-logo" href="https://codex.so/editor">Editor.js 🤩🧦🤨</a>
<div class="ce-example__header-menu">
<a href="https://github.com/editor-js" target="_blank">Plugins</a>
<a href="https://editorjs.io/usage" target="_blank">Usage</a>
<a href="https://editorjs.io/configuration" target="_blank">Configuration</a>
<a href="https://editorjs.io/creating-a-block-tool" target="_blank">API</a>
</div>
</div>
<div class="ce-example__content _ce-example__content--small">
<div id="editorjs"></div>
<div id="hint" style="text-align: center;">
No submodules found. Run <code class="inline-code">yarn pull_tools</code>
</div>
<div class="ce-example__button" id="saveButton">
editor.save()
</div>
</div>
<div class="ce-example__output">
<pre class="ce-example__output-content" id="output"></pre>
<div class="ce-example__output-footer">
<a href="https://codex.so" style="font-weight: bold;">Made by CodeX</a>
</div>
</div>
</div>
<!-- Load Tools -->
<!--
You can upload Tools to your project's directory and use as in example below.
Also you can load each Tool from CDN or use NPM/Yarn packages.
Read more in Tool's README file. For example:
https://github.com/editor-js/header#installation
-->
<script src="./tools/header/dist/bundle.js" onload="document.getElementById('hint').hidden = true"></script><!-- Header -->
<script src="./tools/simple-image/dist/bundle.js"></script><!-- Image -->
<script src="./tools/delimiter/dist/bundle.js"></script><!-- Delimiter -->
<script src="./tools/list/dist/bundle.js"></script><!-- List -->
<script src="./tools/checklist/dist/bundle.js"></script><!-- Checklist -->
<script src="./tools/quote/dist/bundle.js"></script><!-- Quote -->
<script src="./tools/code/dist/bundle.js"></script><!-- Code -->
<script src="./tools/embed/dist/bundle.js"></script><!-- Embed -->
<script src="./tools/table/dist/bundle.js"></script><!-- Table -->
<script src="./tools/link/dist/bundle.js"></script><!-- Link -->
<script src="./tools/raw/dist/bundle.js"></script><!-- Raw -->
<script src="./tools/warning/dist/bundle.js"></script><!-- Warning -->
<script src="./tools/marker/dist/bundle.js"></script><!-- Marker -->
<script src="./tools/inline-code/dist/bundle.js"></script><!-- Inline Code -->
<!-- Load Editor.js's Core -->
<script src="../dist/editor.js"></script>
<!-- Initialization -->
<script>
/**
* Saving button
*/
const saveButton = document.getElementById('saveButton');
/**
* To initialize the Editor, create a new instance with configuration object
* @see docs/installation.md for mode details
*/
var editor = new EditorJS({
/**
* Wrapper of Editor
*/
holder: 'editorjs',
i18n: {
/**
* Text direction
*/
direction: 'rtl',
},
/**
* Tools list
*/
tools: {
/**
* Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
*/
header: {
class: Header,
inlineToolbar: ['link'],
config: {
placeholder: 'Header'
},
shortcut: 'CMD+SHIFT+H'
},
/**
* Or pass class directly without any configuration
*/
image: {
class: SimpleImage,
inlineToolbar: ['link'],
},
list: {
class: List,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+L'
},
checklist: {
class: Checklist,
inlineToolbar: true,
},
quote: {
class: Quote,
inlineToolbar: true,
config: {
quotePlaceholder: 'Enter a quote',
captionPlaceholder: 'Quote\'s author',
},
shortcut: 'CMD+SHIFT+O'
},
warning: Warning,
marker: {
class: Marker,
shortcut: 'CMD+SHIFT+M'
},
code: {
class: CodeTool,
shortcut: 'CMD+SHIFT+C'
},
delimiter: Delimiter,
inlineCode: {
class: InlineCode,
shortcut: 'CMD+SHIFT+C'
},
linkTool: LinkTool,
raw: RawTool,
embed: Embed,
table: {
class: Table,
inlineToolbar: true,
shortcut: 'CMD+ALT+T'
},
},
/**
* This Tool will be used as default
*/
// initialBlock: 'paragraph',
/**
* Initial Editor data
*/
data: {
blocks: [
{
type: "header",
data: {
text: "محرر.js",
level: 2
}
},
{
type : 'paragraph',
data : {
text : 'مرحبا! تعرف على المحرر الجديد. في هذه الصفحة ، يمكنك رؤيتها في العمل - حاول تحرير هذا النص.'
}
},
{
type: "header",
data: {
text: "دلائل الميزات",
level: 3
}
},
{
type : 'list',
data : {
items : [
'وهو محرر بنمط الكتلة',
'تقوم بإرجاع إخراج بيانات نظيفة في JSON',
'مصممة لتكون قابلة للتوسيع والتوصيل مع واجهة برمجة تطبيقات بسيطة'
],
style: 'unordered'
}
}
]
},
onReady: function(){
saveButton.click();
},
onChange: function() {
console.log('something changed');
}
});
/**
* Saving example
*/
saveButton.addEventListener('click', function () {
editor.save().then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
});
});
</script>
</body>
</html>

View file

@ -26,6 +26,16 @@
<div class="ce-example__button" id="saveButton">
editor.save()
</div>
<div class="ce-example__statusbar">
Readonly:
<b id="readonly-state">
Off
</b>
<div class="ce-example__statusbar-button" id="toggleReadOnlyButton">
toggle
</div>
</div>
</div>
<div class="ce-example__output">
<pre class="ce-example__output-content" id="output"></pre>
@ -65,21 +75,29 @@
<!-- Initialization -->
<script>
/**
* Saving button
*/
const saveButton = document.getElementById('saveButton');
/**
* To initialize the Editor, create a new instance with configuration object
* @see docs/installation.md for mode details
*/
var editor = new EditorJS({
/**
* Enable/Disable the read only mode
*/
readOnly: false,
/**
* Wrapper of Editor
*/
holder: 'editorjs',
/**
* Common Inline Toolbar settings
* - if true (or not specified), the order from 'tool' property will be used
* - if an array of tool names, this order will be used
*/
// inlineToolbar: ['link', 'marker', 'bold', 'italic'],
// inlineToolbar: true,
/**
* Tools list
*/
@ -89,7 +107,7 @@
*/
header: {
class: Header,
inlineToolbar: ['link'],
inlineToolbar: ['marker', 'link'],
config: {
placeholder: 'Header'
},
@ -156,7 +174,7 @@
/**
* This Tool will be used as default
*/
// initialBlock: 'paragraph',
// defaultBlock: 'paragraph',
/**
* Initial Editor data
@ -268,13 +286,37 @@
}
});
/**
* Saving button
*/
const saveButton = document.getElementById('saveButton');
/**
* Toggle read-only button
*/
const toggleReadOnlyButton = document.getElementById('toggleReadOnlyButton');
const readOnlyIndicator = document.getElementById('readonly-state');
/**
* Saving example
*/
saveButton.addEventListener('click', function () {
editor.save().then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
});
editor.save()
.then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
})
.catch((error) => {
console.error('Saving error', error);
});
});
/**
* Toggle read-only example
*/
toggleReadOnlyButton.addEventListener('click', async () => {
const readOnlyState = await editor.readOnly.toggle();
readOnlyIndicator.textContent = readOnlyState ? 'On' : 'Off';
});
</script>
</body>

@ -1 +1 @@
Subproject commit df6dcedb92ef586901b04cf72946aced9e36e58d
Subproject commit 197d5d53e0c7d869d76afc4eabae566d391e6d0e

@ -1 +1 @@
Subproject commit 1297b8c280ff34efaca8f3a2a3d263ec4201077d
Subproject commit 83d2d9d3136d48ab67b52bc034370c9a26c82c8c

@ -1 +1 @@
Subproject commit 6819831b7166c1cdfa31df77ab569274d9910aac
Subproject commit 1f2ec8c709a94c5f4f499bb1aaba7f4930e10484

@ -1 +1 @@
Subproject commit 44473de4c60dd836ccb61b4dbcf4cc00088acd19
Subproject commit 9d3d4b5216dce1a933b1e92b45b7b8c404d889b7

@ -1 +1 @@
Subproject commit 93e0b6d6418034f4e7ee704aba090cc25ca16ac2
Subproject commit 2b21da39b57d0abfcd4979444fb0c3d2d435af7d

@ -1 +1 @@
Subproject commit 1d6f474c14613c60344d30ebd930a18ca123e4a4
Subproject commit a983c4e62135c88d6cfd926527e6fc92c304451b

@ -1 +1 @@
Subproject commit 37a5e8d1db305cf75acd6622d9b82e2f308f71c6
Subproject commit 051b8e9e03e2f6bea30875da8253f6c0d858b231

@ -1 +1 @@
Subproject commit 33ffba6f9104d163c69c963ca06fed329a819238
Subproject commit 6a5563630977f223ebafaa03a7df3bf85797437b

@ -1 +1 @@
Subproject commit f537cf6ecb26fece34c56a85b51e79b07451e69e
Subproject commit 458a5fe364e33ad5b5913155d665f335ab742e0f

@ -1 +1 @@
Subproject commit c68375288c40774e8d8ceff79aa559d562078aaa
Subproject commit 6708697c1af79abbf6650f0f14e1cedc45eb5213

@ -1 +1 @@
Subproject commit 58bf8bd571ae259e3d150ac0c12d1676e5706470
Subproject commit 07881fc1020fde79ab9468f740227a99935abb2a

@ -1 +1 @@
Subproject commit 7c6d41603797ebfd00d59fc7ff623342b8f5a48c
Subproject commit 3f40a9cfdb0086c94ee2c7295a96d69c9b266dec

@ -1 +1 @@
Subproject commit 0fd96a70b371af0cc0720b8c2c0d0888b8a44bc5
Subproject commit 1883b28d8aac863d3907c21d5fda231c8e6f799a

@ -1 +1 @@
Subproject commit af9dc3885077ab2ea1b0ae8ae0d145ff1a40fc40
Subproject commit 5c1a73a8022c18ac1c15ee8d0134caae029bfbe9

@ -1 +1 @@
Subproject commit 293109d03d9ff3cdecc52ec959866662d80dd0ce
Subproject commit c0507d91014f9b4fdb514f0499348d7619e3e1a2

View file

@ -53,7 +53,7 @@ export default class EditorJS {
/**
* If `onReady` was passed in `configuration` then redefine onReady function
*/
if (typeof configuration === 'object' && typeof configuration.onReady === 'function') {
if (typeof configuration === 'object' && _.isFunction(configuration.onReady)) {
onReady = configuration.onReady;
}

View file

@ -2,6 +2,12 @@ import { EditorModules } from '../types-internal/editor-modules';
import { EditorConfig } from '../../types';
import { ModuleConfig } from '../types-internal/module-config';
/**
* The type <T> of the Module generic.
* It describes the structure of nodes used in modules.
*/
export type ModuleNodes = object;
/**
* @abstract
* @class Module
@ -11,7 +17,13 @@ import { ModuleConfig } from '../types-internal/module-config';
* @property {object} config - Editor user settings
* @property {EditorModules} Editor - List of Editor modules
*/
export default class Module {
export default class Module<T extends ModuleNodes = {}> {
/**
* Each module can provide some UI elements that will be stored in this property
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public nodes: T = {} as any;
/**
* Editor modules list
*
@ -26,6 +38,50 @@ export default class Module {
*/
protected config: EditorConfig;
/**
* This object provides methods to push into set of listeners that being dropped when read-only mode is enabled
*/
protected readOnlyMutableListeners = {
/**
* Assigns event listener on DOM element and pushes into special array that might be removed
*
* @param {EventTarget} element - DOM Element
* @param {string} eventType - Event name
* @param {Function} handler - Event handler
* @param {boolean|AddEventListenerOptions} options - Listening options
*/
on: (
element: EventTarget,
eventType: string,
handler: (event: Event) => void,
options: boolean | AddEventListenerOptions = false
): void => {
const { Listeners } = this.Editor;
this.mutableListenerIds.push(
Listeners.on(element, eventType, handler, options)
);
},
/**
* Clears all mutable listeners
*/
clearAll: (): void => {
const { Listeners } = this.Editor;
for (const id of this.mutableListenerIds) {
Listeners.offById(id);
}
this.mutableListenerIds = [];
},
};
/**
* The set of listener identifiers which will be dropped in read-only mode
*/
private mutableListenerIds: string[] = [];
/**
* @class
* @param {EditorConfig} config - Editor's config
@ -46,4 +102,24 @@ export default class Module {
public set state(Editor: EditorModules) {
this.Editor = Editor;
}
/**
* Remove memorized nodes
*/
public removeAllNodes(): void {
for (const key in this.nodes) {
const node = this.nodes[key];
if (node instanceof HTMLElement) {
node.remove();
}
}
}
/**
* Returns true if current direction is RTL (Right-To-Left)
*/
protected get isRtl(): boolean {
return this.config.i18n.direction === 'rtl';
}
}

View file

@ -71,9 +71,10 @@ export default class MoveDownTune implements BlockTune {
*/
public handleClick(event: MouseEvent, button: HTMLElement): void {
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
// If Block is last do nothing
if (currentBlockIndex === this.api.blocks.getBlocksCount() - 1) {
if (!nextBlock) {
button.classList.add(this.CSS.animation);
window.setTimeout(() => {
@ -83,7 +84,6 @@ export default class MoveDownTune implements BlockTune {
return;
}
const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
const nextBlockElement = nextBlock.holder;
const nextBlockCoords = nextBlockElement.getBoundingClientRect();

View file

@ -70,8 +70,10 @@ export default class MoveUpTune implements BlockTune {
*/
public handleClick(event: MouseEvent, button: HTMLElement): void {
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
if (currentBlockIndex === 0) {
if (currentBlockIndex === 0 || !currentBlock || !previousBlock) {
button.classList.add(this.CSS.animation);
window.setTimeout(() => {
@ -81,9 +83,7 @@ export default class MoveUpTune implements BlockTune {
return;
}
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
const currentBlockElement = currentBlock.holder;
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
const previousBlockElement = previousBlock.holder;
/**

View file

@ -1,6 +1,6 @@
import Block from './index';
import { BlockToolData, ToolConfig } from '../../../types/tools';
import { SavedData } from '../../types-internal/block-data';
import { SavedData } from '../../../types/data-formats';
import { BlockAPI as BlockAPIInterface } from '../../../types/api';
/**
@ -10,7 +10,9 @@ import { BlockAPI as BlockAPIInterface } from '../../../types/api';
*
* @param {Block} block - Block to expose
*/
function BlockAPI(block: Block): void {
function BlockAPI(
block: Block
): void {
const blockAPI: BlockAPIInterface = {
/**
* Tool name
@ -81,10 +83,10 @@ function BlockAPI(block: Block): void {
* @param {string} methodName - method to call
* @param {object} param - object with parameters
*
* @returns {void}
* @returns {unknown}
*/
call(methodName: string, param?: object): void {
block.call(methodName, param);
call(methodName: string, param?: object): unknown {
return block.call(methodName, param);
},
/**

View file

@ -10,11 +10,10 @@ import {
ToolSettings
} from '../../../types';
import { SavedData } from '../../types-internal/block-data';
import { SavedData } from '../../../types/data-formats';
import $ from '../dom';
import * as _ from '../utils';
import ApiModule from '../modules/api';
import SelectionUtils from '../selection';
import ApiModules from '../modules/api';
import BlockAPI from './api';
import { ToolType } from '../modules/tools';
@ -22,6 +21,7 @@ import { ToolType } from '../modules/tools';
import MoveUpTune from '../block-tunes/block-tune-move-up';
import DeleteTune from '../block-tunes/block-tune-delete';
import MoveDownTune from '../block-tunes/block-tune-move-down';
import SelectionUtils from '../selection';
/**
* Interface describes Block class constructor argument
@ -50,7 +50,12 @@ interface BlockConstructorOptions {
/**
* Editor's API methods
*/
api: ApiModule;
api: ApiModules;
/**
* This flag indicates that the Block should be constructed in the read-only mode.
*/
readOnly: boolean;
}
/**
@ -147,7 +152,7 @@ export default class Block {
/**
* Editor`s API module
*/
private readonly api: ApiModule;
private readonly api: ApiModules;
/**
* Focused input index
@ -198,7 +203,8 @@ export default class Block {
* @param {BlockToolData} options.data - Tool's initial data
* @param {BlockToolConstructable} options.Tool Tool's class
* @param {ToolSettings} options.settings - default tool's config
* @param {ApiModule} options.api - Editor API module for pass it to the Block Tunes
* @param {Module} options.api - Editor API module for pass it to the Block Tunes
* @param {boolean} options.readOnly - Read-Only flag
*/
constructor({
name,
@ -206,6 +212,7 @@ export default class Block {
Tool,
settings,
api,
readOnly,
}: BlockConstructorOptions) {
this.name = name;
this.class = Tool;
@ -221,6 +228,7 @@ export default class Block {
config: this.config,
api: this.api.getMethodsForTool(name, ToolType.Block),
block: this.blockAPI,
readOnly,
});
this.holder = this.compose();
@ -351,7 +359,7 @@ export default class Block {
* @returns {boolean}
*/
public mergeable(): boolean {
return typeof this.tool.merge === 'function';
return _.isFunction(this.tool.merge);
}
/**
@ -621,7 +629,15 @@ export default class Block {
* Update current input index with selection anchor node
*/
public updateCurrentInput(): void {
this.currentInput = SelectionUtils.anchorNode;
/**
* If activeElement is native input, anchorNode points to its parent.
* So if it is native input use it instead of anchorNode
*
* If anchorNode is undefined, also use activeElement
*/
this.currentInput = $.isNativeInput(document.activeElement) || !SelectionUtils.anchorNode
? document.activeElement
: SelectionUtils.anchorNode;
}
/**
@ -640,6 +656,12 @@ export default class Block {
attributes: true,
}
);
/**
* Mutation observer doesn't track changes in "<input>" and "<textarea>"
* so we need to track focus events to update current input and clear cache.
*/
this.addInputEvents();
}
/**
@ -647,6 +669,7 @@ export default class Block {
*/
public willUnselect(): void {
this.mutationObserver.disconnect();
this.removeInputEvents();
}
/**
@ -664,4 +687,37 @@ export default class Block {
return wrapper;
}
/**
* Is fired when text input or contentEditable is focused
*/
private handleFocus = (): void => {
/**
* Drop cache
*/
this.cachedInputs = [];
/**
* Update current input
*/
this.updateCurrentInput();
}
/**
* Adds focus event listeners to all inputs and contentEditables
*/
private addInputEvents(): void {
this.inputs.forEach(input => {
input.addEventListener('focus', this.handleFocus);
});
}
/**
* removes focus event listeners from all inputs and contentEditables
*/
private removeInputEvents(): void {
this.inputs.forEach(input => {
input.removeEventListener('focus', this.handleFocus);
});
}
}

View file

@ -1,11 +1,9 @@
import $ from './dom';
// eslint-disable-next-line import/no-duplicates
import * as _ from './utils';
// eslint-disable-next-line import/no-duplicates
import { LogLevels } from './utils';
import { EditorConfig, OutputData, SanitizerConfig } from '../../types';
import { EditorModules } from '../types-internal/editor-modules';
import I18n from './i18n';
import { CriticalError } from './errors/critical';
/**
* @typedef {Core} Core - editor core class
@ -130,11 +128,10 @@ export default class Core {
/**
* If holderId is preset, assign him to holder property and work next only with holder
*/
_.deprecationAssert(!!config.holderId, 'config.holderId', 'config.holder');
if (config.holderId && !config.holder) {
config.holder = config.holderId;
config.holderId = null;
_.log('holderId property is deprecated and will be removed in the next major release. ' +
'Use holder property instead.', 'warn');
}
/**
@ -152,15 +149,16 @@ export default class Core {
}
if (!this.config.logLevel) {
this.config.logLevel = LogLevels.VERBOSE;
this.config.logLevel = _.LogLevels.VERBOSE;
}
_.setLogLevel(this.config.logLevel);
/**
* If initial Block's Tool was not passed, use the Paragraph Tool
* If default Block's Tool was not passed, use the Paragraph Tool
*/
this.config.initialBlock = this.config.initialBlock || 'paragraph';
_.deprecationAssert(Boolean(this.config.initialBlock), 'config.initialBlock', 'config.defaultBlock');
this.config.defaultBlock = this.config.defaultBlock || this.config.initialBlock || 'paragraph';
/**
* Height of Editor's bottom area that allows to set focus on the last Block
@ -170,13 +168,13 @@ export default class Core {
this.config.minHeight = this.config.minHeight !== undefined ? this.config.minHeight : 300;
/**
* Initial block type
* Default block type
* Uses in case when there is no blocks passed
*
* @type {{type: (*), data: {text: null}}}
*/
const initialBlockData = {
type: this.config.initialBlock,
const defaultBlockData = {
type: this.config.defaultBlock,
data: {},
};
@ -187,32 +185,42 @@ export default class Core {
a: true,
} as SanitizerConfig;
this.config.hideToolbar = this.config.hideToolbar ? this.config.hideToolbar : false;
this.config.tools = this.config.tools || {};
this.config.i18n = this.config.i18n || {};
this.config.data = this.config.data || {} as OutputData;
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.config.onReady = this.config.onReady || ((): void => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.config.onChange = this.config.onChange || ((): void => {});
this.config.inlineToolbar = this.config.inlineToolbar !== undefined ? this.config.inlineToolbar : true;
/**
* Initialize Blocks to pass data to the Renderer
* Initialize default Block to pass data to the Renderer
*/
if (_.isEmpty(this.config.data)) {
this.config.data = {} as OutputData;
this.config.data.blocks = [ initialBlockData ];
this.config.data.blocks = [ defaultBlockData ];
} else {
if (!this.config.data.blocks || this.config.data.blocks.length === 0) {
this.config.data.blocks = [ initialBlockData ];
this.config.data.blocks = [ defaultBlockData ];
}
}
this.config.readOnly = this.config.readOnly as boolean || false;
/**
* Adjust i18n
*/
if (config.i18n && config.i18n.messages) {
if (config.i18n?.messages) {
I18n.setDictionary(config.i18n.messages);
}
/**
* Text direction. If not set, uses ltr
*/
this.config.i18n.direction = config.i18n?.direction || 'ltr';
}
/**
@ -278,10 +286,10 @@ export default class Core {
'UI',
'BlockManager',
'Paste',
'DragNDrop',
'ModificationsObserver',
'BlockSelection',
'RectangleSelection',
'CrossBlockSelection',
'ReadOnly',
];
await modulesToPrepare.reduce(
@ -291,6 +299,13 @@ export default class Core {
try {
await this.moduleInstances[module].prepare();
} catch (e) {
/**
* CriticalError's will not be caught
* It is used when Editor is rendering in read-only mode with unsupported plugin
*/
if (e instanceof CriticalError) {
throw new Error(e.message);
}
_.log(`Module ${module} was skipped because of %o`, 'warn', e);
}
// _.log(`Preparing ${module} module`, 'timeEnd');
@ -314,7 +329,7 @@ export default class Core {
/**
* 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;
const Module = _.isFunction(module) ? module : module.default;
try {
/**

View file

@ -296,7 +296,7 @@ export default class Dom {
}
/**
* Check if object is DocumentFragmemt node
* Check if object is DocumentFragment node
*
* @param {object} node - object to check
* @returns {boolean}

View file

@ -0,0 +1,5 @@
/**
* This type of exception will destroy the Editor! Be careful when using it
*/
export class CriticalError extends Error {
}

View file

@ -259,7 +259,7 @@ export default class Flipper {
this.iterator.currentItem.click();
}
if (typeof this.activateCallback === 'function') {
if (_.isFunction(this.activateCallback)) {
this.activateCallback(this.iterator.currentItem);
}

View file

@ -1,5 +1,4 @@
import defaultDictionary from './locales/en/messages.json';
import * as _ from '../utils';
import { I18nDictionary, Dictionary } from '../../../types/configs';
import { LeavesDictKeys } from '../../types-internal/i18n-internal-namespace';
@ -60,9 +59,12 @@ export default class I18n {
private static _t(namespace: string, dictKey: string): string {
const section = I18n.getNamespace(namespace);
if (section === undefined) {
_.logLabeled('I18n: section %o was not found in current dictionary', 'log', namespace);
}
/**
* For Console Message to Check Section is defined or not
* if (section === undefined) {
* _.logLabeled('I18n: section %o was not found in current dictionary', 'log', namespace);
* }
*/
if (!section || !section[dictKey]) {
return dictKey;

View file

@ -1,9 +1,8 @@
import Module from '../../__module';
import { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';
import { BlockToolData, OutputData, ToolConfig } from '../../../../types';
import * as _ from './../../utils';
import BlockAPI from '../../block/api';
import Module from '../../__module';
/**
* @class BlocksAPI
@ -23,7 +22,7 @@ export default class BlocksAPI extends Module {
delete: (index?: number): void => this.delete(index),
swap: (fromIndex: number, toIndex: number): void => this.swap(fromIndex, toIndex),
move: (toIndex: number, fromIndex?: number): void => this.move(toIndex, fromIndex),
getBlockByIndex: (index: number): BlockAPIInterface => this.getBlockByIndex(index),
getBlockByIndex: (index: number): BlockAPIInterface | void => this.getBlockByIndex(index),
getCurrentBlockIndex: (): number => this.getCurrentBlockIndex(),
getBlocksCount: (): number => this.getBlocksCount(),
stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status),
@ -51,15 +50,19 @@ export default class BlocksAPI extends Module {
}
/**
* Returns Block holder by Block index
* Returns BlockAPI object by Block index
*
* @param {number} index - index to get
*
* @returns {HTMLElement}
*/
public getBlockByIndex(index: number): BlockAPIInterface {
public getBlockByIndex(index: number): BlockAPIInterface | void {
const block = this.Editor.BlockManager.getBlockByIndex(index);
if (block === undefined) {
_.logLabeled('There is no block at index `' + index + '`', 'warn');
return;
}
return new BlockAPI(block);
}
@ -118,7 +121,7 @@ export default class BlocksAPI extends Module {
/**
* in case of last block deletion
* Insert new initial empty block
* Insert the new default empty block
*/
if (this.Editor.BlockManager.blocks.length === 0) {
this.Editor.BlockManager.insert();
@ -127,7 +130,9 @@ export default class BlocksAPI extends Module {
/**
* After Block deletion currentBlock is updated
*/
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock, this.Editor.Caret.positions.END);
if (this.Editor.BlockManager.currentBlock) {
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock, this.Editor.Caret.positions.END);
}
this.Editor.Toolbar.close();
}
@ -172,10 +177,10 @@ export default class BlocksAPI extends Module {
* @deprecated Use BlockAPI interface to stretch Blocks
*/
public stretchBlock(index: number, status = true): void {
_.log(
'`blocks.stretchBlock()` method is deprecated and will be removed in the next major release. ' +
'Use BlockAPI interface instead',
'warn'
_.deprecationAssert(
true,
'blocks.stretchBlock()',
'BlockAPI'
);
const block = this.Editor.BlockManager.getBlockByIndex(index);
@ -197,7 +202,7 @@ export default class BlocksAPI extends Module {
* @param {boolean?} needToFocus - flag to focus inserted Block
*/
public insert = (
type: string = this.config.initialBlock,
type: string = this.config.defaultBlock,
data: BlockToolData = {},
config: ToolConfig = {},
index?: number,

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Caret } from '../../../../types/api';
import Module from '../../__module';
/**
* @class CaretAPI

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Events } from '../../../../types/api';
import Module from '../../__module';
/**
* @class EventsAPI

View file

@ -1,8 +1,8 @@
import Module from '../../__module';
import { I18n } from '../../../../types/api';
import I18nInternal from '../../i18n';
import { ToolType } from '../tools';
import { logLabeled } from '../../utils';
import Module from '../../__module';
/**
* Provides methods for working with i18n

View file

@ -31,7 +31,8 @@ export default class API extends Module {
inlineToolbar: this.Editor.InlineToolbarAPI.methods,
tooltip: this.Editor.TooltipAPI.methods,
i18n: this.Editor.I18nAPI.methods,
} as APIInterfaces;
readOnly: this.Editor.ReadOnlyAPI.methods,
};
}
/**

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { InlineToolbar } from '../../../../types/api/inline-toolbar';
import Module from '../../__module';
/**
* @class InlineToolbarAPI

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Listeners } from '../../../../types/api';
import Module from '../../__module';
/**
* @class ListenersAPI

View file

@ -1,6 +1,6 @@
import Module from '../../__module';
import { Notifier } from '../../../../types/api';
import { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier';
import Module from '../../__module';
/**
*

View file

@ -0,0 +1,28 @@
import { ReadOnly } from '../../../../types/api';
import Module from '../../__module';
/**
* @class ReadOnlyAPI
* @classdesc ReadOnly API
*/
export default class ReadOnlyAPI extends Module {
/**
* Available methods
*/
public get methods(): ReadOnly {
return {
toggle: (state): Promise<boolean> => this.toggle(state),
};
}
/**
* Set or toggle read-only state
*
* @param {boolean|undefined} state - set or toggle state
*
* @returns {boolean} current value
*/
public toggle(state?: boolean): Promise<boolean> {
return this.Editor.ReadOnly.toggle(state);
}
}

View file

@ -1,6 +1,6 @@
import Module from '../../__module';
import { Sanitizer } from '../../../../types/api';
import { SanitizerConfig } from '../../../../types/configs';
import Module from '../../__module';
/**
* @class SanitizerAPI

View file

@ -1,6 +1,7 @@
import Module from '../../__module';
import { Saver } from '../../../../types/api';
import { OutputData } from '../../../../types';
import * as _ from '../../utils';
import Module from '../../__module';
/**
* @class SaverAPI
@ -20,8 +21,18 @@ export default class SaverAPI extends Module {
/**
* Return Editor's data
*
* @returns {OutputData}
*/
public save(): Promise<OutputData> {
const errorText = 'Editor\'s content can not be saved in read-only mode';
if (this.Editor.ReadOnly.isEnabled) {
_.logLabeled(errorText, 'warn');
return Promise.reject(new Error(errorText));
}
return this.Editor.Saver.save();
}
}

View file

@ -1,6 +1,6 @@
import Module from '../../__module';
import SelectionUtils from '../../selection';
import { Selection as SelectionAPIInterface } from '../../../../types/api';
import Module from '../../__module';
/**
* @class SelectionAPI

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Styles } from '../../../../types/api';
import Module from '../../__module';
/**
*

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import { Toolbar } from '../../../../types/api';
import Module from '../../__module';
/**
* @class ToolbarAPI

View file

@ -1,6 +1,6 @@
import Module from '../../__module';
import { Tooltip } from '../../../../types/api';
import { TooltipContent, TooltipOptions } from 'codex-tooltip';
import Module from '../../__module';
/**
* @class TooltipAPI

View file

@ -93,7 +93,7 @@ export default class BlockEvents extends Module {
*
* @param {KeyboardEvent} event - keyup event
*/
public keyup(event): void {
public keyup(event: KeyboardEvent): void {
/**
* If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows)
*/
@ -107,21 +107,6 @@ export default class BlockEvents extends Module {
this.Editor.UI.checkEmptiness();
}
/**
* Set up mouse selection handlers
*
* @param {MouseEvent} event - mouse down event
*/
public mouseDown(event: MouseEvent): void {
/**
* Each mouse down on Block must disable selectAll state
*/
if (!SelectionUtils.isCollapsed) {
this.Editor.BlockSelection.clearSelection(event);
}
this.Editor.CrossBlockSelection.watchSelection(event);
}
/**
* Open Toolbox to leaf Tools
*
@ -140,12 +125,12 @@ export default class BlockEvents extends Module {
return;
}
const canOpenToolbox = Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty;
const canOpenToolbox = Tools.isDefault(currentBlock.tool) && currentBlock.isEmpty;
const conversionToolbarOpened = !currentBlock.isEmpty && ConversionToolbar.opened;
const inlineToolbarOpened = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
/**
* For empty Blocks we show Plus button via Toolbox only for initial Blocks
* For empty Blocks we show Plus button via Toolbox only for default Blocks
*/
if (canOpenToolbox) {
this.activateToolbox();
@ -157,10 +142,10 @@ export default class BlockEvents extends Module {
/**
* Add drop target styles
*
* @param {DragEvent} e - drag over event
* @param {DragEvent} event - drag over event
*/
public dragOver(e: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
public dragOver(event: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node);
block.dropTarget = true;
}
@ -168,10 +153,10 @@ export default class BlockEvents extends Module {
/**
* Remove drop target style
*
* @param {DragEvent} e - drag leave event
* @param {DragEvent} event - drag leave event
*/
public dragLeave(e: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
public dragLeave(event: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node);
block.dropTarget = false;
}
@ -209,7 +194,7 @@ export default class BlockEvents extends Module {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
Caret.setToBlock(BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection(event);
@ -254,7 +239,7 @@ export default class BlockEvents extends Module {
* If enter has been pressed at the start of the text, just insert paragraph Block above
*/
if (this.Editor.Caret.isAtStart && !this.Editor.BlockManager.currentBlock.hasMedia) {
this.Editor.BlockManager.insertInitialBlockAtIndex(this.Editor.BlockManager.currentBlockIndex);
this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex);
} else {
/**
* Split the Current Block into two blocks
@ -268,7 +253,7 @@ export default class BlockEvents extends Module {
/**
* If new Block is empty
*/
if (this.Editor.Tools.isInitial(newCurrent.tool) && newCurrent.isEmpty) {
if (this.Editor.Tools.isDefault(newCurrent.tool) && newCurrent.isEmpty) {
/**
* Show Toolbar
*/
@ -426,7 +411,10 @@ export default class BlockEvents extends Module {
return;
}
if (this.Editor.Caret.navigateNext()) {
const navigateNext = event.keyCode === _.keyCodes.DOWN || (event.keyCode === _.keyCodes.RIGHT && !this.isRtl);
const isNavigated = navigateNext ? this.Editor.Caret.navigateNext() : this.Editor.Caret.navigatePrevious();
if (isNavigated) {
/**
* Default behaviour moves cursor by 1 character, we need to prevent it
*/
@ -481,7 +469,10 @@ export default class BlockEvents extends Module {
return;
}
if (this.Editor.Caret.navigatePrevious()) {
const navigatePrevious = event.keyCode === _.keyCodes.UP || (event.keyCode === _.keyCodes.LEFT && !this.isRtl);
const isNavigated = navigatePrevious ? this.Editor.Caret.navigatePrevious() : this.Editor.Caret.navigateNext();
if (isNavigated) {
/**
* Default behaviour moves cursor by 1 character, we need to prevent it
*/

View file

@ -160,12 +160,9 @@ export default class BlockManager extends Module {
/**
* Should be called after Editor.UI preparation
* Define this._blocks property
*
* @returns {Promise}
*/
public async prepare(): Promise<void> {
public prepare(): void {
const blocks = new Blocks(this.Editor.UI.nodes.redactor);
const { BlockEvents, Listeners } = this.Editor;
/**
* We need to use Proxy to overload set/get [] operator.
@ -187,18 +184,30 @@ export default class BlockManager extends Module {
});
/** Copy event */
Listeners.on(
this.Editor.Listeners.on(
document,
'copy',
(e: ClipboardEvent) => BlockEvents.handleCommandC(e)
(e: ClipboardEvent) => this.Editor.BlockEvents.handleCommandC(e)
);
}
/** Copy and cut */
Listeners.on(
document,
'cut',
(e: ClipboardEvent) => BlockEvents.handleCommandX(e)
);
/**
* Toggle read-only state
*
* If readOnly is true:
* - Unbind event handlers from created Blocks
*
* if readOnly is false:
* - Bind event handlers to all existing Blocks
*
* @param {boolean} readOnlyEnabled - "read only" state
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (!readOnlyEnabled) {
this.enableModuleBindings();
} else {
this.disableModuleBindings();
}
}
/**
@ -211,6 +220,7 @@ export default class BlockManager extends Module {
* @returns {Block}
*/
public composeBlock({ tool, data = {} }: {tool: string; data?: BlockToolData}): Block {
const readOnly = this.Editor.ReadOnly.isEnabled;
const settings = this.Editor.Tools.getToolSettings(tool);
const Tool = this.Editor.Tools.available[tool] as BlockToolConstructable;
const block = new Block({
@ -219,9 +229,12 @@ export default class BlockManager extends Module {
Tool,
settings,
api: this.Editor.API,
readOnly,
});
this.bindEvents(block);
if (!readOnly) {
this.bindBlockEvents(block);
}
return block;
}
@ -230,7 +243,7 @@ export default class BlockManager extends Module {
* Insert new block into _blocks
*
* @param {object} options - insert options
* @param {string} options.tool - plugin name, by default method inserts initial block type
* @param {string} options.tool - plugin name, by default method inserts the default block type
* @param {object} options.data - plugin data
* @param {number} options.index - index where to insert new Block
* @param {boolean} options.needToFocus - flag shows if needed to update current Block index
@ -239,7 +252,7 @@ export default class BlockManager extends Module {
* @returns {Block}
*/
public insert({
tool = this.config.initialBlock,
tool = this.config.defaultBlock,
data = {},
index,
needToFocus = true,
@ -283,7 +296,7 @@ export default class BlockManager extends Module {
* @returns {Block}
*/
public replace({
tool = this.config.initialBlock,
tool = this.config.defaultBlock,
data = {},
}): Block {
return this.insert({
@ -321,7 +334,7 @@ export default class BlockManager extends Module {
}
/**
* Insert new initial block at passed index
* Insert new default block at passed index
*
* @param {number} index - index where Block should be inserted
* @param {boolean} needToFocus - if true, updates current Block index
@ -330,8 +343,8 @@ export default class BlockManager extends Module {
*
* @returns {Block} inserted Block
*/
public insertInitialBlockAtIndex(index: number, needToFocus = false): Block {
const block = this.composeBlock({ tool: this.config.initialBlock });
public insertDefaultBlockAtIndex(index: number, needToFocus = false): Block {
const block = this.composeBlock({ tool: this.config.defaultBlock });
this._blocks[index] = block;
@ -356,7 +369,7 @@ export default class BlockManager extends Module {
this.currentBlockIndex = this.blocks.length - 1;
/**
* Insert initial typed block
* Insert the default typed block
*/
return this.insert();
}
@ -443,7 +456,7 @@ export default class BlockManager extends Module {
/**
* Attention!
* After removing insert new initial typed Block and focus on it
* After removing insert the new default typed Block and focus on it
* Removes all blocks
*/
public removeAllBlocks(): void {
@ -568,6 +581,11 @@ export default class BlockManager extends Module {
*/
this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock as HTMLElement);
/**
* Update current block active input
*/
this.currentBlock.updateCurrentInput();
return this.currentBlock;
} else {
throw new Error('Can not find a Block from this child Node');
@ -649,15 +667,15 @@ export default class BlockManager extends Module {
/**
* Clears Editor
*
* @param {boolean} needAddInitialBlock - 1) in internal calls (for example, in api.blocks.render)
* we don't need to add empty initial block
* @param {boolean} needToAddDefaultBlock - 1) in internal calls (for example, in api.blocks.render)
* we don't need to add an empty default block
* 2) in api.blocks.clear we should add empty block
*/
public clear(needAddInitialBlock = false): void {
public clear(needToAddDefaultBlock = false): void {
this._blocks.removeAll();
this.dropPointer();
if (needAddInitialBlock) {
if (needToAddDefaultBlock) {
this.insert();
}
@ -668,18 +686,63 @@ export default class BlockManager extends Module {
}
/**
* Bind Events
* Cleans up all the block tools' resources
* This is called when editor is destroyed
*/
public async destroy(): Promise<void> {
await Promise.all(this.blocks.map((block) => {
if (_.isFunction(block.tool.destroy)) {
return block.tool.destroy();
}
}));
}
/**
* Bind Block events
*
* @param {Block} block - Block to which event should be bound
*/
private bindEvents(block: Block): void {
const { BlockEvents, Listeners } = this.Editor;
private bindBlockEvents(block: Block): void {
const { BlockEvents } = this.Editor;
Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), false);
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));
Listeners.on(block.holder, 'dragleave', (event) => BlockEvents.dragLeave(event as DragEvent));
this.readOnlyMutableListeners.on(block.holder, 'keydown', (event: KeyboardEvent) => {
BlockEvents.keydown(event);
}, true);
this.readOnlyMutableListeners.on(block.holder, 'keyup', (event: KeyboardEvent) => {
BlockEvents.keyup(event);
});
this.readOnlyMutableListeners.on(block.holder, 'dragover', (event: DragEvent) => {
BlockEvents.dragOver(event);
});
this.readOnlyMutableListeners.on(block.holder, 'dragleave', (event: DragEvent) => {
BlockEvents.dragLeave(event);
});
}
/**
* Disable mutable handlers and bindings
*/
private disableModuleBindings(): void {
this.readOnlyMutableListeners.clearAll();
}
/**
* Enables all module handlers and bindings for all Blocks
*/
private enableModuleBindings(): void {
/** Cut event */
this.readOnlyMutableListeners.on(
document,
'cut',
(e: ClipboardEvent) => this.Editor.BlockEvents.handleCommandX(e)
);
this.blocks.forEach((block: Block) => {
this.bindBlockEvents(block);
});
}
/**

View file

@ -17,6 +17,15 @@ import { SanitizerConfig } from '../../../types/configs';
*
*/
export default class BlockSelection extends Module {
/**
* Sometimes .anyBlockSelected can be called frequently,
* for example at ui@selectionChange (to clear native browser selection in CBS)
* We use cache to prevent multiple iterations through all the blocks
*
* @private
*/
private anyBlockSelectedCache: boolean | null = null;
/**
* Sanitizer Config
*
@ -71,6 +80,8 @@ export default class BlockSelection extends Module {
BlockManager.blocks.forEach((block) => {
block.selected = state;
});
this.clearCache();
}
/**
@ -81,7 +92,11 @@ export default class BlockSelection extends Module {
public get anyBlockSelected(): boolean {
const { BlockManager } = this.Editor;
return BlockManager.blocks.some((block) => block.selected === true);
if (this.anyBlockSelectedCache === null) {
this.anyBlockSelectedCache = BlockManager.blocks.some((block) => block.selected === true);
}
return this.anyBlockSelectedCache;
}
/**
@ -132,15 +147,30 @@ export default class BlockSelection extends Module {
public prepare(): void {
const { Shortcuts } = this.Editor;
/** Selection shortcut */
this.selection = new SelectionUtils();
/**
* CMD/CTRL+A selection shortcut
*/
Shortcuts.add({
name: 'CMD+A',
handler: (event) => {
const { BlockManager } = this.Editor;
const { BlockManager, ReadOnly } = this.Editor;
/**
* We use Editor's Block selection on CMD+A ShortCut instead of Browsers
*/
if (ReadOnly.isEnabled) {
event.preventDefault();
this.selectAllBlocks();
return;
}
/**
* When one page consist of two or more EditorJS instances
* Shortcut module tries to handle all events. Thats why Editor's selection works inside the target Editor, but
* Shortcut module tries to handle all events.
* Thats why Editor's selection works inside the target Editor, but
* for others error occurs because nothing to select.
*
* Prevent such actions if focus is not inside the Editor
@ -152,8 +182,21 @@ export default class BlockSelection extends Module {
this.handleCommandA(event);
},
});
}
this.selection = new SelectionUtils();
/**
* Toggle read-only state
*
* - Remove all ranges
* - Unselect all Blocks
*
* @param {boolean} readOnlyEnabled - "read only" state
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
SelectionUtils.get()
.removeAllRanges();
this.allBlocksSelected = false;
}
/**
@ -173,6 +216,8 @@ export default class BlockSelection extends Module {
}
block.selected = false;
this.clearCache();
}
/**
@ -198,10 +243,18 @@ export default class BlockSelection extends Module {
if (this.anyBlockSelected && isKeyboard && isPrintableKey && !SelectionUtils.isSelectionExists) {
const indexToInsert = BlockManager.removeSelectedBlocks();
BlockManager.insertInitialBlockAtIndex(indexToInsert, true);
BlockManager.insertDefaultBlockAtIndex(indexToInsert, true);
Caret.setToBlock(BlockManager.currentBlock);
_.delay(() => {
Caret.insertContentAtCaretPosition((reason as KeyboardEvent).key);
const eventKey = (reason as KeyboardEvent).key;
/**
* If event.key length >1 that means key is special (e.g. Enter or Dead or Unidentifier).
* So we use empty string
*
* @see https://developer.mozilla.org/ru/docs/Web/API/KeyboardEvent/key
*/
Caret.insertContentAtCaretPosition(eventKey.length > 1 ? '' : eventKey);
}, 20)();
}
@ -290,10 +343,19 @@ export default class BlockSelection extends Module {
block.selected = true;
this.clearCache();
/** close InlineToolbar when we selected any Block */
this.Editor.InlineToolbar.close();
}
/**
* Clear anyBlockSelected cache
*/
public clearCache(): void {
this.anyBlockSelectedCache = null;
}
/**
* Module destruction
* De-registers Shortcut CMD+A

View file

@ -330,10 +330,10 @@ export default class Caret extends Module {
}
/**
* If last block is empty and it is an initialBlock, set to that.
* If last block is empty and it is an defaultBlock, set to that.
* Otherwise, append new empty block and set to that
*/
if (this.Editor.Tools.isInitial(lastBlock.tool) && lastBlock.isEmpty) {
if (this.Editor.Tools.isDefault(lastBlock.tool) && lastBlock.isEmpty) {
this.setToBlock(lastBlock);
} else {
const newBlock = this.Editor.BlockManager.insertAtEnd();
@ -355,12 +355,30 @@ export default class Caret extends Module {
selectRange.deleteContents();
if (currentBlockInput) {
const range = selectRange.cloneRange();
if ($.isNativeInput(currentBlockInput)) {
/**
* If input is native text input we need to use it's value
* Text before the caret stays in the input,
* while text after the caret is returned as a fragment to be inserted after the block.
*/
const input = currentBlockInput as HTMLInputElement | HTMLTextAreaElement;
const newFragment = document.createDocumentFragment();
range.selectNodeContents(currentBlockInput);
range.setStart(selectRange.endContainer, selectRange.endOffset);
const inputRemainingText = input.value.substring(0, input.selectionStart);
const fragmentText = input.value.substring(input.selectionStart);
return range.extractContents();
newFragment.textContent = fragmentText;
input.value = inputRemainingText;
return newFragment;
} else {
const range = selectRange.cloneRange();
range.selectNodeContents(currentBlockInput);
range.setStart(selectRange.endContainer, selectRange.endOffset);
return range.extractContents();
}
}
}
}
@ -383,15 +401,15 @@ export default class Caret extends Module {
if (!nextBlock && !nextInput) {
/**
* If there is no nextBlock and currentBlock is initial, do not navigate
* If there is no nextBlock and currentBlock is default, do not navigate
*/
if (Tools.isInitial(currentBlock.tool)) {
if (Tools.isDefault(currentBlock.tool)) {
return false;
}
/**
* If there is no nextBlock, but currentBlock is not initial,
* insert new initial block at the end and navigate to it
* If there is no nextBlock, but currentBlock is not default,
* insert new default block at the end and navigate to it
*/
nextBlock = BlockManager.insertAtEnd();
}
@ -505,6 +523,13 @@ export default class Caret extends Module {
Array.from(wrapper.childNodes).forEach((child: Node) => fragment.appendChild(child));
/**
* If there is no child node, append empty one
*/
if (fragment.childNodes.length === 0) {
fragment.appendChild(new Text(''));
}
const lastChild = fragment.lastChild;
range.deleteContents();

View file

@ -17,6 +17,19 @@ export default class CrossBlockSelection extends Module {
*/
private lastSelectedBlock: Block;
/**
* Module preparation
*
* @returns {Promise}
*/
public async prepare(): Promise<void> {
const { Listeners } = this.Editor;
Listeners.on(document, 'mousedown', (event: MouseEvent) => {
this.enableCrossBlockSelection(event);
});
}
/**
* Sets up listeners
*
@ -51,7 +64,7 @@ export default class CrossBlockSelection extends Module {
* @param {boolean} next - if true, toggle next block. Previous otherwise
*/
public toggleBlockSelectedState(next = true): void {
const { BlockManager } = this.Editor;
const { BlockManager, BlockSelection } = this.Editor;
if (!this.lastSelectedBlock) {
this.lastSelectedBlock = this.firstSelectedBlock = BlockManager.currentBlock;
@ -59,6 +72,8 @@ export default class CrossBlockSelection extends Module {
if (this.firstSelectedBlock === this.lastSelectedBlock) {
this.firstSelectedBlock.selected = true;
BlockSelection.clearCache();
SelectionUtils.get().removeAllRanges();
}
@ -71,8 +86,12 @@ export default class CrossBlockSelection extends Module {
if (this.lastSelectedBlock.selected !== nextBlock.selected) {
nextBlock.selected = true;
BlockSelection.clearCache();
} else {
this.lastSelectedBlock.selected = false;
BlockSelection.clearCache();
}
this.lastSelectedBlock = nextBlock;
@ -120,6 +139,34 @@ export default class CrossBlockSelection extends Module {
this.firstSelectedBlock = this.lastSelectedBlock = null;
}
/**
* Enables Cross Block Selection
*
* @param {MouseEvent} event - mouse down event
*/
private enableCrossBlockSelection(event: MouseEvent): void {
const { UI } = this.Editor;
/**
* Each mouse down on must disable selectAll state
*/
if (!SelectionUtils.isCollapsed) {
this.Editor.BlockSelection.clearSelection(event);
}
/**
* If mouse down is performed inside the editor, we should watch CBS
*/
if (UI.nodes.redactor.contains(event.target as Node)) {
this.watchSelection(event);
} else {
/**
* Otherwise, clear selection
*/
this.Editor.BlockSelection.clearSelection(event);
}
}
/**
* Mouse up event handler.
* Removes the listeners
@ -138,7 +185,7 @@ export default class CrossBlockSelection extends Module {
* @param {MouseEvent} event - mouse over event
*/
private onMouseOver = (event: MouseEvent): void => {
const { BlockManager } = this.Editor;
const { BlockManager, BlockSelection } = this.Editor;
const relatedBlock = BlockManager.getBlockByChildNode(event.relatedTarget as Node) || this.lastSelectedBlock;
const targetBlock = BlockManager.getBlockByChildNode(event.target as Node);
@ -157,6 +204,8 @@ export default class CrossBlockSelection extends Module {
relatedBlock.selected = true;
targetBlock.selected = true;
BlockSelection.clearCache();
return;
}
@ -164,6 +213,8 @@ export default class CrossBlockSelection extends Module {
relatedBlock.selected = false;
targetBlock.selected = false;
BlockSelection.clearCache();
return;
}
@ -180,7 +231,7 @@ export default class CrossBlockSelection extends Module {
* @param {Block} lastBlock - last block in range
*/
private toggleBlocksSelectedState(firstBlock: Block, lastBlock: Block): void {
const { BlockManager } = this.Editor;
const { BlockManager, BlockSelection } = this.Editor;
const fIndex = BlockManager.blocks.indexOf(firstBlock);
const lIndex = BlockManager.blocks.indexOf(lastBlock);
@ -199,6 +250,8 @@ export default class CrossBlockSelection extends Module {
block !== (shouldntSelectFirstBlock ? firstBlock : lastBlock)
) {
BlockManager.blocks[i].selected = !BlockManager.blocks[i].selected;
BlockSelection.clearCache();
}
}
}

View file

@ -14,30 +14,51 @@ export default class DragNDrop extends Module {
private isStartedAtEditor = false;
/**
* Bind events
* Toggle read-only state
*
* if state is true:
* - disable all drag-n-drop event handlers
*
* if state is false:
* - restore drag-n-drop event handlers
*
* @param {boolean} readOnlyEnabled - "read only" state
*/
public prepare(): void {
this.bindEvents();
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (readOnlyEnabled) {
this.disableModuleBindings();
} else {
this.enableModuleBindings();
}
}
/**
* Add drag events listeners to editor zone
*
* @private
*/
private bindEvents(): void {
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'drop', this.processDrop, true);
private enableModuleBindings(): void {
const { UI } = this.Editor;
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragstart', (dragEvent: DragEvent) => {
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {
this.isStartedAtEditor = true;
}
this.readOnlyMutableListeners.on(UI.nodes.holder, 'drop', async (dropEvent: DragEvent) => {
await this.processDrop(dropEvent);
}, true);
this.Editor.InlineToolbar.close();
this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragstart', () => {
this.processDragStart();
});
/* Prevent default browser behavior to allow drop on non-contenteditable elements */
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragover', (e) => e.preventDefault(), true);
/**
* Prevent default browser behavior to allow drop on non-contenteditable elements
*/
this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragover', (dragEvent: DragEvent) => {
this.processDragOver(dragEvent);
}, true);
}
/**
* Unbind drag-n-drop event handlers
*/
private disableModuleBindings(): void {
this.readOnlyMutableListeners.clearAll();
}
/**
@ -45,7 +66,7 @@ export default class DragNDrop extends Module {
*
* @param {DragEvent} dropEvent - drop event
*/
private processDrop = async (dropEvent: DragEvent): Promise<void> => {
private async processDrop(dropEvent: DragEvent): Promise<void> {
const {
BlockManager,
Caret,
@ -78,6 +99,24 @@ export default class DragNDrop extends Module {
this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END);
}
Paste.processDataTransfer(dropEvent.dataTransfer, true);
await Paste.processDataTransfer(dropEvent.dataTransfer, true);
}
/**
* Handle drag start event
*/
private processDragStart(): void {
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {
this.isStartedAtEditor = true;
}
this.Editor.InlineToolbar.close();
}
/**
* @param {DragEvent} dragEvent - drag event
*/
private processDragOver(dragEvent: DragEvent): void {
dragEvent.preventDefault();
}
}

View file

@ -1,4 +1,5 @@
import Module from '../__module';
import * as _ from '../utils';
/**
* Event listener information
@ -6,6 +7,11 @@ import Module from '../__module';
* @interface ListenerData
*/
export interface ListenerData {
/**
* Listener unique identifier
*/
id: string;
/**
* Element where to listen to dispatched events
*/
@ -53,20 +59,24 @@ export default class Listeners extends Module {
private allListeners: ListenerData[] = [];
/**
* Assigns event listener on element
* Assigns event listener on element and returns unique identifier
*
* @param {EventTarget} element - DOM element that needs to be listened
* @param {string} eventType - event type
* @param {Function} handler - method that will be fired on event
* @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}
*
* @returns {string}
*/
public on(
element: EventTarget,
eventType: string,
handler: (event: Event) => void,
options: boolean | AddEventListenerOptions = false
): void {
): string {
const id = _.generateId('l');
const assignedEventData = {
id,
element,
eventType,
handler,
@ -81,6 +91,8 @@ export default class Listeners extends Module {
this.allListeners.push(assignedEventData);
element.addEventListener(eventType, handler, options);
return id;
}
/**
@ -110,6 +122,21 @@ export default class Listeners extends Module {
});
}
/**
* Removes listener by id
*
* @param {string} id - listener identifier
*/
public offById(id: string): void {
const listener = this.findById(id);
if (!listener) {
return;
}
listener.element.removeEventListener(listener.eventType, listener.handler, listener.options);
}
/**
* Finds and returns first listener by passed params
*
@ -211,4 +238,15 @@ export default class Listeners extends Module {
}
});
}
/**
* Returns listener data found by id
*
* @param {string} id - listener identifier
*
* @returns {ListenerData}
*/
private findById(id: string): ListenerData {
return this.allListeners.find((listener) => listener.id === id);
}
}

View file

@ -28,7 +28,7 @@ export default class ModificationsObserver extends Module {
/**
* Allows to temporary disable mutations handling
*/
private disabled: boolean;
private disabled = false;
/**
* Used to prevent several mutation callback execution
@ -37,7 +37,10 @@ export default class ModificationsObserver extends Module {
*/
private mutationDebouncer = _.debounce(() => {
this.updateNativeInputs();
this.config.onChange(this.Editor.API.methods);
if (_.isFunction(this.config.onChange)) {
this.config.onChange(this.Editor.API.methods);
}
}, ModificationsObserver.DebounceTimer);
/**
@ -56,20 +59,20 @@ export default class ModificationsObserver extends Module {
}
this.observer = null;
this.nativeInputs.forEach((input) => this.Editor.Listeners.off(input, 'input', this.mutationDebouncer));
this.mutationDebouncer = null;
}
/**
* Preparation method
* Set read-only state
*
* @returns {Promise<void>}
* @param {boolean} readOnlyEnabled - read only flag value
*/
public async prepare(): Promise<void> {
/**
* wait till Browser render Editor's Blocks
*/
window.setTimeout(() => {
this.setObserver();
}, 1000);
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (readOnlyEnabled) {
this.disableModule();
} else {
this.enableModule();
}
}
/**
@ -116,7 +119,7 @@ export default class ModificationsObserver extends Module {
* @param {MutationRecord[]} mutationList - list of mutations
* @param {MutationObserver} observer - observer instance
*/
private mutationHandler(mutationList: MutationRecord[], observer): void {
private mutationHandler(mutationList: MutationRecord[], observer: MutationObserver): void {
/**
* Skip mutations in stealth mode
*/
@ -168,4 +171,25 @@ export default class ModificationsObserver extends Module {
this.nativeInputs.forEach((input) => this.Editor.Listeners.on(input, 'input', this.mutationDebouncer));
}
/**
* Sets observer and enables it
*/
private enableModule(): void {
/**
* wait till Browser render Editor's Blocks
*/
window.setTimeout(() => {
this.setObserver();
this.updateNativeInputs();
this.enable();
}, 1000);
}
/**
* Disables observer
*/
private disableModule(): void {
this.disable();
}
}

View file

@ -9,7 +9,7 @@ import {
PasteEventDetail
} from '../../../types';
import Block from '../block';
import { SavedData } from '../../types-internal/block-data';
import { SavedData } from '../../../types/data-formats';
/**
* Tag substitute object.
@ -141,14 +141,24 @@ export default class Paste extends Module {
/**
* Set onPaste callback and collect tools` paste configurations
*
* @public
*/
public async prepare(): Promise<void> {
this.setCallback();
this.processTools();
}
/**
* Set read-only state
*
* @param {boolean} readOnlyEnabled - read only flag value
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (!readOnlyEnabled) {
this.setCallback();
} else {
this.unsetCallback();
}
}
/**
* Handle pasted or dropped data transfer object
*
@ -237,8 +247,8 @@ export default class Paste extends Module {
return;
}
const isCurrentBlockInitial = BlockManager.currentBlock && Tools.isInitial(BlockManager.currentBlock.tool);
const needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;
const isCurrentBlockDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool);
const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;
dataToInsert.map(
async (content, i) => this.insertBlock(content, i === 0 && needToReplaceCurrentBlock)
@ -258,6 +268,15 @@ export default class Paste extends Module {
Listeners.on(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);
}
/**
* Unset onPaste callback handler
*/
private unsetCallback(): void {
const { Listeners } = this.Editor;
Listeners.off(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);
}
/**
* Get and process tool`s paste configs
*/
@ -276,6 +295,7 @@ export default class Paste extends Module {
api: this.Editor.API.getMethodsForTool(name),
config: {},
data: {},
readOnly: false,
}) as BlockTool;
if (tool.pasteConfig === false) {
@ -427,7 +447,7 @@ export default class Paste extends Module {
}
/**
* If Tools is in list of exceptions, skip processing of paste event
* If Tools is in list of errors, skip processing of paste event
*/
if (BlockManager.currentBlock && this.exceptionList.includes(BlockManager.currentBlock.name)) {
return;
@ -457,8 +477,8 @@ export default class Paste extends Module {
);
dataToInsert = dataToInsert.filter((data) => !!data);
const isCurrentBlockInitial = Tools.isInitial(BlockManager.currentBlock.tool);
const needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;
const isCurrentBlockDefault = Tools.isDefault(BlockManager.currentBlock.tool);
const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;
dataToInsert.forEach(
(data, i) => {
@ -514,7 +534,7 @@ export default class Paste extends Module {
*/
private processHTML(innerHTML: string): PasteData[] {
const { Tools, Sanitizer } = this.Editor;
const initialTool = this.config.initialBlock;
const initialTool = this.config.defaultBlock;
const wrapper = $.make('DIV');
wrapper.innerHTML = innerHTML;
@ -576,13 +596,13 @@ export default class Paste extends Module {
* @returns {PasteData[]}
*/
private processPlain(plain: string): PasteData[] {
const { initialBlock } = this.config as {initialBlock: string};
const { defaultBlock } = this.config as {defaultBlock: string};
if (!plain) {
return [];
}
const tool = initialBlock;
const tool = defaultBlock;
return plain
.split(/\r?\n/)
@ -622,7 +642,7 @@ export default class Paste extends Module {
dataToInsert.tool !== currentBlock.name ||
!$.containsOnlyInlineElements(dataToInsert.content.innerHTML)
) {
this.insertBlock(dataToInsert, currentBlock && Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty);
this.insertBlock(dataToInsert, currentBlock && Tools.isDefault(currentBlock.tool) && currentBlock.isEmpty);
return;
}
@ -642,14 +662,14 @@ export default class Paste extends Module {
const { BlockManager, Caret, Sanitizer, Tools } = this.Editor;
const { content } = dataToInsert;
const currentBlockIsInitial = BlockManager.currentBlock && Tools.isInitial(BlockManager.currentBlock.tool);
const currentBlockIsDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool);
if (currentBlockIsInitial && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {
if (currentBlockIsDefault && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {
const blockData = await this.processPattern(content.textContent);
if (blockData) {
const needToReplaceCurrentBlock = BlockManager.currentBlock &&
Tools.isInitial(BlockManager.currentBlock.tool) &&
Tools.isDefault(BlockManager.currentBlock.tool) &&
BlockManager.currentBlock.isEmpty;
const insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock);
@ -746,9 +766,9 @@ export default class Paste extends Module {
let needToReplaceCurrentBlock = false;
if (i === 0) {
const isCurrentBlockInitial = BlockManager.currentBlock && Tools.isInitial(BlockManager.currentBlock.tool);
const isCurrentBlockDefault = BlockManager.currentBlock && Tools.isDefault(BlockManager.currentBlock.tool);
needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;
needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;
}
BlockManager.insert({

View file

@ -0,0 +1,113 @@
import Module from '../__module';
import { CriticalError } from '../errors/critical';
/**
* @module ReadOnly
*
* Has one important method:
* - {Function} toggleReadonly - Set read-only mode or toggle current state
*
* @version 1.0.0
*
* @typedef {ReadOnly} ReadOnly
* @property {boolean} readOnlyEnabled - read-only state
*/
export default class ReadOnly extends Module {
/**
* Array of tools name which don't support read-only mode
*/
private toolsDontSupportReadOnly: string[] = [];
/**
* Value to track read-only state
*
* @type {boolean}
*/
private readOnlyEnabled = false;
/**
* Returns state of read only mode
*/
public get isEnabled(): boolean {
return this.readOnlyEnabled;
}
/**
* Set initial state
*/
public async prepare(): Promise<void> {
const { Tools } = this.Editor;
const { blockTools } = Tools;
const toolsDontSupportReadOnly: string[] = [];
Object.entries(blockTools).forEach(([name, tool]) => {
if (!Tools.isReadOnlySupported(tool)) {
toolsDontSupportReadOnly.push(name);
}
});
this.toolsDontSupportReadOnly = toolsDontSupportReadOnly;
if (this.config.readOnly && toolsDontSupportReadOnly.length > 0) {
this.throwCriticalError();
}
this.toggle(this.config.readOnly);
}
/**
* Set read-only mode or toggle current state
* Call all Modules `toggleReadOnly` method and re-render Editor
*
* @param {boolean} state - (optional) read-only state or toggle
*/
public async toggle(state = !this.readOnlyEnabled): Promise<boolean> {
if (state && this.toolsDontSupportReadOnly.length > 0) {
this.throwCriticalError();
}
const oldState = this.readOnlyEnabled;
this.readOnlyEnabled = state;
for (const name in this.Editor) {
/**
* Verify module has method `toggleReadOnly` method
*/
if (!this.Editor[name].toggleReadOnly) {
continue;
}
/**
* set or toggle read-only state
*/
this.Editor[name].toggleReadOnly(state);
}
/**
* If new state equals old one, do not re-render blocks
*/
if (oldState === state) {
return this.readOnlyEnabled;
}
/**
* Save current Editor Blocks and render again
*/
const savedBlocks = await this.Editor.Saver.save();
await this.Editor.BlockManager.clear();
await this.Editor.Renderer.render(savedBlocks.blocks);
return this.readOnlyEnabled;
}
/**
* Throws an error about tools which don't support read-only mode
*/
private throwCriticalError(): never {
throw new CriticalError(
`To enable read-only mode all connected tools should support it. Tools ${this.toolsDontSupportReadOnly.join(', ')} don't support read-only mode.`
);
}
}

View file

@ -96,38 +96,17 @@ export default class RectangleSelection extends Module {
*/
private overlayRectangle: HTMLDivElement;
/**
* Listener identifiers
*/
private listenerIds: string[] = [];
/**
* Module Preparation
* Creating rect and hang handlers
*/
public prepare(): void {
const { Listeners } = this.Editor;
const { container } = this.genHTML();
Listeners.on(container, 'mousedown', (event: MouseEvent) => {
if (event.button !== this.MAIN_MOUSE_BUTTON) {
return;
}
this.startSelection(event.pageX, event.pageY);
}, false);
Listeners.on(document.body, 'mousemove', (event: MouseEvent) => {
this.changingRectangle(event);
this.scrollByZones(event.clientY);
}, false);
Listeners.on(document.body, 'mouseleave', () => {
this.clearSelection();
this.endSelection();
});
Listeners.on(window, 'scroll', (event) => {
this.changingRectangle(event);
}, false);
Listeners.on(document.body, 'mouseup', () => {
this.endSelection();
}, false);
this.enableModuleBindings();
}
/**
@ -196,6 +175,78 @@ export default class RectangleSelection extends Module {
this.isRectSelectionActivated = false;
}
/**
* Sets Module necessary event handlers
*/
private enableModuleBindings(): void {
const { Listeners } = this.Editor;
const { container } = this.genHTML();
Listeners.on(container, 'mousedown', (mouseEvent: MouseEvent) => {
this.processMouseDown(mouseEvent);
}, false);
Listeners.on(document.body, 'mousemove', (mouseEvent: MouseEvent) => {
this.processMouseMove(mouseEvent);
}, false);
Listeners.on(document.body, 'mouseleave', () => {
this.processMouseLeave();
});
Listeners.on(window, 'scroll', (mouseEvent: MouseEvent) => {
this.processScroll(mouseEvent);
}, false);
Listeners.on(document.body, 'mouseup', () => {
this.processMouseUp();
}, false);
}
/**
* Handle mouse down events
*
* @param {MouseEvent} mouseEvent - mouse event payload
*/
private processMouseDown(mouseEvent: MouseEvent): void {
if (mouseEvent.button !== this.MAIN_MOUSE_BUTTON) {
return;
}
this.startSelection(mouseEvent.pageX, mouseEvent.pageY);
}
/**
* Handle mouse move events
*
* @param {MouseEvent} mouseEvent - mouse event payload
*/
private processMouseMove(mouseEvent: MouseEvent): void {
this.changingRectangle(mouseEvent);
this.scrollByZones(mouseEvent.clientY);
}
/**
* Handle mouse leave
*/
private processMouseLeave(): void {
this.clearSelection();
this.endSelection();
}
/**
* @param {MouseEvent} mouseEvent - mouse event payload
*/
private processScroll(mouseEvent: MouseEvent): void {
this.changingRectangle(mouseEvent);
}
/**
* Handle mouse up
*/
private processMouseUp(): void {
this.endSelection();
}
/**
* Scroll If mouse in scroll zone
*
@ -270,7 +321,7 @@ export default class RectangleSelection extends Module {
*
* @param {MouseEvent} event - mouse event
*/
private changingRectangle(event): void {
private changingRectangle(event: MouseEvent): void {
if (!this.mousedown) {
return;
}

View file

@ -1,7 +1,5 @@
import Module from '../__module';
/* eslint-disable import/no-duplicates */
import * as _ from '../utils';
import { ChainData } from '../utils';
import { BlockToolConstructable, OutputBlockData } from '../../../types';
/**
@ -47,7 +45,7 @@ export default class Renderer extends Module {
public async render(blocks: OutputBlockData[]): Promise<void> {
const chainData = blocks.map((block) => ({ function: (): Promise<void> => this.insertBlock(block) }));
const sequence = await _.sequence(chainData as ChainData[]);
const sequence = await _.sequence(chainData as _.ChainData[]);
this.Editor.UI.checkEmptiness();
@ -60,6 +58,7 @@ export default class Renderer extends Module {
* Insert block to working zone
*
* @param {object} item - Block data to insert
*
* @returns {Promise<void>}
*/
public async insertBlock(item: OutputBlockData): Promise<void> {
@ -91,7 +90,7 @@ export default class Renderer extends Module {
const toolToolboxSettings = (Tools.unavailable[tool] as BlockToolConstructable).toolbox;
const userToolboxSettings = Tools.getToolSettings(tool).toolbox;
stubData.title = toolToolboxSettings.title || userToolboxSettings.title || stubData.title;
stubData.title = toolToolboxSettings.title || (userToolboxSettings && userToolboxSettings.title) || stubData.title;
}
const stub = BlockManager.insert({

View file

@ -37,7 +37,7 @@ import * as _ from '../utils';
import HTMLJanitor from 'html-janitor';
import { BlockToolData, InlineToolConstructable, SanitizerConfig } from '../../../types';
import { SavedData } from '../../types-internal/block-data';
import { SavedData } from '../../../types/data-formats';
/**
*
@ -309,7 +309,7 @@ export default class Sanitizer extends Module {
* @param {SanitizerConfig} config - config to check
*/
private isRule(config: SanitizerConfig): boolean {
return typeof config === 'object' || typeof config === 'boolean' || typeof config === 'function';
return typeof config === 'object' || typeof config === 'boolean' || _.isFunction(config);
}
/**

View file

@ -7,7 +7,7 @@
*/
import Module from '../__module';
import { OutputData } from '../../../types';
import { ValidatedData } from '../../types-internal/block-data';
import { ValidatedData } from '../../../types/data-formats';
import Block from '../block';
import * as _ from '../utils';
@ -36,16 +36,18 @@ export default class Saver extends Module {
*/
ModificationsObserver.disable();
blocks.forEach((block: Block) => {
chainData.push(this.getSavedData(block));
});
try {
blocks.forEach((block: Block) => {
chainData.push(this.getSavedData(block));
});
const extractedData = await Promise.all(chainData);
const sanitizedData = await Sanitizer.sanitizeBlocks(extractedData);
const extractedData = await Promise.all(chainData);
const sanitizedData = await Sanitizer.sanitizeBlocks(extractedData);
ModificationsObserver.enable();
return this.makeOutput(sanitizedData);
return this.makeOutput(sanitizedData);
} finally {
ModificationsObserver.enable();
}
}
/**

View file

@ -64,6 +64,10 @@ export default class Shortcuts extends Module {
public remove(shortcut: string): void {
const index = this.registeredShortcuts.findIndex((shc) => shc.name === shortcut);
if (index === -1 || !this.registeredShortcuts[index]) {
return;
}
this.registeredShortcuts[index].remove();
this.registeredShortcuts.splice(index, 1);
}

View file

@ -4,6 +4,15 @@ import Flipper, { FlipperOptions } from '../../flipper';
import * as _ from '../../utils';
import SelectionUtils from '../../selection';
/**
* HTML Elements that used for BlockSettings
*/
interface BlockSettingsNodes {
wrapper: HTMLElement;
toolSettings: HTMLElement;
defaultSettings: HTMLElement;
}
/**
* Block Settings
*
@ -15,7 +24,7 @@ import SelectionUtils from '../../selection';
* | ...................... |
* |________________________|
*/
export default class BlockSettings extends Module {
export default class BlockSettings extends Module<BlockSettingsNodes> {
/**
* Module Events
*
@ -57,15 +66,6 @@ export default class BlockSettings extends Module {
return this.nodes.wrapper.classList.contains(this.CSS.wrapperOpened);
}
/**
* Block settings UI HTML elements
*/
public nodes: {[key: string]: HTMLElement} = {
wrapper: null,
toolSettings: null,
defaultSettings: null,
};
/**
* List of buttons
*/
@ -103,6 +103,15 @@ export default class BlockSettings extends Module {
this.enableFlipper();
}
/**
* Destroys module
*/
public destroy(): void {
this.flipper.deactivate();
this.flipper = null;
this.removeAllNodes();
}
/**
* Open Block Settings pane
*/
@ -119,6 +128,7 @@ export default class BlockSettings extends Module {
* Highlight content of a Block we are working with
*/
this.Editor.BlockManager.currentBlock.selected = true;
this.Editor.BlockSelection.clearCache();
/**
* Fill Tool's settings
@ -206,7 +216,7 @@ export default class BlockSettings extends Module {
* Add Tool's settings
*/
private addToolSettings(): void {
if (typeof this.Editor.BlockManager.currentBlock.tool.renderSettings === 'function') {
if (_.isFunction(this.Editor.BlockManager.currentBlock.tool.renderSettings)) {
$.append(this.nodes.toolSettings, this.Editor.BlockManager.currentBlock.tool.renderSettings());
}
}

View file

@ -2,15 +2,23 @@ import Module from '../../__module';
import $ from '../../dom';
import { BlockToolConstructable } from '../../../../types';
import * as _ from '../../utils';
import { SavedData } from '../../../types-internal/block-data';
import { SavedData } from '../../../../types/data-formats';
import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* HTML Elements used for ConversionToolbar
*/
interface ConversionToolbarNodes {
wrapper: HTMLElement;
tools: HTMLElement;
}
/**
* Block Converter
*/
export default class ConversionToolbar extends Module {
export default class ConversionToolbar extends Module<ConversionToolbarNodes> {
/**
* CSS getter
*/
@ -29,14 +37,6 @@ export default class ConversionToolbar extends Module {
};
}
/**
* HTML Elements used for UI
*/
public nodes: { [key: string]: HTMLElement } = {
wrapper: null,
tools: null,
};
/**
* Conversion Toolbar open/close state
*
@ -65,7 +65,10 @@ export default class ConversionToolbar extends Module {
* Create UI of Conversion Toolbar
*/
public make(): HTMLElement {
this.nodes.wrapper = $.make('div', ConversionToolbar.CSS.conversionToolbarWrapper);
this.nodes.wrapper = $.make('div', [
ConversionToolbar.CSS.conversionToolbarWrapper,
...(this.isRtl ? [ this.Editor.UI.CSS.editorRtlFix ] : []),
]);
this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);
const label = $.make('div', ConversionToolbar.CSS.conversionToolbarLabel, {
@ -88,6 +91,15 @@ export default class ConversionToolbar extends Module {
return this.nodes.wrapper;
}
/**
* Deactivates flipper and removes all nodes
*/
public destroy(): void {
this.flipper.deactivate();
this.flipper = null;
this.removeAllNodes();
}
/**
* Toggle conversion dropdown visibility
*
@ -100,7 +112,7 @@ export default class ConversionToolbar extends Module {
this.close();
}
if (typeof togglingCallback === 'function') {
if (_.isFunction(togglingCallback)) {
this.togglingCallback = togglingCallback;
}
}
@ -124,7 +136,7 @@ export default class ConversionToolbar extends Module {
}));
this.flipper.focusFirst();
if (typeof this.togglingCallback === 'function') {
if (_.isFunction(this.togglingCallback)) {
this.togglingCallback(true);
}
}, 50);
@ -138,7 +150,7 @@ export default class ConversionToolbar extends Module {
this.flipper.deactivate();
this.nodes.wrapper.classList.remove(ConversionToolbar.CSS.conversionToolbarShowed);
if (typeof this.togglingCallback === 'function') {
if (_.isFunction(this.togglingCallback)) {
this.togglingCallback(false);
}
}
@ -149,7 +161,7 @@ export default class ConversionToolbar extends Module {
public hasTools(): boolean {
const tools = Object.keys(this.tools); // available tools in array representation
return !(tools.length === 1 && tools.shift() === this.config.initialBlock);
return !(tools.length === 1 && tools.shift() === this.config.defaultBlock);
}
/**
@ -158,7 +170,7 @@ export default class ConversionToolbar extends Module {
*
* @param {string} replacingToolName - name of Tool which replaces current
*/
public async replaceWithBlock(replacingToolName: string): Promise <void> {
public async replaceWithBlock(replacingToolName: string): Promise<void> {
/**
* At first, we get current Block data
*
@ -172,10 +184,10 @@ export default class ConversionToolbar extends Module {
/**
* When current Block name is equals to the replacing tool Name,
* than convert this Block back to the initial Block
* than convert this Block back to the default Block
*/
if (currentBlockName === replacingToolName) {
replacingToolName = this.config.initialBlock;
replacingToolName = this.config.defaultBlock;
}
/**
@ -195,7 +207,7 @@ export default class ConversionToolbar extends Module {
let exportData = '';
const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export;
if (typeof exportProp === 'function') {
if (_.isFunction(exportProp)) {
exportData = exportProp(blockData);
} else if (typeof exportProp === 'string') {
exportData = blockData[exportProp];
@ -222,7 +234,7 @@ export default class ConversionToolbar extends Module {
let newBlockData = {};
const importProp = replacingTool[INTERNAL_SETTINGS.CONVERSION_CONFIG].import;
if (typeof importProp === 'function') {
if (_.isFunction(importProp)) {
newBlockData = importProp(cleaned);
} else if (typeof importProp === 'string') {
newBlockData[importProp] = cleaned;
@ -264,10 +276,15 @@ export default class ConversionToolbar extends Module {
const toolToolboxSettings = toolClass[internalSettings.TOOLBOX];
const conversionConfig = toolClass[internalSettings.CONVERSION_CONFIG];
const userSettings = this.Editor.Tools.USER_SETTINGS;
const userToolboxSettings = this.Editor.Tools.getToolSettings(toolName)[userSettings.TOOLBOX];
const toolboxSettings = userToolboxSettings ?? toolToolboxSettings;
/**
* Skip tools that don't pass 'toolbox' property
*/
if (_.isEmpty(toolToolboxSettings) || !toolToolboxSettings.icon) {
if (_.isEmpty(toolboxSettings) || !toolboxSettings.icon) {
continue;
}
@ -278,7 +295,7 @@ export default class ConversionToolbar extends Module {
continue;
}
this.addTool(toolName, toolToolboxSettings.icon, toolToolboxSettings.title);
this.addTool(toolName, toolboxSettings.icon, toolboxSettings.title);
}
}

View file

@ -4,6 +4,21 @@ import * as _ from '../../utils';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* HTML Elements used for Toolbar UI
*/
interface ToolbarNodes {
wrapper: HTMLElement;
content: HTMLElement;
actions: HTMLElement;
// Content Zone
plusButton: HTMLElement;
// Actions Zone
blockActionsButtons: HTMLElement;
settingsToggler: HTMLElement;
}
/**
*
* «Toolbar» is the node that moves up/down over current block
@ -56,23 +71,7 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
* @property {Element} nodes.pluginSettings - Plugin Settings section of Settings Panel
* @property {Element} nodes.defaultSettings - Default Settings section of Settings Panel
*/
export default class Toolbar extends Module {
/**
* HTML Elements used for Toolbar UI
*/
public nodes: {[key: string]: HTMLElement} = {
wrapper: null,
content: null,
actions: null,
// Content Zone
plusButton: null,
// Actions Zone
blockActionsButtons: null,
settingsToggler: null,
};
export default class Toolbar extends Module<ToolbarNodes> {
/**
* CSS styles
*
@ -99,89 +98,60 @@ export default class Toolbar extends Module {
}
/**
* Makes toolbar
* Returns the Toolbar opening state
*
* @returns {boolean}
*/
public make(): void {
this.nodes.wrapper = $.make('div', this.CSS.toolbar);
public get opened(): boolean {
return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);
}
/**
* Make Content Zone and Actions Zone
*/
['content', 'actions'].forEach((el) => {
this.nodes[el] = $.make('div', this.CSS[el]);
});
/**
* Plus Button public methods
*
* @returns {{hide: function(): void, show: function(): void}}
*/
public get plusButton(): {hide: () => void; show: () => void} {
return {
hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
show: (): void => {
if (this.Editor.Toolbox.isEmpty) {
return;
}
this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden);
},
};
}
/**
* Actions will be included to the toolbar content so we can align in to the right of the content
*/
$.append(this.nodes.wrapper, this.nodes.content);
$.append(this.nodes.content, this.nodes.actions);
/**
* Block actions appearance manipulations
*
* @returns {{hide: function(): void, show: function(): void}}
*/
private get blockActions(): {hide: () => void; show: () => void} {
return {
hide: (): void => {
this.nodes.actions.classList.remove(this.CSS.actionsOpened);
},
show: (): void => {
this.nodes.actions.classList.add(this.CSS.actionsOpened);
},
};
}
/**
* Fill Content Zone:
* - Plus Button
* - 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
*/
const tooltipContent = $.make('div');
tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add')));
tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {
textContent: '⇥ Tab',
}));
this.Editor.Tooltip.onHover(this.nodes.plusButton, tooltipContent);
/**
* Make a Toolbox
*/
this.Editor.Toolbox.make();
/**
* Fill Actions Zone:
* - Settings Toggler
* - Remove Block Button
* - Settings Panel
*/
this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons);
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
const settingsIcon = $.svg('dots', 8, 8);
$.append(this.nodes.settingsToggler, settingsIcon);
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
this.Editor.Tooltip.onHover(
this.nodes.settingsToggler,
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
{
placement: 'top',
}
);
/**
* Make and append Settings Panel
*/
this.Editor.BlockSettings.make();
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
/**
* Append toolbar to the Editor
*/
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
/**
* Bind events on the Toolbar elements
*/
this.bindEvents();
/**
* Toggles read-only mode
*
* @param {boolean} readOnlyEnabled - read-only mode
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (!readOnlyEnabled) {
this.drawUI();
this.enableModuleBindings();
} else {
this.destroy();
this.disableModuleBindings();
}
}
/**
@ -251,15 +221,6 @@ export default class Toolbar extends Module {
}, 50)();
}
/**
* returns toolbar opened state
*
* @returns {boolean}
*/
public get opened(): boolean {
return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);
}
/**
* Close the Toolbar
*/
@ -273,36 +234,81 @@ export default class Toolbar extends Module {
}
/**
* Plus Button public methods
*
* @returns {{hide: function(): void, show: function(): void}}
* Draws Toolbar elements
*/
public get plusButton(): {hide: () => void; show: () => void} {
return {
hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
show: (): void => {
if (this.Editor.Toolbox.isEmpty) {
return;
}
this.nodes.plusButton.classList.remove(this.CSS.plusButtonHidden);
},
};
}
private make(): void {
this.nodes.wrapper = $.make('div', this.CSS.toolbar);
/**
* Block actions appearance manipulations
*
* @returns {{hide: function(): void, show: function(): void}}
*/
private get blockActions(): {hide: () => void; show: () => void} {
return {
hide: (): void => {
this.nodes.actions.classList.remove(this.CSS.actionsOpened);
},
show: (): void => {
this.nodes.actions.classList.add(this.CSS.actionsOpened);
},
};
/**
* Make Content Zone and Actions Zone
*/
['content', 'actions'].forEach((el) => {
this.nodes[el] = $.make('div', this.CSS[el]);
});
/**
* Actions will be included to the toolbar content so we can align in to the right of the content
*/
$.append(this.nodes.wrapper, this.nodes.content);
$.append(this.nodes.content, this.nodes.actions);
/**
* Fill Content Zone:
* - Plus Button
* - 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.readOnlyMutableListeners.on(this.nodes.plusButton, 'click', () => {
this.plusButtonClicked();
}, false);
/**
* Add events to show/hide tooltip for plus button
*/
const tooltipContent = $.make('div');
tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add')));
tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {
textContent: '⇥ Tab',
}));
this.Editor.Tooltip.onHover(this.nodes.plusButton, tooltipContent);
/**
* Fill Actions Zone:
* - Settings Toggler
* - Remove Block Button
* - Settings Panel
*/
this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons);
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
const settingsIcon = $.svg('dots', 8, 8);
$.append(this.nodes.settingsToggler, settingsIcon);
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
this.Editor.Tooltip.onHover(
this.nodes.settingsToggler,
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
{
placement: 'top',
}
);
/**
* Appending Toolbar components to itself
*/
$.append(this.nodes.content, this.Editor.Toolbox.nodes.toolbox);
$.append(this.nodes.actions, this.Editor.BlockSettings.nodes.wrapper);
/**
* Append toolbar to the Editor
*/
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
}
/**
@ -313,14 +319,22 @@ export default class Toolbar extends Module {
}
/**
* Bind events on the Toolbar Elements:
* - Block Settings
* Enable bindings
*/
private bindEvents(): void {
private enableModuleBindings(): void {
/**
* Settings toggler
*/
this.Editor.Listeners.on(this.nodes.settingsToggler, 'click', () => this.settingsTogglerClicked());
this.readOnlyMutableListeners.on(this.nodes.settingsToggler, 'click', () => {
this.settingsTogglerClicked();
});
}
/**
* Disable bindings
*/
private disableModuleBindings(): void {
this.readOnlyMutableListeners.clearAll();
}
/**
@ -333,4 +347,42 @@ export default class Toolbar extends Module {
this.Editor.BlockSettings.open();
}
}
/**
* Draws Toolbar UI
*
* Toolbar contains BlockSettings and Toolbox.
* Thats why at first we draw its components and then Toolbar itself
*
* Steps:
* - Make Toolbar dependent components like BlockSettings, Toolbox and so on
* - Make itself and append dependent nodes to itself
*
*/
private drawUI(): void {
/**
* Make BlockSettings Panel
*/
this.Editor.BlockSettings.make();
/**
* Make Toolbox
*/
this.Editor.Toolbox.make();
/**
* Make Toolbar
*/
this.make();
}
/**
* Removes all created and saved HTMLElements
* It is used in Read-Only mode
*/
private destroy(): void {
this.Editor.Toolbox.destroy();
this.Editor.BlockSettings.destroy();
this.removeAllNodes();
}
}

View file

@ -7,6 +7,22 @@ import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* Inline Toolbar elements
*/
interface InlineToolbarNodes {
wrapper: HTMLElement;
togglerAndButtonsWrapper: HTMLElement;
buttons: HTMLElement;
conversionToggler: HTMLElement;
conversionTogglerContent: HTMLElement;
/**
* 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
*/
actions: HTMLElement;
}
/**
* Inline toolbar with actions that modifies selected text fragment
*
@ -14,7 +30,7 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
* | B i [link] [mark] |
* |________________________|
*/
export default class InlineToolbar extends Module {
export default class InlineToolbar extends Module<InlineToolbarNodes> {
/**
* CSS styles
*/
@ -27,12 +43,12 @@ export default class InlineToolbar extends Module {
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',
togglerAndButtonsWrapper: 'ce-inline-toolbar__toggler-and-button-wrapper',
};
/**
@ -42,34 +58,13 @@ export default class InlineToolbar extends Module {
*/
public opened = false;
/**
* Inline Toolbar elements
*/
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
*/
actions: null,
};
/**
* Margin above/below the Toolbar
*/
private readonly toolbarVerticalMargin: number = 5;
/**
* Tools instances
* Currently visible tools instances
*/
private toolsInstances: Map<string, InlineTool>;
@ -93,76 +88,16 @@ export default class InlineToolbar extends Module {
private flipper: Flipper = null;
/**
* Inline Toolbar Tools
* Toggles read-only mode
*
* @returns {Map<string, InlineTool>}
* @param {boolean} readOnlyEnabled - read-only mode
*/
public get tools(): Map<string, InlineTool> {
if (!this.toolsInstances || this.toolsInstances.size === 0) {
const allTools = this.inlineTools;
this.toolsInstances = new Map();
for (const tool in allTools) {
if (Object.prototype.hasOwnProperty.call(allTools, tool)) {
this.toolsInstances.set(tool, allTools[tool]);
}
}
public toggleReadOnly(readOnlyEnabled: boolean): void {
if (!readOnlyEnabled) {
this.make();
} else {
this.destroy();
}
return this.toolsInstances;
}
/**
* Making DOM
*/
public make(): void {
this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);
this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);
this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
// To prevent reset of a selection when click on the wrapper
this.Editor.Listeners.on(this.nodes.wrapper, 'mousedown', (event) => {
const isClickedOnActionsWrapper = (event.target as Element).closest(`.${this.CSS.actionsWrapper}`);
// If click is on actions wrapper,
// do not prevent default behaviour because actions might include interactive elements
if (!isClickedOnActionsWrapper) {
event.preventDefault();
}
});
/**
* Append Inline Toolbar to the Editor
*/
$.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();
/**
* Prepare conversion toolbar.
* If it has any conversion tool then it will be enabled in the Inline Toolbar
*/
this.prepareConversionToolbar();
/**
* Recalculate initial width with all buttons
*/
this.recalculateWidth();
/**
* Allow to leaf buttons by arrows / tab
* Buttons will be filled on opening
*/
this.enableFlipper();
}
/**
@ -188,9 +123,6 @@ export default class InlineToolbar extends Module {
this.move();
this.open();
this.Editor.Toolbar.close();
/** Check Tools state for selected fragment */
this.checkToolsState();
}
/**
@ -243,9 +175,20 @@ export default class InlineToolbar extends Module {
* Hides Inline Toolbar
*/
public close(): void {
if (!this.opened) {
return;
}
if (this.Editor.ReadOnly.isEnabled) {
return;
}
this.nodes.wrapper.classList.remove(this.CSS.inlineToolbarShowed);
this.tools.forEach((toolInstance) => {
if (typeof toolInstance.clear === 'function') {
this.toolsInstances.forEach((toolInstance) => {
/**
* @todo replace 'clear' with 'destroy'
*/
if (_.isFunction(toolInstance.clear)) {
toolInstance.clear();
}
});
@ -260,25 +203,19 @@ export default class InlineToolbar extends Module {
* Shows Inline Toolbar
*/
public open(): void {
if (this.opened) {
return;
}
/**
* Filter inline-tools and show only allowed by Block's Tool
*/
this.filterTools();
this.addToolsFiltered();
/**
* Show Inline Toolbar
*/
this.nodes.wrapper.classList.add(this.CSS.inlineToolbarShowed);
/**
* Call 'clear' method for Inline Tools (for example, 'link' want to clear input)
*/
this.tools.forEach((toolInstance: InlineTool) => {
if (typeof toolInstance.clear === 'function') {
toolInstance.clear();
}
});
this.buttonsList = this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`);
this.opened = true;
@ -314,6 +251,154 @@ export default class InlineToolbar extends Module {
return this.nodes.wrapper.contains(node);
}
/**
* Removes UI and its components
*/
public destroy(): void {
this.flipper.deactivate();
this.flipper = null;
this.Editor.ConversionToolbar.destroy();
}
/**
* Returns inline toolbar settings for a particular tool
*
* @param {string} toolName - user specified name of tool
* @returns - array of ordered tool names or false
*/
private getInlineToolbarSettings(toolName): string[]|boolean {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
/**
* InlineToolbar property of a particular tool
*/
const settingsForTool = toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS];
/**
* Whether to enable IT for a particular tool is the decision of the editor user.
* He can enable it by the inlineToolbar settings for this tool. To enable, he should pass true or strings[]
*/
const enabledForTool = settingsForTool === true || Array.isArray(settingsForTool);
/**
* Disabled by user
*/
if (!enabledForTool) {
return false;
}
/**
* 1st priority.
*
* If user pass the list of inline tools for the particular tool, return it.
*/
if (Array.isArray(settingsForTool)) {
return settingsForTool;
}
/**
* 2nd priority.
*
* If user pass just 'true' for tool, get common inlineToolbar settings
* - if common settings is an array, use it
* - if common settings is 'true' or not specified, get default order
*/
/**
* Common inlineToolbar settings got from the root of EditorConfig
*/
const commonInlineToolbarSettings = this.config.inlineToolbar;
/**
* If common settings is an array, use it
*/
if (Array.isArray(commonInlineToolbarSettings)) {
return commonInlineToolbarSettings;
}
/**
* If common settings is 'true' or not specified (will be set as true at core.ts), get the default order
*/
if (commonInlineToolbarSettings === true) {
const defaultToolsOrder: string[] = Object.entries(this.Editor.Tools.available)
.filter(([name, tool]) => {
return tool[this.Editor.Tools.INTERNAL_SETTINGS.IS_INLINE];
})
.map(([name, tool]) => {
return name;
});
return defaultToolsOrder;
}
return false;
}
/**
* Making DOM
*/
private make(): void {
this.nodes.wrapper = $.make('div', [
this.CSS.inlineToolbar,
...(this.isRtl ? [ this.Editor.UI.CSS.editorRtlFix ] : []),
]);
/**
* Creates a different wrapper for toggler and buttons.
*/
this.nodes.togglerAndButtonsWrapper = $.make('div', this.CSS.togglerAndButtonsWrapper);
this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);
this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
// To prevent reset of a selection when click on the wrapper
this.Editor.Listeners.on(this.nodes.wrapper, 'mousedown', (event) => {
const isClickedOnActionsWrapper = (event.target as Element).closest(`.${this.CSS.actionsWrapper}`);
// If click is on actions wrapper,
// do not prevent default behaviour because actions might include interactive elements
if (!isClickedOnActionsWrapper) {
event.preventDefault();
}
});
/**
* Append the intermediary wrapper which contains toggler and buttons and button actions.
*/
$.append(this.nodes.wrapper, [this.nodes.togglerAndButtonsWrapper, this.nodes.actions]);
/**
* Append the inline toolbar to the editor.
*/
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
/**
* Add button that will allow switching block type
*/
this.addConversionToggler();
/**
* Wrapper for the inline tools
* Will be appended after the Conversion Toolbar toggler
*/
$.append(this.nodes.togglerAndButtonsWrapper, this.nodes.buttons);
/**
* Prepare conversion toolbar.
* If it has any conversion tool then it will be enabled in the Inline Toolbar
*/
this.prepareConversionToolbar();
/**
* Recalculate initial width with all buttons
*/
this.recalculateWidth();
/**
* Allow to leaf buttons by arrows / tab
* Buttons will be filled on opening
*/
this.enableFlipper();
}
/**
* Need to show Inline Toolbar or not
*/
@ -358,59 +443,12 @@ export default class InlineToolbar extends Module {
return false;
}
const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name);
return toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS];
}
/**
* Show only allowed Tools
*/
private filterTools(): void {
const currentSelection = SelectionUtils.get(),
currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name),
inlineToolbarSettings = toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS];
/**
* All Inline Toolbar buttons
*
* @type {HTMLElement[]}
* getInlineToolbarSettings could return an string[] (order of tools) or false (Inline Toolbar disabled).
*/
const buttons = Array.from(this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`)) as HTMLElement[];
const inlineToolbarSettings = this.getInlineToolbarSettings(currentBlock.name);
/**
* Show previously hided
*/
buttons.forEach((button) => {
button.hidden = false;
button.classList.remove(this.CSS.inlineToolButtonLast);
});
/**
* Filter buttons if Block Tool pass config like inlineToolbar=['link']
*/
if (Array.isArray(inlineToolbarSettings)) {
buttons.forEach((button) => {
button.hidden = !inlineToolbarSettings.includes(button.dataset.tool);
});
}
/**
* Tick for removing right-margin from last visible button.
* Current generation of CSS does not allow to filter :visible elements
*/
const lastVisibleButton = buttons.filter((button) => !button.hidden).pop();
if (lastVisibleButton) {
lastVisibleButton.classList.add(this.CSS.inlineToolButtonLast);
}
/**
* Recalculate width because some buttons can be hidden
*/
this.recalculateWidth();
return inlineToolbarSettings !== false;
}
/**
@ -433,7 +471,7 @@ export default class InlineToolbar extends Module {
this.nodes.conversionToggler.appendChild(this.nodes.conversionTogglerContent);
this.nodes.conversionToggler.appendChild(icon);
this.nodes.buttons.appendChild(this.nodes.conversionToggler);
this.nodes.togglerAndButtonsWrapper.appendChild(this.nodes.conversionToggler);
this.Editor.Listeners.on(this.nodes.conversionToggler, 'click', () => {
this.Editor.ConversionToolbar.toggle((conversionToolbarOpened) => {
@ -506,12 +544,40 @@ export default class InlineToolbar extends Module {
*/
/**
* Fill Inline Toolbar with Tools
* Append only allowed Tools
*/
private addTools(): void {
this.tools.forEach((toolInstance, toolName) => {
this.addTool(toolName, toolInstance);
private addToolsFiltered(): void {
const currentSelection = SelectionUtils.get();
const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
/**
* Clear buttons list
*/
this.nodes.buttons.innerHTML = '';
this.nodes.actions.innerHTML = '';
this.toolsInstances = new Map();
/**
* Filter buttons if Block Tool pass config like inlineToolbar=['link']
* Else filter them according to the default inlineToolbar property.
*
* For this moment, inlineToolbarOrder could not be 'false'
* because this method will be called only if the Inline Toolbar is enabled
*/
const inlineToolbarOrder = this.getInlineToolbarSettings(currentBlock.name) as string[];
inlineToolbarOrder.forEach((toolName) => {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const tool = this.Editor.Tools.constructInline(this.Editor.Tools.inline[toolName], toolName, toolSettings);
this.addTool(toolName, tool);
tool.checkState(SelectionUtils.get());
});
/**
* Recalculate width because some buttons can be hidden
*/
this.recalculateWidth();
}
/**
@ -537,8 +603,9 @@ export default class InlineToolbar extends Module {
button.dataset.tool = toolName;
this.nodes.buttons.appendChild(button);
this.toolsInstances.set(toolName, tool);
if (typeof tool.renderActions === 'function') {
if (_.isFunction(tool.renderActions)) {
const actions = tool.renderActions();
this.nodes.actions.appendChild(actions);
@ -665,7 +732,7 @@ export default class InlineToolbar extends Module {
* Check Tools` state by selection
*/
private checkToolsState(): void {
this.tools.forEach((toolInstance) => {
this.toolsInstances.forEach((toolInstance) => {
toolInstance.checkState(SelectionUtils.get());
});
}

View file

@ -1,12 +1,20 @@
import Module from '../../__module';
import $ from '../../dom';
import * as _ from '../../utils';
import { BlockToolConstructable } from '../../../../types';
import { BlockToolConstructable, ToolConstructable } from '../../../../types';
import Flipper from '../../flipper';
import { BlockToolAPI } from '../../block';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* HTMLElements used for Toolbox UI
*/
interface ToolboxNodes {
toolbox: HTMLElement;
buttons: HTMLElement[];
}
/**
* @class Toolbox
* @classdesc Holder for Tools
@ -17,7 +25,15 @@ import { I18nInternalNS } from '../../i18n/namespace-internal';
* @property {object} CSS - CSS class names
*
*/
export default class Toolbox extends Module {
export default class Toolbox extends Module<ToolboxNodes> {
/**
* Current module HTML Elements
*/
public nodes = {
toolbox: null,
buttons: [],
}
/**
* CSS styles
*
@ -52,17 +68,6 @@ export default class Toolbox extends Module {
*/
public opened = false;
/**
* HTMLElements used for Toolbox UI
*/
public nodes: {
toolbox: HTMLElement;
buttons: HTMLElement[];
} = {
toolbox: null,
buttons: [],
};
/**
* How many tools displayed in Toolbox
*
@ -82,12 +87,21 @@ export default class Toolbox extends Module {
*/
public make(): void {
this.nodes.toolbox = $.make('div', this.CSS.toolbox);
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
this.addTools();
this.enableFlipper();
}
/**
* Destroy Module
*/
public destroy(): void {
this.flipper.deactivate();
this.flipper = null;
this.removeAllNodes();
this.removeAllShortcuts();
}
/**
* Toolbox Tool's button click handler
*
@ -183,12 +197,19 @@ export default class Toolbox extends Module {
// return;
// }
const userToolboxSettings = this.Editor.Tools.getToolSettings(toolName)[userSettings.TOOLBOX] || {};
const userToolboxSettings = this.Editor.Tools.getToolSettings(toolName)[userSettings.TOOLBOX];
/**
* Hide Toolbox button if Toolbox settings is false
*/
if ((userToolboxSettings ?? toolToolboxSettings) === false) {
return;
}
const button = $.make('li', [ this.CSS.toolboxButton ]);
button.dataset.tool = toolName;
button.innerHTML = userToolboxSettings.icon || toolToolboxSettings.icon;
button.innerHTML = (userToolboxSettings && userToolboxSettings.icon) || toolToolboxSettings.icon;
$.append(this.nodes.toolbox, button);
@ -212,19 +233,34 @@ export default class Toolbox extends Module {
hidingDelay: 200,
});
/**
* Enable shortcut
*/
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const shortcut = this.getToolShortcut(toolName, tool);
if (toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT]) {
this.enableShortcut(tool, toolName, toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT]);
if (shortcut) {
this.enableShortcut(tool, toolName, shortcut);
}
/** Increment Tools count */
this.displayedToolsCount++;
}
/**
* Returns tool's shortcut
* It can be specified via internal 'shortcut' static getter or by user settings for tool
*
* @param {string} toolName - tool's name
* @param {ToolConstructable} tool - tool's class (not instance)
*/
private getToolShortcut(toolName: string, tool: ToolConstructable): string|null {
/**
* Enable shortcut
*/
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const internalToolShortcut = tool[this.Editor.Tools.INTERNAL_SETTINGS.SHORTCUT];
const userSpecifiedShortcut = toolSettings ? toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT] : null;
return userSpecifiedShortcut || internalToolShortcut;
}
/**
* Draw tooltip for toolbox tools
*
@ -232,12 +268,13 @@ export default class Toolbox extends Module {
* @returns {HTMLElement}
*/
private drawTooltip(toolName: string): HTMLElement {
const tool = this.Editor.Tools.available[toolName];
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {};
const name = I18n.t(I18nInternalNS.toolNames, userToolboxSettings.title || toolboxSettings.title || toolName);
let shortcut = toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT];
let shortcut = this.getToolShortcut(toolName, tool);
const tooltip = $.make('div', this.CSS.buttonTooltip);
const hint = document.createTextNode(_.capitalize(name));
@ -272,6 +309,24 @@ export default class Toolbox extends Module {
});
}
/**
* Removes all added shortcuts
* Fired when the Read-Only mode is activated
*/
private removeAllShortcuts(): void {
const tools = this.Editor.Tools.available;
for (const toolName in tools) {
if (Object.prototype.hasOwnProperty.call(tools, toolName)) {
const shortcut = this.getToolShortcut(toolName, tools[toolName]);
if (shortcut) {
this.Editor.Shortcuts.remove(shortcut);
}
}
}
}
/**
* Creates Flipper instance to be able to leaf tools
*/

View file

@ -43,9 +43,9 @@ export default class Tools extends Module {
/**
* Returns available Tools
*
* @returns {Tool[]}
* @returns {object<Tool>}
*/
public get available(): {[name: string]: ToolConstructable} {
public get available(): { [name: string]: ToolConstructable } {
return this.toolsAvailable;
}
@ -54,7 +54,7 @@ export default class Tools extends Module {
*
* @returns {Tool[]}
*/
public get unavailable(): {[name: string]: ToolConstructable} {
public get unavailable(): { [name: string]: ToolConstructable } {
return this.toolsUnavailable;
}
@ -63,7 +63,7 @@ export default class Tools extends Module {
*
* @returns {object} - object of Inline Tool's classes
*/
public get inline(): {[name: string]: ToolConstructable} {
public get inline(): { [name: string]: InlineToolConstructable } {
if (this._inlineTools) {
return this._inlineTools;
}
@ -112,7 +112,7 @@ export default class Tools extends Module {
/**
* Return editor block tools
*/
public get blockTools(): {[name: string]: BlockToolConstructable} {
public get blockTools(): { [name: string]: BlockToolConstructable } {
const tools = Object.entries(this.available).filter(([, tool]) => {
return !tool[this.INTERNAL_SETTINGS.IS_INLINE];
});
@ -134,7 +134,7 @@ export default class Tools extends Module {
*
* @returns {object}
*/
public get INTERNAL_SETTINGS(): {[name: string]: string} {
public get INTERNAL_SETTINGS(): { [name: string]: string } {
return {
IS_ENABLED_LINE_BREAKS: 'enableLineBreaks',
IS_INLINE: 'isInline',
@ -143,6 +143,7 @@ export default class Tools extends Module {
TOOLBOX: 'toolbox',
SANITIZE_CONFIG: 'sanitize',
CONVERSION_CONFIG: 'conversionConfig',
IS_READ_ONLY_SUPPORTED: 'isReadOnlySupported',
};
}
@ -151,7 +152,7 @@ export default class Tools extends Module {
*
* return {object}
*/
public get USER_SETTINGS(): {[name: string]: string} {
public get USER_SETTINGS(): { [name: string]: string } {
return {
SHORTCUT: 'shortcut',
TOOLBOX: 'toolbox',
@ -166,24 +167,24 @@ export default class Tools extends Module {
*
* @type {object}
*/
public readonly toolsClasses: {[name: string]: ToolConstructable} = {};
public readonly toolsClasses: { [name: string]: ToolConstructable } = {};
/**
* Tools` classes available to use
*/
private readonly toolsAvailable: {[name: string]: ToolConstructable} = {};
private readonly toolsAvailable: { [name: string]: ToolConstructable } = {};
/**
* Tools` classes not available to use because of preparation failure
*/
private readonly toolsUnavailable: {[name: string]: ToolConstructable} = {};
private readonly toolsUnavailable: { [name: string]: ToolConstructable } = {};
/**
* Tools settings in a map {name: settings, ...}
*
* @type {object}
*/
private readonly toolsSettings: {[name: string]: ToolSettings} = {};
private readonly toolsSettings: { [name: string]: ToolSettings } = {};
/**
* Cache for the prepared inline tools
@ -191,7 +192,7 @@ export default class Tools extends Module {
* @type {null|object}
* @private
*/
private _inlineTools: {[name: string]: ToolConstructable} = {};
private _inlineTools: { [name: string]: ToolConstructable } = {};
/**
* @class
@ -300,9 +301,9 @@ export default class Tools extends Module {
/**
* to see how it works {@link '../utils.ts#sequence'}
*/
return _.sequence(sequenceData, (data: {toolName: string}) => {
return _.sequence(sequenceData, (data: { toolName: string }) => {
this.success(data);
}, (data: {toolName: string}) => {
}, (data: { toolName: string }) => {
this.fallback(data);
});
}
@ -312,7 +313,7 @@ export default class Tools extends Module {
*
* @param {object} data - append tool to available list
*/
public success(data: {toolName: string}): void {
public success(data: { toolName: string }): void {
this.toolsAvailable[data.toolName] = this.toolsClasses[data.toolName];
}
@ -321,7 +322,7 @@ export default class Tools extends Module {
*
* @param {object} data - append tool to unavailable list
*/
public fallback(data: {toolName: string}): void {
public fallback(data: { toolName: string }): void {
this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName];
}
@ -334,7 +335,11 @@ export default class Tools extends Module {
*
* @returns {InlineTool} instance
*/
public constructInline(tool: InlineToolConstructable, name: string, toolSettings: ToolSettings = {} as ToolSettings): InlineTool {
public constructInline(
tool: InlineToolConstructable,
name: string,
toolSettings: ToolSettings = {} as ToolSettings
): InlineTool {
const constructorOptions = {
api: this.Editor.API.getMethodsForTool(name),
config: (toolSettings[this.USER_SETTINGS.CONFIG] || {}) as ToolSettings,
@ -345,14 +350,14 @@ export default class Tools extends Module {
}
/**
* Check if passed Tool is an instance of Initial Block Tool
* Check if passed Tool is an instance of Default Block Tool
*
* @param {Tool} tool - Tool to check
*
* @returns {boolean}
*/
public isInitial(tool): boolean {
return tool instanceof this.available[this.config.initialBlock];
public isDefault(tool): boolean {
return tool instanceof this.available[this.config.defaultBlock];
}
/**
@ -366,8 +371,8 @@ export default class Tools extends Module {
const settings = this.toolsSettings[toolName];
const config = settings[this.USER_SETTINGS.CONFIG] || {};
// Pass placeholder to initial Block config
if (toolName === this.config.initialBlock && !config.placeholder) {
// Pass placeholder to default Block config
if (toolName === this.config.defaultBlock && !config.placeholder) {
config.placeholder = this.config.placeholder;
settings[this.USER_SETTINGS.CONFIG] = config;
}
@ -379,7 +384,7 @@ export default class Tools extends Module {
* Returns internal tools
* Includes Bold, Italic, Link and Paragraph
*/
public get internalTools(): {[toolName: string]: ToolConstructable|ToolSettings} {
public get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings } {
return {
bold: { class: BoldInlineTool },
italic: { class: ItalicInlineTool },
@ -392,19 +397,40 @@ export default class Tools extends Module {
};
}
/**
* Returns true if tool supports read-only mode
*
* @param tool - tool to check
*/
public isReadOnlySupported(tool: BlockToolConstructable): boolean {
return tool[this.INTERNAL_SETTINGS.IS_READ_ONLY_SUPPORTED] === true;
}
/**
* Calls each Tool reset method to clean up anything set by Tool
*/
public destroy(): void {
Object.values(this.available).forEach(async tool => {
if (_.isFunction(tool.reset)) {
await tool.reset();
}
});
}
/**
* Binds prepare function of plugins with user or default config
*
* @returns {Array} list of functions that needs to be fired sequentially
*/
private getListOfPrepareFunctions(): Array<{
function: (data: {toolName: string; config: ToolConfig}) => void;
data: {toolName: string; config: ToolConfig};
function: (data: { toolName: string; config: ToolConfig }) => void;
data: { toolName: string; config: ToolConfig };
}> {
const toolPreparationList: Array<{
function: (data: {toolName: string; config: ToolConfig}) => void;
data: {toolName: string; config: ToolConfig};}
> = [];
function: (data: { toolName: string; config: ToolConfig }) => void;
data: { toolName: string; config: ToolConfig };
}
> = [];
for (const toolName in this.toolsClasses) {
if (Object.prototype.hasOwnProperty.call(this.toolsClasses, toolName)) {
@ -418,7 +444,7 @@ export default class Tools extends Module {
*/
toolPreparationList.push({
// eslint-disable-next-line @typescript-eslint/no-empty-function
function: typeof toolClass.prepare === 'function' ? toolClass.prepare : (): void => {},
function: _.isFunction(toolClass.prepare) ? toolClass.prepare : (): void => { },
data: {
toolName,
config: toolConfig,

View file

@ -17,6 +17,16 @@ import Selection from '../selection';
import Block from '../block';
import Flipper from '../flipper';
/**
* HTML Elements used for UI
*/
interface UINodes {
holder: HTMLElement;
wrapper: HTMLElement;
redactor: HTMLElement;
loader: HTMLElement;
}
/**
* @class
*
@ -35,7 +45,7 @@ import Flipper from '../flipper';
* @property {Element} nodes.wrapper - <codex-editor>
* @property {Element} nodes.redactor - <ce-redactor>
*/
export default class UI extends Module {
export default class UI extends Module<UINodes> {
/**
* Editor.js UI CSS class names
*
@ -43,7 +53,7 @@ export default class UI extends Module {
*/
public get CSS(): {
editorWrapper: string; editorWrapperNarrow: string; editorZone: string; editorZoneHidden: string;
editorLoader: string; editorEmpty: string;
editorLoader: string; editorEmpty: string; editorRtlFix: string;
} {
return {
editorWrapper: 'codex-editor',
@ -52,6 +62,7 @@ export default class UI extends Module {
editorZoneHidden: 'codex-editor__redactor--hidden',
editorLoader: 'codex-editor__loader',
editorEmpty: 'codex-editor--empty',
editorRtlFix: 'codex-editor--rtl',
};
}
@ -90,15 +101,6 @@ export default class UI extends Module {
*/
public isMobile = false;
/**
* HTML Elements used for UI
*/
public nodes: { [key: string]: HTMLElement } = {
holder: null,
wrapper: null,
redactor: null,
};
/**
* Cache for center column rectangle info
* Invalidates on window resize
@ -145,7 +147,7 @@ export default class UI extends Module {
/**
* Make main UI elements
*/
await this.make();
this.make();
/**
* Loader for rendering process
@ -155,27 +157,40 @@ export default class UI extends Module {
/**
* Append SVG sprite
*/
await this.appendSVGSprite();
/**
* Make toolbar
*/
await this.Editor.Toolbar.make();
/**
* Make the Inline toolbar
*/
await this.Editor.InlineToolbar.make();
this.appendSVGSprite();
/**
* Load and append CSS
*/
await this.loadStyles();
this.loadStyles();
}
/**
* Toggle read-only state
*
* If readOnly is true:
* - removes all listeners from main UI module elements
*
* if readOnly is false:
* - enables all listeners to UI module elements
*
* @param {boolean} readOnlyEnabled - "read only" state
*/
public toggleReadOnly(readOnlyEnabled: boolean): void {
/**
* Bind events for the UI elements
* Prepare components based on read-only state
*/
await this.bindEvents();
if (!readOnlyEnabled) {
/**
* Unbind all events
*/
this.enableModuleBindings();
} else {
/**
* Bind events for the UI elements
*/
this.disableModuleBindings();
}
}
/**
@ -239,10 +254,8 @@ export default class UI extends Module {
/**
* Makes Editor.js interface
*
* @returns {Promise<void>}
*/
private async make(): Promise<void> {
private make(): void {
/**
* Element where we need to append Editor.js
*
@ -253,7 +266,10 @@ export default class UI extends Module {
/**
* Create and save main UI elements
*/
this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);
this.nodes.wrapper = $.make('div', [
this.CSS.editorWrapper,
...(this.isRtl ? [ this.CSS.editorRtlFix ] : []),
]);
this.nodes.redactor = $.make('div', this.CSS.editorZone);
/**
@ -307,41 +323,48 @@ export default class UI extends Module {
/**
* Bind events on the Editor.js interface
*/
private bindEvents(): void {
this.Editor.Listeners.on(
this.nodes.redactor,
'click',
(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
);
private enableModuleBindings(): void {
this.readOnlyMutableListeners.on(this.nodes.redactor, 'click', (event: MouseEvent) => {
this.redactorClicked(event);
}, false);
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true);
this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), true);
this.readOnlyMutableListeners.on(this.nodes.redactor, 'mousedown', (event: MouseEvent | TouchEvent) => {
this.documentTouched(event);
}, true);
this.readOnlyMutableListeners.on(this.nodes.redactor, 'touchstart', (event: MouseEvent | TouchEvent) => {
this.documentTouched(event);
}, true);
this.readOnlyMutableListeners.on(document, 'keydown', (event: KeyboardEvent) => {
this.documentKeydown(event);
}, true);
this.readOnlyMutableListeners.on(document, 'click', (event: MouseEvent) => {
this.documentClicked(event);
}, true);
/**
* Handle selection change to manipulate Inline Toolbar appearance
*/
this.Editor.Listeners.on(document, 'selectionchange', (event: Event) => {
this.readOnlyMutableListeners.on(document, 'selectionchange', (event: Event) => {
this.selectionChanged(event);
}, true);
this.Editor.Listeners.on(window, 'resize', () => {
this.readOnlyMutableListeners.on(window, 'resize', () => {
this.resizeDebouncer();
}, {
passive: true,
});
}
/**
* Unbind events on the Editor.js interface
*/
private disableModuleBindings(): void {
this.readOnlyMutableListeners.clearAll();
}
/**
* Resize window handler
*/
@ -388,10 +411,19 @@ export default class UI extends Module {
* @param {KeyboardEvent} event - keyboard event
*/
private defaultBehaviour(event: KeyboardEvent): void {
const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`);
const { currentBlock } = this.Editor.BlockManager;
const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`);
const isMetaKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
/**
* When some block is selected, but the caret is not set inside the editor, treat such keydowns as keydown on selected block.
*/
if (currentBlock !== undefined && keyDownOnEditor === null) {
this.Editor.BlockEvents.keydown(event);
return;
}
/**
* Ignore keydowns on editor and meta keys
*/
@ -423,7 +455,7 @@ export default class UI extends Module {
if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
Caret.setToBlock(BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection(event);
@ -478,10 +510,6 @@ export default class UI extends Module {
* remove selected blocks
*/
if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection(event);
@ -506,7 +534,7 @@ export default class UI extends Module {
*/
if (!this.someToolbarOpened && hasPointerToBlock && (event.target as HTMLElement).tagName === 'BODY') {
/**
* Insert initial typed Block
* Insert the default typed Block
*/
const newBlock = this.Editor.BlockManager.insert();
@ -566,13 +594,6 @@ export default class UI extends Module {
if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted) {
this.Editor.BlockSelection.clearSelection(event);
}
/**
* Clear Selection if user clicked somewhere
*/
if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted) {
this.Editor.BlockSelection.clearSelection(event);
}
}
/**
@ -647,8 +668,10 @@ export default class UI extends Module {
return;
}
event.stopImmediatePropagation();
event.stopPropagation();
const stopPropagation = (): void => {
event.stopImmediatePropagation();
event.stopPropagation();
};
/**
* case when user clicks on anchor element
@ -658,6 +681,8 @@ export default class UI extends Module {
const ctrlKey = event.metaKey || event.ctrlKey;
if ($.isAnchor(element) && ctrlKey) {
stopPropagation();
const href = element.getAttribute('href');
const validUrl = _.getValidUrl(href);
@ -667,17 +692,21 @@ export default class UI extends Module {
}
if (!this.Editor.BlockManager.currentBlock) {
stopPropagation();
this.Editor.BlockManager.insert();
}
/**
* Show the Plus Button if:
* - Block is an initial-block (Text)
* - Block is an default-block (Text)
* - Block is empty
*/
const isInitialBlock = this.Editor.Tools.isInitial(this.Editor.BlockManager.currentBlock.tool);
const isDefaultBlock = this.Editor.Tools.isDefault(this.Editor.BlockManager.currentBlock.tool);
if (isDefaultBlock) {
stopPropagation();
if (isInitialBlock) {
/**
* Check isEmpty only for paragraphs to prevent unnecessary tree-walking on Tools with many nodes (for ex. Table)
*/
@ -696,8 +725,16 @@ export default class UI extends Module {
* @param {Event} event - selection event
*/
private selectionChanged(event: Event): void {
const { CrossBlockSelection, BlockSelection } = this.Editor;
const focusedElement = Selection.anchorElement as Element;
if (CrossBlockSelection.isCrossBlockSelectionStarted) {
// Removes all ranges when any Block is selected
if (BlockSelection.anyBlockSelected) {
Selection.get().removeAllRanges();
}
}
/**
* Event can be fired on clicks at the Editor elements, for example, at the Inline Toolbar
* We need to skip such firings
@ -713,6 +750,9 @@ export default class UI extends Module {
return;
}
/**
* @todo add debounce
*/
this.Editor.InlineToolbar.tryToShow(true);
}

@ -1 +1 @@
Subproject commit 306ed49135905d56ebb7d55f90f26fcd603ca7f1
Subproject commit 4832182dcf3874cbaedcb789f682bd61782e49ad

View file

@ -1,7 +1,7 @@
import $ from '../../dom';
import { API, BlockTool, BlockToolData, BlockToolConstructorOptions } from '../../../../types';
export interface StubData extends BlockToolData{
export interface StubData extends BlockToolData {
title: string;
savedData: BlockToolData;
}
@ -11,6 +11,11 @@ export interface StubData extends BlockToolData{
* It will store its data inside and pass it back with article saving
*/
export default class Stub implements BlockTool {
/**
* Notify core that tool supports read-only mode
*/
public static isReadOnlySupported = true;
/**
* Stub styles
*

View file

@ -183,6 +183,7 @@ export const logLabeled = _log.bind(window, true);
export function isPrintableKey(keyCode: number): boolean {
return (keyCode > 47 && keyCode < 58) || // number keys
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)
keyCode === 229 || // processing key input for certain languages — Chinese, Japanese, etc.
(keyCode > 64 && keyCode < 91) || // letter keys
(keyCode > 95 && keyCode < 112) || // Numpad keys
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
@ -275,7 +276,7 @@ export function isFunction(fn: any): fn is Function {
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isClass(fn: any): boolean {
return typeof fn === 'function' && /^\s*class\s+/.test(fn.toString());
return isFunction(fn) && /^\s*class\s+/.test(fn.toString());
}
/**
@ -556,3 +557,30 @@ export function getValidUrl(url: string): string {
export function openTab(url: string): void {
window.open(url, '_blank');
}
/**
* Returns random generated identifier
*
* @param {string} prefix - identifier prefix
*
* @returns {string}
*/
export function generateId(prefix = ''): string {
// tslint:disable-next-line:no-bitwise
return `${prefix}${(Math.floor(Math.random() * 1e8)).toString(16)}`;
}
/**
* Common method for printing a warning about the usage of deprecated property or method.
*
* @param condition - condition for deprecation.
* @param oldProperty - deprecated property.
* @param newProperty - the property that should be used instead.
*/
export function deprecationAssert(condition: boolean, oldProperty: string, newProperty: string): void {
const message = `«${oldProperty}» is deprecated and will be removed in the next major release. Please use the «${newProperty}» instead.`;
if (condition) {
logLabeled(message, 'warn');
}
}

View file

@ -35,9 +35,14 @@
display: none !important;
}
&__toggler-and-button-wrapper {
display: flex;
width: 100%;
padding: 0 6px;
}
&__buttons {
display: flex;
padding: 0 6px;
}
&__actions {
@ -98,10 +103,6 @@
height: 12px;
}
&--last {
margin-right: 0 !important;
}
&--link {
.icon--unlink {
display: none;

View file

@ -9,3 +9,4 @@
@import './animations.css';
@import './export.css';
@import './stub.css';
@import './rtl.css';

82
src/styles/rtl.css Normal file
View file

@ -0,0 +1,82 @@
.codex-editor.codex-editor--rtl {
direction: rtl;
.cdx-list {
padding-left: 0;
padding-right: 40px;
}
.ce-toolbar {
&__plus {
right: calc(var(--toolbox-buttons-size) * -1);
left: auto;
}
&__actions {
right: auto;
left: calc(var(--toolbox-buttons-size) * -1);
@media (--mobile){
margin-left: 0;
margin-right: auto;
padding-right: 0;
padding-left: 10px;
}
}
}
.ce-settings {
left: 5px;
right: auto;
&::before{
right: auto;
left: 25px;
}
&__button {
&:not(:nth-child(3n+3)) {
margin-left: 3px;
margin-right: 0;
}
}
}
.ce-conversion-tool {
&__icon {
margin-right: 0px;
margin-left: 10px;
}
}
.ce-inline-toolbar {
&__dropdown {
border-right: 0px solid transparent;
border-left: 1px solid var(--color-gray-border);
margin: 0 -6px 0 6px;
.icon--toggler-down {
margin-left: 0px;
margin-right: 4px;
}
}
}
}
.codex-editor--narrow.codex-editor--rtl {
.ce-toolbar__plus {
@media (--not-mobile) {
left: 0px;
right: 5px;
}
}
.ce-toolbar__actions {
@media (--not-mobile) {
left: -5px;
}
}
}

View file

@ -103,6 +103,7 @@
color: var(--grayText);
cursor: pointer;
background: var(--bg-light);
user-select: none;
&:hover {
color: var(--color-dark);

View file

@ -33,6 +33,13 @@
}
}
&--narrow&--rtl &__redactor {
@media (--not-mobile) {
margin-left: var(--narrow-mode-right-padding);
margin-right: 0;
}
}
&--narrow .ce-toolbar__actions {
@media (--not-mobile) {
right: -5px;

View file

@ -1,20 +0,0 @@
import {BlockToolData} from '../../types/tools';
/**
* Tool's saved data
*/
export interface SavedData {
tool: string;
data: BlockToolData;
time: number;
}
/**
* Tool's data after validation
*/
export interface ValidatedData {
tool?: string;
data?: BlockToolData;
time?: number;
isValid: boolean;
}

View file

@ -35,6 +35,8 @@ 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';
import ReadOnly from '../components/modules/readonly';
import ReadOnlyAPI from '../components/modules/api/readonly';
import I18nAPI from '../components/modules/api/i18n';
export interface EditorModules {
@ -75,5 +77,7 @@ export interface EditorModules {
CrossBlockSelection: CrossBlockSelection;
NotifierAPI: NotifierAPI;
TooltipAPI: TooltipAPI;
ReadOnly: ReadOnly;
ReadOnlyAPI: ReadOnlyAPI;
I18nAPI: I18nAPI;
}

View file

@ -1,5 +1,5 @@
import {BlockToolData, ToolConfig} from '../tools';
import {SavedData} from '../../src/types-internal/block-data';
import {SavedData} from '../data-formats';
/**
* @interface BlockAPI Describes Block API methods and properties

11
types/api/blocks.d.ts vendored
View file

@ -13,8 +13,10 @@ export interface Blocks {
/**
* Render passed data
* @param {OutputData} data
* @return {Promise<void>}
*
* @param {OutputData} data - saved Block data
*
* @returns {Promise<void>}
*/
render(data: OutputData): Promise<void>;
@ -47,11 +49,10 @@ export interface Blocks {
move(toIndex: number, fromIndex?: number): void;
/**
* Returns Block holder by Block index
* Returns Block API object by passed Block index
* @param {number} index
* @returns {HTMLElement}
*/
getBlockByIndex(index: number): BlockAPI;
getBlockByIndex(index: number): BlockAPI | void;
/**
* Returns current Block index

View file

@ -11,4 +11,5 @@ export * from './notifier';
export * from './tooltip';
export * from './inline-toolbar';
export * from './block';
export * from './readonly';
export * from './i18n';

12
types/api/readonly.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
/**
* ReadOnly API
*/
export interface ReadOnly {
/**
* Set or toggle read-only state
*
* @param {Boolean|undefined} state - set or toggle state
* @returns {Promise<boolean>} current value
*/
toggle: (state?: boolean) => Promise<boolean>;
}

View file

@ -25,6 +25,13 @@ export interface EditorConfig {
* Name should be equal to one of Tool`s keys of passed tools
* If not specified, Paragraph Tool will be used
*/
defaultBlock?: string;
/**
* @deprecated
* This property will be deprecated in the next major release.
* Use the 'defaultBlock' property instead.
*/
initialBlock?: string;
/**
@ -63,6 +70,11 @@ export interface EditorConfig {
*/
logLevel?: LogLevels;
/**
* Enable read-only mode
*/
readOnly?: boolean;
/**
* Internalization config
*/
@ -78,4 +90,9 @@ export interface EditorConfig {
* @param {API} api - editor.js api
*/
onChange?(api: API): void;
/**
* Defines default toolbar for all tools.
*/
inlineToolbar?: string[]|boolean;
}

View file

@ -7,5 +7,10 @@ export interface I18nConfig {
/**
* Dictionary used for translation
*/
messages: I18nDictionary;
messages?: I18nDictionary;
/**
* Text direction. If not set, uses ltr
*/
direction?: 'ltr' | 'rtl';
}

20
types/data-formats/block-data.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
import {BlockToolData} from '../tools';
/**
* Tool's saved data
*/
export interface SavedData {
tool: string;
data: BlockToolData;
time: number;
}
/**
* Tool's data after validation
*/
export interface ValidatedData {
tool?: string;
data?: BlockToolData;
time?: number;
isValid: boolean;
}

2
types/data-formats/index.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
export * from './block-data';
export * from './output-data';

11
types/index.d.ts vendored
View file

@ -5,12 +5,13 @@
*/
import {
EditorConfig,
I18nDictionary,
Dictionary,
DictValue,
EditorConfig,
I18nConfig,
I18nDictionary,
} from './configs';
import {
Blocks,
Caret,
@ -18,6 +19,7 @@ import {
InlineToolbar,
Listeners,
Notifier,
ReadOnly,
Sanitizer,
Saver,
Selection,
@ -26,7 +28,8 @@ import {
Tooltip,
I18n,
} from './api';
import {OutputData} from './data-formats/output-data';
import { OutputData } from './data-formats';
/**
* Interfaces used for development
@ -88,6 +91,7 @@ export interface API {
inlineToolbar: InlineToolbar;
tooltip: Tooltip;
i18n: I18n;
readOnly: ReadOnly;
}
/**
@ -108,6 +112,7 @@ declare class EditorJS {
public styles: Styles;
public toolbar: Toolbar;
public inlineToolbar: InlineToolbar;
public readOnly: ReadOnly;
constructor(configuration?: EditorConfig|string);
/**

View file

@ -49,6 +49,12 @@ export interface BlockTool extends BaseTool {
*/
onPaste?(event: PasteEvent): void;
/**
* Cleanup resources used by your tool here
* Called when the editor is destroyed
*/
destroy?(): void;
/**
* Lifecycle hooks
*/
@ -82,6 +88,7 @@ export interface BlockToolConstructorOptions<D extends object = any, C extends o
data: BlockToolData<D>;
config?: ToolConfig<C>;
block?: BlockAPI;
readOnly: boolean;
}
export interface BlockToolConstructable extends BaseToolConstructable {
@ -110,6 +117,11 @@ export interface BlockToolConstructable extends BaseToolConstructable {
*/
conversionConfig?: ConversionConfig;
/**
* Is Tool supports read-only mode, this property should return true
*/
isReadOnlySupported?: boolean;
/**
* @constructor
*

View file

@ -31,6 +31,8 @@ export interface InlineTool extends BaseTool {
/**
* Function called with Inline Toolbar closing
* @deprecated 2020 10/02 - The new instance will be created each time the button is rendered. So clear is not needed.
* Better to create the 'destroy' method in a future.
*/
clear?(): void;
}

View file

@ -44,6 +44,7 @@ export interface ToolSettings {
/**
* Tool's Toolbox settings
* It will be hidden from Toolbox when false is specified.
*/
toolbox?: ToolboxConfig;
toolbox?: ToolboxConfig | false;
}

View file

@ -40,4 +40,9 @@ export interface BaseToolConstructable {
* @param data
*/
prepare?(data: {toolName: string, config: ToolConfig}): void | Promise<void>;
/**
* Tool`s reset method to clean up anything set by prepare. Can be async
*/
reset?(): void | Promise<void>;
}

View file

@ -1230,8 +1230,9 @@ bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
version "4.11.9"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
boolbase@^1.0.0, boolbase@~1.0.0:
version "1.0.0"
@ -1268,6 +1269,7 @@ braces@^3.0.1:
brorand@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
version "1.2.0"
@ -2157,8 +2159,9 @@ electron-to-chromium@^1.3.413:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.418.tgz#840021191f466b803a873e154113620c9f53cec6"
elliptic@^6.0.0:
version "6.5.2"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762"
version "6.5.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
@ -2927,6 +2930,7 @@ hash-base@^3.0.0:
hash.js@^1.0.0, hash.js@^1.0.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
dependencies:
inherits "^2.0.3"
minimalistic-assert "^1.0.1"
@ -2938,6 +2942,7 @@ hex-color-regex@^1.1.0:
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
dependencies:
hash.js "^1.0.3"
minimalistic-assert "^1.0.0"
@ -3089,6 +3094,7 @@ inflight@^1.0.4:
inherits@2, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@2.0.1:
version "2.0.1"
@ -3790,10 +3796,12 @@ min-indent@^1.0.0:
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
minimatch@^3.0.4:
version "3.0.4"