Release / 2.18 (#1181)

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>
This commit is contained in:
Peter Savchenko 2020-06-03 11:17:29 +03:00 committed by GitHub
parent 4ead91fa65
commit 63eeec0f3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 5794 additions and 3721 deletions

4
.eslintignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
*.d.ts
src/components/tools/paragraph
src/polyfills.ts

131
.eslintrc
View file

@ -1,92 +1,45 @@
{ {
/** Enable ES6 features */ "extends": [
"parserOptions": { "codex"
"ecmaVersion": 2017, ],
"sourceType": "module" "rules": {
}, /**
"rules": { * Temporary suppress some errors. We need to fix them partially in next patches
*/
"arrow-spacing": [2, { "import/no-duplicates": ["warn"],
"before": true, },
"after": true "settings": {
}], "jsdoc": {
"mode": "typescript"
/** Variables */
"no-catch-shadow": 2,
"no-delete-var": 2,
"no-label-var": 2,
"no-shadow-restricted-names": 2,
"no-shadow": 2,
"no-undef-init": 2,
"no-undef": 2,
"no-unused-vars": 0,
/** Style */
"array-bracket-spacing": [2, "never", {
"singleValue": true,
"objectsInArrays": true,
"arraysInArrays": true
}],
"quotes": [2, "single", {
"avoidEscape": true,
"allowTemplateLiterals": true
}],
"eqeqeq": 0,
"brace-style": [2, "1tbs"],
"comma-spacing": [2, {
"before": false,
"after": true
}],
"comma-style": [2, "last"],
"eol-last": 0,
"no-nested-ternary": 1,
"no-trailing-spaces": 2,
"no-mixed-spaces-and-tabs": 2,
"padded-blocks": [2, "never"],
"space-before-blocks": 1,
"space-before-function-paren": [1, {
"anonymous": "always",
"named": "never"
}],
"spaced-comment": [2, "always", {
"exceptions": ["-", "+"],
"markers": ["=", "!"]
}],
"semi": [2, "always"],
"indent": [2, 2, {
"SwitchCase": 1
}],
"camelcase": [2, {
"properties": "always"
}],
"newline-after-var": [1, "always"]
},
"globals":{
"document": true,
"module": true,
"require": true,
"window": true,
"console" : true,
"codex": true,
"VERSION" : true,
"Promise" : true,
"MutationObserver": true,
"FormData": true,
"XMLHttpRequest": true,
"ActiveXObject": true,
"RegExp": true,
"Module": true,
"Node": true,
"Element": true,
"DocumentFragment": true,
"Proxy": true,
"Symbol": true,
"$": true,
"_": true,
"setTimeout": true,
"process": true,
"__dirname": true,
"Map": true
} }
},
"globals": {
"Node": true,
"Range": true,
"HTMLElement": true,
"HTMLDivElement": true,
"Element": true,
"Selection": true,
"SVGElement": true,
"Text": true,
"InsertPosition": true,
"PropertyKey": true,
"MouseEvent": true,
"TouchEvent": true,
"KeyboardEvent": true,
"ClipboardEvent": true,
"DragEvent": true,
"Event": true,
"EventTarget": true,
"Document": true,
"NodeList": true,
"File": true,
"FileList": true,
"MutationRecord": true,
"AddEventListenerOptions": true,
"DataTransfer": true,
"DOMRect": true,
"ClientRect": true,
"ArrayLike": true
}
} }

76
.github/CODE_OF_CONDUCT.md vendored Normal file
View file

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at team@codex.so. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View file

@ -1,38 +1,30 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve Editor.js
title: "⚠️" title: "[Bug]"
labels: bug labels: bug
assignees: '' assignees: ''
--- ---
**Describe the bug** Describe a bug.
A clear and concise description of what the bug is.
**To Reproduce** Steps to reproduce:
Steps to reproduce the behavior: 1. Go to …
1. Go to '...' 2. Click on …
2. Click on '....' 3. …
3. Scroll down to '....'
4. See error
**Expected behavior** Expected behavior:
A clear and concise description of what you expected to happen.
**Screenshots** Screenshots:
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** Device, Browser, OS:
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):** Editor.js version:
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context** Plugins you use with their versions:
Add any other context about the problem here.
<!--
🤫 If you like Editor.js, please consider supporting us via OpenCollective:
https://opencollective.com/editorjs
-->

17
.github/ISSUE_TEMPLATE/discussion.md vendored Normal file
View file

@ -0,0 +1,17 @@
---
name: Discussion
about: Any question about the Editor.js to discuss
title: ''
labels: discussion
assignees: ''
---
The question.
Why and how the question has come up.
<!--
🤫 If you like Editor.js, please consider supporting us via OpenCollective:
https://opencollective.com/editorjs
-->

View file

@ -1,20 +1,19 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea to improve Editor.js
title: "\U0001F4A1" title: "\U0001F4A1"
labels: feature labels: feature
assignees: '' assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** 1. Describe a problem.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like** 2. Describe the solution you'd like. Mockups are welcome.
A clear and concise description of what you want to happen.
**Describe alternatives you've considered** 3. Are there any alternatives?
A clear and concise description of any alternative solutions or features you've considered.
**Additional context** <!--
Add any other context or screenshots about the feature request here. 🤫 If you like Editor.js, please consider supporting us via OpenCollective:
https://opencollective.com/editorjs
-->

24
.github/workflows/eslint.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: ESLint CodeX
on: [pull_request]
jobs:
lint:
name: ESlint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache node modules
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.OS }}-build-${{ env.cache-name }}-
${{ runner.OS }}-build-
${{ runner.OS }}-
- run: yarn install
- run: yarn lint

4
dist/editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,23 @@
/*!
* CodeX.Tooltips
*
* @version 1.0.1
*
* @licence MIT
* @author CodeX <https://codex.so>
*
*
*/
/*!
* Codex JavaScript Notification module
* https://github.com/codex-team/js-notifier
*/
/*! /*!
* Editor.js * Editor.js
* *
* @version 2.17.0 * @version 2.18.0
* *
* @licence Apache-2.0 * @licence Apache-2.0
* @author CodeX <https://codex.so> * @author CodeX <https://codex.so>
@ -10,32 +26,6 @@
* @licence Apache-2.0 (https://github.com/guardian/html-janitor/blob/master/LICENSE) * @licence Apache-2.0 (https://github.com/guardian/html-janitor/blob/master/LICENSE)
*/ */
/**
* If developer uses editor's API, then he can customize sanitize restrictions.
* Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses Default configuration
*
* @uses https://www.npmjs.com/package/html-janitor
* @license https://github.com/guardian/html-janitor/blob/master/LICENSE
*
* @param {SanitizerConfig} config - sanitizer extension
*/
/**
* Editor.js
*
* Short Description (눈_눈;)
* @version 2.0
*
* @licence Apache-2.0
* @author CodeX-Team <https://ifmo.su>
*/
/*!
* Codex JavaScript Notification module
* https://github.com/codex-team/js-notifier
*/
/*! /*!
* Library for handling keyboard shortcuts * Library for handling keyboard shortcuts
* @copyright CodeX (https://codex.so) * @copyright CodeX (https://codex.so)
@ -44,6 +34,29 @@
* @version 1.1.1 * @version 1.1.1
*/ */
/**
* If developer uses editor's API, then he can customize sanitize restrictions.
* Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses Default configuration
*
* @see {@link https://www.npmjs.com/package/html-janitor}
* @license Apache-2.0
* @see {@link https://github.com/guardian/html-janitor/blob/master/LICENSE}
*
* @param {SanitizerConfig} config - sanitizer extension
*/
/**
* Editor.js
*
* Short Description (눈_눈;)
*
* @version 2.18.0
*
* @license Apache-2.0
* @author CodeX-Team <https://ifmo.su>
*/
/** /**
* Base Paragraph Block for the Editor.js. * Base Paragraph Block for the Editor.js.
* Represents simple paragraph * Represents simple paragraph
@ -52,14 +65,3 @@
* @copyright CodeX 2018 * @copyright CodeX 2018
* @license The MIT License (MIT) * @license The MIT License (MIT)
*/ */
/*!
* CodeX.Tooltips
*
* @version 1.0.0
*
* @licence MIT
* @author CodeX <https://codex.so>
*
*
*/

View file

@ -1,5 +1,30 @@
# Changelog # Changelog
### 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)
- `New` — Block API that allows you to access certain Block properties and methods
- `Improvements` - TSLint (deprecated) replaced with ESLint, old config changed to [CodeX ESLint Config](https://github.com/codex-team/eslint-config).
- `Improvements` - Fix many code-style issues, add missed annotations.
- `Improvements` - Adjusted GitHub action for ESLint.
- `Improvements` - Blocks API: if `blocks.delete` method is called, but no Block is selected, show warning instead of throwing an error [#1102](https://github.com/codex-team/editor.js/issues/1102)
- `Improvements` - Blocks API: allow deletion of blocks by specifying block index via `blocks.delete(index)`.
- `Improvements` - UX: Navigate next Block from the last non-initial one creates new initial Block now [#1103](https://github.com/codex-team/editor.js/issues/1103)
- `Improvements` - Improve performance of DOM traversing at the `isEmpty()` method [#1095](https://github.com/codex-team/editor.js/issues/1095)
- `Improvements` - CODE OF CONDUCT added
- `Improvements` - Disabled useCapture flag for a block keydown handling. That will allow plugins to override keydown and stop event propagation, for example, to make own Tab behavior.
- `Improvements` - All modules now might have `destroy` method called on Editor.js destroy
- `Improvements` - Block settings can contain text inputs, focus will be restored after settings closed [#1090](https://github.com/codex-team/editor.js/issues/1090)
- `Fix` - Editor's styles won't be appended to the `<head>` when another instance have already do that [#1079](https://github.com/codex-team/editor.js/issues/1079)
- `Fix` - Fixed wrong toolbar icon centering in Firefox [#1120](https://github.com/codex-team/editor.js/pull/1120)
- `Fix` - Toolbox: Tool's order in Toolbox now saved in accordance with `tools` object keys order [#1073](https://github.com/codex-team/editor.js/issues/1073)
- `Fix` - Setting `autofocus` config property to `true` cause adding `.ce-block--focused` for the autofocused block [#1073](https://github.com/codex-team/editor.js/issues/1124)
- `Fix` - Public getter `shortcut` now works for Inline Tools [#1132](https://github.com/codex-team/editor.js/issues/1132)
- `Fix` - `CMD+A` handler removed after Editor.js destroy [#1133](https://github.com/codex-team/editor.js/issues/1133)
> *Breaking changes* `blocks.getBlockByIndex` method now returns BlockAPI object. To access old value, use BlockAPI.holder property
### 2.17 ### 2.17
- `Improvements` - Editor's [onchange callback](https://editorjs.io/configuration#editor-modifications-callback) now accepts an API as a parameter - `Improvements` - Editor's [onchange callback](https://editorjs.io/configuration#editor-modifications-callback) now accepts an API as a parameter
@ -15,6 +40,7 @@
- `Fix` — Improve plain text paste [#1012](https://github.com/codex-team/editor.js/issues/1012) - `Fix` — Improve plain text paste [#1012](https://github.com/codex-team/editor.js/issues/1012)
- `Fix` — Fix multiline paste [#1015](https://github.com/codex-team/editor.js/issues/1015) - `Fix` — Fix multiline paste [#1015](https://github.com/codex-team/editor.js/issues/1015)
### 2.16.1 ### 2.16.1
- `Fix` — Fix Firefox bug with incorrect height and cursor position of empty content editable elements [#947](https://github.com/codex-team/editor.js/issues/947) [#876](https://github.com/codex-team/editor.js/issues/876) [#608](https://github.com/codex-team/editor.js/issues/608) [#876](https://github.com/codex-team/editor.js/issues/876) - `Fix` — Fix Firefox bug with incorrect height and cursor position of empty content editable elements [#947](https://github.com/codex-team/editor.js/issues/947) [#876](https://github.com/codex-team/editor.js/issues/876) [#608](https://github.com/codex-team/editor.js/issues/608) [#876](https://github.com/codex-team/editor.js/issues/876)

View file

@ -1,9 +1,40 @@
# Editor.js API # Editor.js API
---
Most actual API described by [this interface](../types/api/index.d.ts).
Blocks have access to the public methods provided by Editor.js API Module. Plugin and Tune Developers ---
📃 See official API documentation [https://editorjs.io/api](https://editorjs.io/api)
---
Tools have access to the public methods provided by Editor.js API Module. Plugin and Tune Developers
can use Editor\`s API as they want. can use Editor\`s API as they want.
## Block API
API for certain Block methods and properties. You can access it through `editor.api.block.getBlockByIndex` method or get it form `block` property of [Tool constructor](../types/tools/block-tool.d.ts) argument.
`name: string` — Block's Tool name (key, specified in `tools` property of initial configuration)
`config: ToolConfig` — Tool config passed on Editor initialization
`holder: HTMLElement` — HTML Element that wraps Tool's HTML content
`isEmpty: boolean``true` if Block has any editable content
`selected: boolean` - `true` if Block is selected with Cross-Block Selection
`set stretched(state: boolean)` — set Block's stretch state
`stretched: boolean``true` if Block is stretched
`call(methodName: string, param?: object): void` — method to call any Tool's instance methods with checks and error handlers under-the-hood. For example, [Block lifecycle hooks](./tools.md#block-lifecycle-hooks)
`save(): Promise<void|SavedData>` — returns data saved from current Block's state, including Tool name and saving exec time
`validate(data: BlockToolData): Promise<boolean>` — calls Tool's validate method if exists
## Api object description ## Api object description
Common API interface. Common API interface.
@ -36,11 +67,11 @@ use 'move' instead)
`getCurrentBlockIndex()` - current Block index `getCurrentBlockIndex()` - current Block index
`getBlockByIndex(index: Number)` - returns Block with passed index `getBlockByIndex(index: Number)` - returns Block API object by passed index
`getBlocksCount()` - returns Blocks count `getBlocksCount()` - returns Blocks count
`stretchBlock(index: number, status: boolean)` - make Block stretched `stretchBlock(index: number, status: boolean)` - _Deprecated. Use Block API interface instead._ make Block stretched.
`insertNewBlock()` - __Deprecated__ insert new Block after working place `insertNewBlock()` - __Deprecated__ insert new Block after working place

View file

@ -12,11 +12,12 @@ Each Tool should have an installation guide.
Each Tool's instance called with an params object. Each Tool's instance called with an params object.
| Param | Type | Description | | Param | Type | Description |
| ------ | ------------------- | ----------------------------------------------- | | ------ | ------------------------------------------------------ | ----------------------------------------------- |
| api | [`IAPI`][iapi-link] | Editor.js's API methods | | api | [`IAPI`](../types/index.d.ts) | Editor.js's API methods |
| config | `object` | Special configuration params passed in «config» | | config | [`ToolConfig`](../types/tools/tool-config.d.ts) | Special configuration params passed in «config» |
| data | `object` | Data to be rendered in this Tool | | data | [`BlockToolData`](../types/tools/block-tool-data.d.ts) | Data to be rendered in this Tool |
| block | [`BlockAPI`](../types/api/block.d.ts) | Block's API methods |
[iapi-link]: ../src/types-internal/api.ts [iapi-link]: ../src/types-internal/api.ts

View file

@ -93,7 +93,8 @@ body {
max-width: 180px; max-width: 180px;
background: #4A9DF8; background: #4A9DF8;
padding: 17px 30px; padding: 17px 30px;
box-shadow: 0 6px 4px -4px rgba(137, 207, 255, 0.77); box-shadow: 0 22px 18px -4px rgba(137, 207, 255, 0.77);
transition: all 150ms ease;
cursor: pointer; cursor: pointer;
border-radius: 31px; border-radius: 31px;
color: #fff; color: #fff;
@ -103,6 +104,8 @@ body {
.ce-example__button:hover { .ce-example__button:hover {
background: #3D8DE5; background: #3D8DE5;
transform: translateY(2px);
box-shadow: 0 20px 15px -4px rgba(137, 207, 255, 0.77);
} }
.ce-example__output-footer { .ce-example__output-footer {

View file

@ -276,9 +276,10 @@
}, },
onChange: function() { onChange: function() {
console.log('something changed'); console.log('something changed');
} },
}); });
/** /**
* Saving example * Saving example
*/ */

381
example/example-i18n.html Normal file
View file

@ -0,0 +1,381 @@
<!--
This page contains example of editor.js internalization.
See <script> section -> i18n property of the configuration object
\ (•◡•) /
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Editor.js 🤩🧦🤨 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 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 -->
<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/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 -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@latest"></script><!-- Table -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/link@latest"></script><!-- Link -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/warning@latest"></script><!-- Warning -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@latest"></script><!-- Marker -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest"></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',
/**
* 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: SimpleImage,
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,
embed: Embed,
table: {
class: Table,
inlineToolbar: true,
shortcut: 'CMD+ALT+T'
},
},
/**
* To provide localization of the editor.js you need to provide 'i18n' option with 'messages' dictionary:
*
* 1. At the 'ui' section of 'messages' there are translations for the internal editor.js UI elements.
* You can create or find/download a dictionary for your language
*
* 2. As long as tools list is a user-specific thing (we do not know which tools you use and under which names),
* so we can't provide a ready-to-use tool names dictionary.
* There is a 'toolNames' section for that reason. Put translations for the names of your tools there.
*
* 3. Also, the UI of the tools you use is also invisible to editor.js core.
* To pass translations for specific tools (that supports I18n API), there are 'tools' and 'blockTunes' section.
* Pass dictionaries for specific plugins through them.
*/
i18n: {
/**
* @type {I18nDictionary}
*/
messages: {
/**
* Other below: translation of different UI components of the editor.js core
*/
"ui": {
"blockTunes": {
"toggler": {
"Click to tune": "Нажмите, чтобы настроить",
"or drag to move": "или перетащите"
},
},
"inlineToolbar": {
"converter": {
"Convert to": "Конвертировать в"
}
},
"toolbar": {
"toolbox": {
"Add": "Добавить"
}
}
},
/**
* Section for translation Tool Names: both block and inline tools
*/
"toolNames": {
"Text": "Параграф",
"Heading": "Заголовок",
"List": "Список",
"Warning": "Примечание",
"Checklist": "Чеклист",
"Quote": "Цитата",
"Code": "Код",
"Delimiter": "Разделитель",
"Raw HTML": "HTML-фрагмент",
"Table": "Таблица",
"Link": "Ссылка",
"Marker": "Маркер",
"Bold": "Полужирный",
"Italic": "Курсив",
"InlineCode": "Моноширинный",
},
/**
* Section for passing translations to the external tools classes
*/
"tools": {
/**
* Each subsection is the i18n dictionary that will be passed to the corresponded plugin
* The name of a plugin should be equal the name you specify in the 'tool' section for that plugin
*/
"warning": { // <-- 'Warning' tool will accept this dictionary section
"Title": "Название",
"Message": "Сообщение",
},
/**
* Link is the internal Inline Tool
*/
"link": {
"Add a link": "Вставьте ссылку"
},
/**
* The "stub" is an internal block tool, used to fit blocks that does not have the corresponded plugin
*/
"stub": {
'The block can not be displayed correctly.': 'Блок не может быть отображен'
}
},
/**
* Section allows to translate Block Tunes
*/
"blockTunes": {
/**
* Each subsection is the i18n dictionary that will be passed to the corresponded Block Tune plugin
* The name of a plugin should be equal the name you specify in the 'tunes' section for that plugin
*
* Also, there are few internal block tunes: "delete", "moveUp" and "moveDown"
*/
"delete": {
"Delete": "Удалить"
},
"moveUp": {
"Move up": "Переместить вверх"
},
"moveDown": {
"Move down": "Переместить вниз"
}
},
}
},
/**
* Initial Editor data
*/
data: {
blocks: [
{
type: "header",
data: {
text: "Editor.js",
level: 2
}
},
{
type : 'paragraph',
data : {
text : 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration.'
}
},
{
type: "header",
data: {
text: "Key features",
level: 3
}
},
{
type : 'list',
data : {
items : [
'It is a block-styled editor',
'It returns clean data output in JSON',
'Designed to be extendable and pluggable with a simple API',
],
style: 'unordered'
}
},
{
type: "header",
data: {
text: "What does it mean «block-styled editor»",
level: 3
}
},
{
type : 'paragraph',
data : {
text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js <mark class=\"cdx-marker\">workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc</mark>. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.'
}
},
{
type : 'paragraph',
data : {
text : `There are dozens of <a href="https://github.com/editor-js">ready-to-use Blocks</a> and the <a href="https://editorjs.io/creating-a-block-tool">simple API</a> for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.`
}
},
{
type: "header",
data: {
text: "What does it mean clean data output",
level: 3
}
},
{
type : 'paragraph',
data : {
text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below'
}
},
{
type : 'paragraph',
data : {
text : `Given data can be used as you want: render with HTML for <code class="inline-code">Web clients</code>, render natively for <code class="inline-code">mobile apps</code>, create markup for <code class="inline-code">Facebook Instant Articles</code> or <code class="inline-code">Google AMP</code>, generate an <code class="inline-code">audio version</code> and so on.`
}
},
{
type : 'paragraph',
data : {
text : 'Clean data is useful to sanitize, validate and process on the backend.'
}
},
{
type : 'delimiter',
data : {}
},
{
type : 'paragraph',
data : {
text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏'
}
},
{
type: 'image',
data: {
url: 'assets/codex2x.png',
caption: '',
stretched: false,
withBorder: true,
withBackground: false,
}
},
]
},
onReady: function(){
saveButton.click();
},
});
/**
* Saving example
*/
saveButton.addEventListener('click', function () {
editor.save().then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
});
});
</script>
</body>
</html>

@ -1 +1 @@
Subproject commit 0083d3bcbce82fd2063a15e3914158cb4bf8da7e Subproject commit 44473de4c60dd836ccb61b4dbcf4cc00088acd19

@ -1 +1 @@
Subproject commit 0e143926c9c8d693f2441d0f6a1982d28dcd5bf8 Subproject commit 93e0b6d6418034f4e7ee704aba090cc25ca16ac2

@ -1 +1 @@
Subproject commit 70876fde289e9f8baa81ee4b5c8c3dc036ac7035 Subproject commit 1d6f474c14613c60344d30ebd930a18ca123e4a4

@ -1 +1 @@
Subproject commit a2a0dabb0a6f5f93d96264ee3774e53f2a64898e Subproject commit c68375288c40774e8d8ceff79aa559d562078aaa

@ -1 +1 @@
Subproject commit e0ace3a0be6561f103b693596618095dffdfb34d Subproject commit 7c6d41603797ebfd00d59fc7ff623342b8f5a48c

@ -1 +1 @@
Subproject commit a2647e64feb5a744aa6cd51a5e08ca76b6cbfe6b Subproject commit af9dc3885077ab2ea1b0ae8ae0d145ff1a40fc40

View file

@ -1,6 +1,6 @@
{ {
"name": "@editorjs/editorjs", "name": "@editorjs/editorjs",
"version": "2.17.0", "version": "2.18.0",
"description": "Editor.js — Native JS, based on API and Open Source", "description": "Editor.js — Native JS, based on API and Open Source",
"main": "dist/editor.js", "main": "dist/editor.js",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
@ -16,6 +16,9 @@
"build:win": "rimraf dist && yarn svg:win && yarn build:prod", "build:win": "rimraf dist && yarn svg:win && yarn build:prod",
"build:dev": "webpack --mode development --progress --display-error-details --display-entrypoints --watch", "build:dev": "webpack --mode development --progress --display-error-details --display-entrypoints --watch",
"build:prod": "webpack --mode production", "build:prod": "webpack --mode production",
"lint": "eslint src/ --ext .ts",
"lint:errors": "eslint src/ --ext .ts --quiet",
"lint:fix": "eslint src/ --ext .ts --fix",
"svg:win": "if not exist dist md dist && yarn svg", "svg:win": "if not exist dist md dist && yarn svg",
"svg": "svg-sprite-generate -d src/assets/ -o dist/sprite.svg", "svg": "svg-sprite-generate -d src/assets/ -o dist/sprite.svg",
"pull_tools": "git submodule update --init --recursive", "pull_tools": "git submodule update --init --recursive",
@ -28,43 +31,43 @@
"url": "git+https://github.com/codex-team/editor.js.git" "url": "git+https://github.com/codex-team/editor.js.git"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.5", "@babel/core": "^7.9.0",
"@babel/plugin-transform-runtime": "^7.7.5", "@babel/plugin-transform-runtime": "^7.9.0",
"@babel/polyfill": "^7.2.5", "@babel/polyfill": "^7.8.7",
"@babel/preset-env": "^7.7.5", "@babel/preset-env": "^7.9.5",
"@babel/register": "^7.0.0", "@babel/register": "^7.9.0",
"@babel/runtime": "^7.7.5", "@babel/runtime": "^7.9.2",
"@codexteam/shortcuts": "^1.1.1", "@codexteam/shortcuts": "^1.1.1",
"@types/webpack": "^4.4.25", "@types/webpack": "^4.41.12",
"@types/webpack-env": "^1.13.9", "@types/webpack-env": "^1.15.2",
"babel-loader": "^8.0.5", "babel-loader": "^8.1.0",
"babel-plugin-add-module-exports": "^1.0.0", "babel-plugin-add-module-exports": "^1.0.0",
"babel-plugin-class-display-name": "^2.1.0", "babel-plugin-class-display-name": "^2.1.0",
"core-js": "3", "core-js": "3.6.5",
"css-loader": "^3.2.1", "css-loader": "^3.5.3",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"eslint": "^6.7.2", "eslint": "^6.8.0",
"eslint-loader": "^3.0.3", "eslint-config-codex": "^1.3.3",
"eslint-loader": "^4.0.2",
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"html-janitor": "^2.0.4", "html-janitor": "^2.0.4",
"license-webpack-plugin": "^2.1.1", "license-webpack-plugin": "^2.1.4",
"postcss-apply": "^0.12.0", "postcss-apply": "^0.12.0",
"postcss-import": "^12.0.1", "postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"postcss-nested": "^4.1.2", "postcss-nested": "^4.1.2",
"postcss-nested-ancestors": "^2.0.0", "postcss-nested-ancestors": "^2.0.0",
"postcss-preset-env": "^6.6.0", "postcss-preset-env": "^6.6.0",
"raw-loader": "^4.0.0", "raw-loader": "^4.0.1",
"rimraf": "^3.0.0", "rimraf": "^3.0.2",
"stylelint": "^12.0.0", "stylelint": "^13.3.3",
"svg-sprite-generator": "^0.0.7", "svg-sprite-generator": "^0.0.7",
"terser-webpack-plugin": "^2.2.2", "terser-webpack-plugin": "^2.3.6",
"ts-loader": "^6.2.1", "ts-loader": "^7.0.1",
"tslint": "^5.14.0", "tslint": "^6.1.1",
"tslint-loader": "^3.5.4", "typescript": "3.8.3",
"typescript": "^3.7.3", "webpack": "^4.43.0",
"webpack": "^4.29.6", "webpack-cli": "^3.3.11"
"webpack-cli": "^3.2.3"
}, },
"collective": { "collective": {
"type": "opencollective", "type": "opencollective",
@ -72,6 +75,6 @@
}, },
"dependencies": { "dependencies": {
"codex-notifier": "^1.1.2", "codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.0" "codex-tooltip": "^1.0.1"
} }
} }

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
import {EditorConfig} from '../types';
declare const VERSION: string; import { EditorConfig } from '../types';
/** /**
* Apply polyfills * Apply polyfills
@ -10,14 +9,18 @@ import '@babel/register';
import 'components/polyfills'; import 'components/polyfills';
import Core from './components/core'; import Core from './components/core';
import * as _ from './components/utils';
declare const VERSION: string;
/** /**
* Editor.js * Editor.js
* *
* Short Description (_눈;) * Short Description (_눈;)
* @version 2.0
* *
* @licence Apache-2.0 * @version 2.18.0
*
* @license Apache-2.0
* @author CodeX-Team <https://ifmo.su> * @author CodeX-Team <https://ifmo.su>
*/ */
export default class EditorJS { export default class EditorJS {
@ -33,20 +36,19 @@ export default class EditorJS {
public destroy: () => void; public destroy: () => void;
/** Editor version */ /** Editor version */
static get version(): string { public static get version(): string {
return VERSION; return VERSION;
} }
/** /**
* @constructor * @param {EditorConfig|string|undefined} [configuration] - user configuration
*
* @param {EditorConfig|String|undefined} [configuration] - user configuration
*/ */
public constructor(configuration?: EditorConfig|string) { constructor(configuration?: EditorConfig|string) {
/** /**
* Set default onReady function * Set default onReady function
*/ */
let onReady = () => {}; // eslint-disable-next-line @typescript-eslint/no-empty-function
let onReady = (): void => {};
/** /**
* If `onReady` was passed in `configuration` then redefine onReady function * If `onReady` was passed in `configuration` then redefine onReady function
@ -63,6 +65,7 @@ export default class EditorJS {
/** /**
* We need to export isReady promise in the constructor * We need to export isReady promise in the constructor
* as it can be used before other API methods are exported * as it can be used before other API methods are exported
*
* @type {Promise<void>} * @type {Promise<void>}
*/ */
this.isReady = editor.isReady.then(() => { this.isReady = editor.isReady.then(() => {
@ -74,18 +77,22 @@ export default class EditorJS {
/** /**
* Export external API methods * Export external API methods
* *
* @param editor * @param {Core} editor Editor's instance
*/ */
public exportAPI(editor: Core): void { public exportAPI(editor: Core): void {
const fieldsToExport = [ 'configuration' ]; const fieldsToExport = [ 'configuration' ];
const destroy = () => { const destroy = (): void => {
editor.moduleInstances.Listeners.removeAll(); Object.values(editor.moduleInstances)
editor.moduleInstances.UI.destroy(); .forEach((moduleInstance) => {
editor.moduleInstances.ModificationsObserver.destroy(); if (_.isFunction(moduleInstance.destroy)) {
moduleInstance.destroy();
}
});
editor = null; editor = null;
for (const field in this) { for (const field in this) {
if (this.hasOwnProperty(field)) { if (Object.prototype.hasOwnProperty.call(this, field)) {
delete this[field]; delete this[field];
} }
} }

View file

@ -1,6 +1,6 @@
import {EditorModules} from '../types-internal/editor-modules'; import { EditorModules } from '../types-internal/editor-modules';
import {EditorConfig} from '../../types'; import { EditorConfig } from '../../types';
import {ModuleConfig} from '../types-internal/module-config'; import { ModuleConfig } from '../types-internal/module-config';
/** /**
* @abstract * @abstract
@ -8,28 +8,29 @@ import {ModuleConfig} from '../types-internal/module-config';
* @classdesc All modules inherits from this class. * @classdesc All modules inherits from this class.
* *
* @typedef {Module} Module * @typedef {Module} Module
* @property {Object} config - Editor user settings * @property {object} config - Editor user settings
* @property {EditorModules} Editor - List of Editor modules * @property {EditorModules} Editor - List of Editor modules
*/ */
export default class Module { export default class Module {
/** /**
* Editor modules list * Editor modules list
*
* @type {EditorModules} * @type {EditorModules}
*/ */
protected Editor: EditorModules; protected Editor: EditorModules;
/** /**
* Editor configuration object * Editor configuration object
*
* @type {EditorConfig} * @type {EditorConfig}
*/ */
protected config: EditorConfig; protected config: EditorConfig;
/** /**
* @constructor * @class
* @param {EditorConfig} * @param {EditorConfig} config - Editor's config
*/ */
constructor({config}: ModuleConfig) { constructor({ config }: ModuleConfig) {
if (new.target === Module) { if (new.target === Module) {
throw new TypeError('Constructors for abstract class Module are not allowed.'); throw new TypeError('Constructors for abstract class Module are not allowed.');
} }
@ -39,9 +40,10 @@ export default class Module {
/** /**
* Editor modules setter * Editor modules setter
* @param {EditorModules} Editor *
* @param {EditorModules} Editor - Editor's Modules
*/ */
set state(Editor: EditorModules) { public set state(Editor: EditorModules) {
this.Editor = Editor; this.Editor = Editor;
} }
} }

View file

@ -4,14 +4,17 @@
* *
* @copyright <CodeX Team> 2018 * @copyright <CodeX Team> 2018
*/ */
import {API, BlockTune} from '../../../types'; import { API, BlockTune } from '../../../types';
import $ from '../dom'; import $ from '../dom';
/**
*
*/
export default class DeleteTune implements BlockTune { export default class DeleteTune implements BlockTune {
/** /**
* Property that contains Editor.js API methods * Property that contains Editor.js API methods
* @see {docs/api.md} *
* @see {@link docs/api.md}
*/ */
private readonly api: API; private readonly api: API;
@ -32,7 +35,7 @@ export default class DeleteTune implements BlockTune {
/** /**
* set false confirmation state * set false confirmation state
*/ */
private resetConfirmation: () => void; private readonly resetConfirmation: () => void;
/** /**
* Tune nodes * Tune nodes
@ -44,21 +47,22 @@ export default class DeleteTune implements BlockTune {
/** /**
* DeleteTune constructor * DeleteTune constructor
* *
* @param {{api: API}} api * @param {API} api - Editor's API
*/ */
constructor({api}) { constructor({ api }) {
this.api = api; this.api = api;
this.resetConfirmation = () => { this.resetConfirmation = (): void => {
this.setConfirmation(false); this.setConfirmation(false);
}; };
} }
/** /**
* Create "Delete" button and add click event listener * Create "Delete" button and add click event listener
* @returns [Element} *
* @returns {HTMLElement}
*/ */
public render() { public render(): HTMLElement {
this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {}); this.nodes.button = $.make('div', [this.CSS.button, this.CSS.buttonDelete], {});
this.nodes.button.appendChild($.svg('cross', 12, 12)); this.nodes.button.appendChild($.svg('cross', 12, 12));
this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false); this.api.listeners.on(this.nodes.button, 'click', (event: MouseEvent) => this.handleClick(event), false);
@ -66,17 +70,17 @@ export default class DeleteTune implements BlockTune {
/** /**
* Enable tooltip module * Enable tooltip module
*/ */
this.api.tooltip.onHover(this.nodes.button, 'Delete'); this.api.tooltip.onHover(this.nodes.button, this.api.i18n.t('Delete'));
return this.nodes.button; return this.nodes.button;
} }
/** /**
* Delete block conditions passed * Delete block conditions passed
* @param {MouseEvent} event *
* @param {MouseEvent} event - click event
*/ */
public handleClick(event: MouseEvent): void { public handleClick(event: MouseEvent): void {
/** /**
* if block is not waiting the confirmation, subscribe on block-settings-closing event to reset * if block is not waiting the confirmation, subscribe on block-settings-closing event to reset
* otherwise delete block * otherwise delete block
@ -90,9 +94,7 @@ export default class DeleteTune implements BlockTune {
* then reset confirmation state * then reset confirmation state
*/ */
this.api.events.on('block-settings-closed', this.resetConfirmation); this.api.events.on('block-settings-closed', this.resetConfirmation);
} else { } else {
/** /**
* Unsubscribe from block-settings closing event * Unsubscribe from block-settings closing event
*/ */
@ -111,10 +113,11 @@ export default class DeleteTune implements BlockTune {
/** /**
* change tune state * change tune state
*
* @param {boolean} state - delete confirmation state
*/ */
private setConfirmation(state): void { private setConfirmation(state: boolean): void {
this.needConfirmation = state; this.needConfirmation = state;
this.nodes.button.classList.add(this.CSS.buttonConfirm); this.nodes.button.classList.add(this.CSS.buttonConfirm);
} }
} }

View file

@ -6,17 +6,22 @@
*/ */
import $ from '../dom'; import $ from '../dom';
import {API, BlockTune} from '../../../types'; import { API, BlockTune } from '../../../types';
/**
*
*/
export default class MoveDownTune implements BlockTune { export default class MoveDownTune implements BlockTune {
/** /**
* Property that contains Editor.js API methods * Property that contains Editor.js API methods
* @see {api.md} *
* @see {@link docs/api.md}
*/ */
private readonly api: API; private readonly api: API;
/** /**
* Styles * Styles
*
* @type {{wrapper: string}} * @type {{wrapper: string}}
*/ */
private CSS = { private CSS = {
@ -28,53 +33,58 @@ export default class MoveDownTune implements BlockTune {
/** /**
* MoveDownTune constructor * MoveDownTune constructor
* *
* @param {{api: API}} api * @param {API} api Editor's API
*/ */
public constructor({api}) { constructor({ api }) {
this.api = api; this.api = api;
} }
/** /**
* Return 'move down' button * Return 'move down' button
*
* @returns {HTMLElement}
*/ */
public render() { public render(): HTMLElement {
const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {}); const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
moveDownButton.appendChild($.svg('arrow-down', 14, 14)); moveDownButton.appendChild($.svg('arrow-down', 14, 14));
this.api.listeners.on( this.api.listeners.on(
moveDownButton, moveDownButton,
'click', 'click',
(event) => this.handleClick(event as MouseEvent, moveDownButton), (event) => this.handleClick(event as MouseEvent, moveDownButton),
false, false
); );
/** /**
* Enable tooltip module on button * Enable tooltip module on button
*/ */
this.api.tooltip.onHover(moveDownButton, 'Move down'); this.api.tooltip.onHover(moveDownButton, this.api.i18n.t('Move down'));
return moveDownButton; return moveDownButton;
} }
/** /**
* Handle clicks on 'move down' button * Handle clicks on 'move down' button
* @param {MouseEvent} event *
* @param {HTMLElement} button * @param {MouseEvent} event - click event
* @param {HTMLElement} button - clicked button
*/ */
public handleClick(event: MouseEvent, button: HTMLElement) { public handleClick(event: MouseEvent, button: HTMLElement): void {
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
// If Block is last do nothing // If Block is last do nothing
if (currentBlockIndex === this.api.blocks.getBlocksCount() - 1) { if (currentBlockIndex === this.api.blocks.getBlocksCount() - 1) {
button.classList.add(this.CSS.animation); button.classList.add(this.CSS.animation);
window.setTimeout( () => { window.setTimeout(() => {
button.classList.remove(this.CSS.animation); button.classList.remove(this.CSS.animation);
}, 500); }, 500);
return; return;
} }
const nextBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex + 1); const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);
const nextBlockElement = nextBlock.holder;
const nextBlockCoords = nextBlockElement.getBoundingClientRect(); const nextBlockCoords = nextBlockElement.getBoundingClientRect();
let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight); let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight);
@ -84,9 +94,7 @@ export default class MoveDownTune implements BlockTune {
* Increment scroll by next block's height to save element onscreen-position * Increment scroll by next block's height to save element onscreen-position
*/ */
if (nextBlockCoords.top < window.innerHeight) { if (nextBlockCoords.top < window.innerHeight) {
scrollOffset = window.scrollY + nextBlockElement.offsetHeight; scrollOffset = window.scrollY + nextBlockElement.offsetHeight;
} }
window.scrollTo(0, scrollOffset); window.scrollTo(0, scrollOffset);

View file

@ -5,18 +5,22 @@
* @copyright <CodeX Team> 2018 * @copyright <CodeX Team> 2018
*/ */
import $ from '../dom'; import $ from '../dom';
import {API, BlockTune} from '../../../types'; import { API, BlockTune } from '../../../types';
/**
*
*/
export default class MoveUpTune implements BlockTune { export default class MoveUpTune implements BlockTune {
/** /**
* Property that contains Editor.js API methods * Property that contains Editor.js API methods
* @see {api.md} *
* @see {@link docs/api.md}
*/ */
private readonly api: API; private readonly api: API;
/** /**
* Styles * Styles
*
* @type {{wrapper: string}} * @type {{wrapper: string}}
*/ */
private CSS = { private CSS = {
@ -28,54 +32,59 @@ export default class MoveUpTune implements BlockTune {
/** /**
* MoveUpTune constructor * MoveUpTune constructor
* *
* @param {{api: API}} api * @param {API} api - Editor's API
*/ */
public constructor({api}) { constructor({ api }) {
this.api = api; this.api = api;
} }
/** /**
* Create "MoveUp" button and add click event listener * Create "MoveUp" button and add click event listener
* @returns [HTMLElement} *
* @returns {HTMLElement}
*/ */
public render(): HTMLElement { public render(): HTMLElement {
const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {}); const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
moveUpButton.appendChild($.svg('arrow-up', 14, 14)); moveUpButton.appendChild($.svg('arrow-up', 14, 14));
this.api.listeners.on( this.api.listeners.on(
moveUpButton, moveUpButton,
'click', 'click',
(event) => this.handleClick(event as MouseEvent, moveUpButton), (event) => this.handleClick(event as MouseEvent, moveUpButton),
false, false
); );
/** /**
* Enable tooltip module on button * Enable tooltip module on button
*/ */
this.api.tooltip.onHover(moveUpButton, 'Move up'); this.api.tooltip.onHover(moveUpButton, this.api.i18n.t('Move up'));
return moveUpButton; return moveUpButton;
} }
/** /**
* Move current block up * Move current block up
* @param {MouseEvent} event *
* @param {HTMLElement} button * @param {MouseEvent} event - click event
* @param {HTMLElement} button - clicked button
*/ */
public handleClick(event: MouseEvent, button: HTMLElement): void { public handleClick(event: MouseEvent, button: HTMLElement): void {
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex(); const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
if (currentBlockIndex === 0) { if (currentBlockIndex === 0) {
button.classList.add(this.CSS.animation); button.classList.add(this.CSS.animation);
window.setTimeout( () => { window.setTimeout(() => {
button.classList.remove(this.CSS.animation); button.classList.remove(this.CSS.animation);
}, 500); }, 500);
return; return;
} }
const currentBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex); const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
const previousBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex - 1); const currentBlockElement = currentBlock.holder;
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
const previousBlockElement = previousBlock.holder;
/** /**
* Here is two cases: * Here is two cases:
@ -86,7 +95,7 @@ export default class MoveUpTune implements BlockTune {
* than we scroll window to the difference between this offsets. * than we scroll window to the difference between this offsets.
*/ */
const currentBlockCoords = currentBlockElement.getBoundingClientRect(), const currentBlockCoords = currentBlockElement.getBoundingClientRect(),
previousBlockCoords = previousBlockElement.getBoundingClientRect(); previousBlockCoords = previousBlockElement.getBoundingClientRect();
let scrollUpOffset; let scrollUpOffset;

114
src/components/block/api.ts Normal file
View file

@ -0,0 +1,114 @@
import Block from './index';
import { BlockToolData, ToolConfig } from '../../../types/tools';
import { SavedData } from '../../types-internal/block-data';
import { BlockAPI as BlockAPIInterface } from '../../../types/api';
/**
* Constructs new BlockAPI object
*
* @class
*
* @param {Block} block - Block to expose
*/
function BlockAPI(block: Block): void {
const blockAPI: BlockAPIInterface = {
/**
* Tool name
*
* @returns {string}
*/
get name(): string {
return block.name;
},
/**
* Tool config passed on Editor's initialization
*
* @returns {ToolConfig}
*/
get config(): ToolConfig {
return block.config;
},
/**
* .ce-block element, that wraps plugin contents
*
* @returns {HTMLElement}
*/
get holder(): HTMLElement {
return block.holder;
},
/**
* True if Block content is empty
*
* @returns {boolean}
*/
get isEmpty(): boolean {
return block.isEmpty;
},
/**
* True if Block is selected with Cross-Block selection
*
* @returns {boolean}
*/
get selected(): boolean {
return block.selected;
},
/**
* Set Block's stretch state
*
* @param {boolean} state state to set
*/
set stretched(state: boolean) {
block.stretched = state;
},
/**
* True if Block is stretched
*
* @returns {boolean}
*/
get stretched(): boolean {
return block.stretched;
},
/**
* Call Tool method with errors handler under-the-hood
*
* @param {string} methodName - method to call
* @param {object} param - object with parameters
*
* @returns {void}
*/
call(methodName: string, param?: object): void {
block.call(methodName, param);
},
/**
* Save Block content
*
* @returns {Promise<void|SavedData>}
*/
save(): Promise<void|SavedData> {
return block.save();
},
/**
* Validate Block data
*
* @param {BlockToolData} data - data to validate
*
* @returns {Promise<boolean>}
*/
validate(data: BlockToolData): Promise<boolean> {
return block.validate(data);
},
};
Object.setPrototypeOf(this, blockAPI);
}
export default BlockAPI;

View file

@ -1,5 +1,5 @@
import { import {
API, BlockAPI as BlockAPIInterface,
BlockTool, BlockTool,
BlockToolConstructable, BlockToolConstructable,
BlockToolData, BlockToolData,
@ -7,27 +7,61 @@ import {
BlockTuneConstructable, BlockTuneConstructable,
SanitizerConfig, SanitizerConfig,
ToolConfig, ToolConfig,
} from '../../types'; ToolSettings
} from '../../../types';
import {SavedData} from '../types-internal/block-data'; import { SavedData } from '../../types-internal/block-data';
import $ from './dom'; import $ from '../dom';
import * as _ from './utils'; import * as _ from '../utils';
import ApiModule from '../modules/api';
import SelectionUtils from '../selection';
import BlockAPI from './api';
import { ToolType } from '../modules/tools';
/** Import default tunes */
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';
/**
* Interface describes Block class constructor argument
*/
interface BlockConstructorOptions {
/**
* Tool's name
*/
name: string;
/**
* Initial Block data
*/
data: BlockToolData;
/**
* Tool's class or constructor function
*/
Tool: BlockToolConstructable;
/**
* Tool settings from initial config
*/
settings: ToolSettings;
/**
* Editor's API methods
*/
api: ApiModule;
}
/** /**
* @class Block * @class Block
* @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool * @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool
* *
* @property {BlockTool} tool current block tool (Paragraph, for example) * @property {BlockTool} tool current block tool (Paragraph, for example)
* @property {Object} CSS block`s css classes * @property {object} CSS block`s css classes
* *
*/ */
/** Import default tunes */
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';
/** /**
* Available Block Tool API methods * Available Block Tool API methods
*/ */
@ -47,18 +81,17 @@ export enum BlockToolAPI {
/** /**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance * @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
* *
* @property tool - Tool instance * @property {BlockTool} tool - Tool instance
* @property html - Returns HTML content of plugin * @property {HTMLElement} holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class
* @property holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class * @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function
* @property pluginsContent - HTML content that returns by Tool's render function
*/ */
export default class Block { export default class Block {
/** /**
* CSS classes for the Block * CSS classes for the Block
* @return {{wrapper: string, content: string}} *
* @returns {{wrapper: string, content: string}}
*/ */
static get CSS() { public static get CSS(): {[name: string]: string} {
return { return {
wrapper: 'ce-block', wrapper: 'ce-block',
wrapperStretched: 'ce-block--stretched', wrapperStretched: 'ce-block--stretched',
@ -69,247 +102,6 @@ export default class Block {
}; };
} }
/**
* Find and return all editable elements (contenteditables and native inputs) in the Tool HTML
*
* @returns {HTMLElement[]}
*/
get inputs(): HTMLElement[] {
/**
* Return from cache if existed
*/
if (this.cachedInputs.length !== 0) {
return this.cachedInputs;
}
const content = this.holder;
const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];
const selector = '[contenteditable], textarea, input:not([type]), '
+ allowedInputTypes.map((type) => `input[type="${type}"]`).join(', ');
let inputs = _.array(content.querySelectorAll(selector));
/**
* If contenteditable element contains block elements, treat them as inputs.
*/
inputs = inputs.reduce((result, input) => {
if ($.isNativeInput(input) || $.containsOnlyInlineElements(input)) {
return [...result, input];
}
return [...result, ...$.getDeepestBlockElements(input)];
}, []);
/**
* If inputs amount was changed we need to check if input index is bigger then inputs array length
*/
if (this.inputIndex > inputs.length - 1) {
this.inputIndex = inputs.length - 1;
}
/**
* Cache inputs
*/
this.cachedInputs = inputs;
return inputs;
}
/**
* Return current Tool`s input
*
* @returns {HTMLElement}
*/
get currentInput(): HTMLElement | Node {
return this.inputs[this.inputIndex];
}
/**
* Set input index to the passed element
*
* @param {HTMLElement} element
*/
set currentInput(element: HTMLElement | Node) {
const index = this.inputs.findIndex((input) => input === element || input.contains(element));
if (index !== -1) {
this.inputIndex = index;
}
}
/**
* Return first Tool`s input
*
* @returns {HTMLElement}
*/
get firstInput(): HTMLElement {
return this.inputs[0];
}
/**
* Return first Tool`s input
*
* @returns {HTMLElement}
*/
get lastInput(): HTMLElement {
const inputs = this.inputs;
return inputs[inputs.length - 1];
}
/**
* Return next Tool`s input or undefined if it doesn't exist
*
* @returns {HTMLElement}
*/
get nextInput(): HTMLElement {
return this.inputs[this.inputIndex + 1];
}
/**
* Return previous Tool`s input or undefined if it doesn't exist
*
* @returns {HTMLElement}
*/
get previousInput(): HTMLElement {
return this.inputs[this.inputIndex - 1];
}
/**
* Returns Plugins content
* @return {HTMLElement}
*/
get pluginsContent(): HTMLElement {
const blockContentNodes = this.holder.querySelector(`.${Block.CSS.content}`);
if (blockContentNodes && blockContentNodes.childNodes.length) {
/**
* Editors Block content can contain different Nodes from extensions
* We use DOM isExtensionNode to ignore such Nodes and return first Block that does not match filtering list
*/
for (let child = blockContentNodes.childNodes.length - 1; child >= 0; child--) {
const contentNode = blockContentNodes.childNodes[child];
if (!$.isExtensionNode(contentNode)) {
return contentNode as HTMLElement;
}
}
}
return null;
}
/**
* Get Block's JSON data
* @return {Object}
*/
get data(): BlockToolData {
return this.save().then((savedObject) => {
if (savedObject && !_.isEmpty(savedObject.data)) {
return savedObject.data;
} else {
return {};
}
});
}
/**
* Returns tool's sanitizer config
* @return {object}
*/
get sanitize(): SanitizerConfig {
return this.tool.sanitize;
}
/**
* is block mergeable
* We plugin have merge function then we call it mergable
* @return {boolean}
*/
get mergeable(): boolean {
return typeof this.tool.merge === 'function';
}
/**
* Check block for emptiness
* @return {Boolean}
*/
get isEmpty(): boolean {
const emptyText = $.isEmpty(this.pluginsContent);
const emptyMedia = !this.hasMedia;
return emptyText && emptyMedia;
}
/**
* Check if block has a media content such as images, iframes and other
* @return {Boolean}
*/
get hasMedia(): boolean {
/**
* This tags represents media-content
* @type {string[]}
*/
const mediaTags = [
'img',
'iframe',
'video',
'audio',
'source',
'input',
'textarea',
'twitterwidget',
];
return !!this.holder.querySelector(mediaTags.join(','));
}
/**
* Set focused state
* @param {Boolean} state - 'true' to select, 'false' to remove selection
*/
set focused(state: boolean) {
this.holder.classList.toggle(Block.CSS.focused, state);
}
/**
* Set selected state
* We don't need to mark Block as Selected when it is empty
* @param {Boolean} state - 'true' to select, 'false' to remove selection
*/
set selected(state: boolean) {
if (state) {
this.holder.classList.add(Block.CSS.selected);
} else {
this.holder.classList.remove(Block.CSS.selected);
}
}
/**
* Returns True if it is Selected
* @return {boolean}
*/
get selected(): boolean {
return this.holder.classList.contains(Block.CSS.selected);
}
/**
* Set stretched state
* @param {Boolean} state - 'true' to enable, 'false' to disable stretched statte
*/
set stretched(state: boolean) {
this.holder.classList.toggle(Block.CSS.wrapperStretched, state);
}
/**
* Toggle drop target state
* @param {boolean} state
*/
public set dropTarget(state) {
this.holder.classList.toggle(Block.CSS.dropTarget, state);
}
/** /**
* Block Tool`s name * Block Tool`s name
*/ */
@ -340,31 +132,40 @@ export default class Block {
*/ */
public tunes: BlockTune[]; public tunes: BlockTune[];
/**
* Tool's user configuration
*/
public readonly config: ToolConfig;
/** /**
* Cached inputs * Cached inputs
*
* @type {HTMLElement[]} * @type {HTMLElement[]}
*/ */
private cachedInputs: HTMLElement[] = []; private cachedInputs: HTMLElement[] = [];
/** /**
* Editor`s API * Editor`s API module
*/ */
private readonly api: API; private readonly api: ApiModule;
/** /**
* Focused input index * Focused input index
*
* @type {number} * @type {number}
*/ */
private inputIndex = 0; private inputIndex = 0;
/** /**
* Mutation observer to handle DOM mutations * Mutation observer to handle DOM mutations
*
* @type {MutationObserver} * @type {MutationObserver}
*/ */
private mutationObserver: MutationObserver; private mutationObserver: MutationObserver;
/** /**
* Debounce Timer * Debounce Timer
*
* @type {number} * @type {number}
*/ */
private readonly modificationDebounceTimer = 450; private readonly modificationDebounceTimer = 450;
@ -387,49 +188,323 @@ export default class Block {
}, this.modificationDebounceTimer); }, this.modificationDebounceTimer);
/** /**
* @constructor * Current block API interface
* @param {String} toolName - Tool name that passed on initialization
* @param {Object} toolInstance passed Tool`s instance that rendered the Block
* @param {Object} toolClass Tool's class
* @param {Object} settings - default settings
* @param {Object} apiMethods - Editor API
*/ */
constructor( private readonly blockAPI: BlockAPIInterface;
toolName: string,
toolInstance: BlockTool, /**
toolClass: BlockToolConstructable, * @param {object} options - block constructor options
settings: ToolConfig, * @param {string} options.name - Tool name that passed on initialization
apiMethods: API, * @param {BlockToolData} options.data - Tool's initial data
) { * @param {BlockToolConstructable} options.Tool Tool's class
this.name = toolName; * @param {ToolSettings} options.settings - default tool's config
this.tool = toolInstance; * @param {ApiModule} options.api - Editor API module for pass it to the Block Tunes
this.class = toolClass; */
constructor({
name,
data,
Tool,
settings,
api,
}: BlockConstructorOptions) {
this.name = name;
this.class = Tool;
this.settings = settings; this.settings = settings;
this.api = apiMethods; this.config = settings.config || {};
this.holder = this.compose(); this.api = api;
this.blockAPI = new BlockAPI(this);
this.mutationObserver = new MutationObserver(this.didMutated); this.mutationObserver = new MutationObserver(this.didMutated);
this.tool = new Tool({
data,
config: this.config,
api: this.api.getMethodsForTool(name, ToolType.Block),
block: this.blockAPI,
});
this.holder = this.compose();
/** /**
* @type {BlockTune[]} * @type {BlockTune[]}
*/ */
this.tunes = this.makeTunes(); this.tunes = this.makeTunes();
} }
/**
* Find and return all editable elements (contenteditables and native inputs) in the Tool HTML
*
* @returns {HTMLElement[]}
*/
public get inputs(): HTMLElement[] {
/**
* Return from cache if existed
*/
if (this.cachedInputs.length !== 0) {
return this.cachedInputs;
}
const inputs = $.findAllInputs(this.holder);
/**
* If inputs amount was changed we need to check if input index is bigger then inputs array length
*/
if (this.inputIndex > inputs.length - 1) {
this.inputIndex = inputs.length - 1;
}
/**
* Cache inputs
*/
this.cachedInputs = inputs;
return inputs;
}
/**
* Return current Tool`s input
*
* @returns {HTMLElement}
*/
public get currentInput(): HTMLElement | Node {
return this.inputs[this.inputIndex];
}
/**
* Set input index to the passed element
*
* @param {HTMLElement | Node} element - HTML Element to set as current input
*/
public set currentInput(element: HTMLElement | Node) {
const index = this.inputs.findIndex((input) => input === element || input.contains(element));
if (index !== -1) {
this.inputIndex = index;
}
}
/**
* Return first Tool`s input
*
* @returns {HTMLElement}
*/
public get firstInput(): HTMLElement {
return this.inputs[0];
}
/**
* Return first Tool`s input
*
* @returns {HTMLElement}
*/
public get lastInput(): HTMLElement {
const inputs = this.inputs;
return inputs[inputs.length - 1];
}
/**
* Return next Tool`s input or undefined if it doesn't exist
*
* @returns {HTMLElement}
*/
public get nextInput(): HTMLElement {
return this.inputs[this.inputIndex + 1];
}
/**
* Return previous Tool`s input or undefined if it doesn't exist
*
* @returns {HTMLElement}
*/
public get previousInput(): HTMLElement {
return this.inputs[this.inputIndex - 1];
}
/**
* Get Block's JSON data
*
* @returns {object}
*/
public get data(): Promise<BlockToolData> {
return this.save().then((savedObject) => {
if (savedObject && !_.isEmpty(savedObject.data)) {
return savedObject.data;
} else {
return {};
}
});
}
/**
* Returns tool's sanitizer config
*
* @returns {object}
*/
public get sanitize(): SanitizerConfig {
return this.tool.sanitize;
}
/**
* is block mergeable
* We plugin have merge function then we call it mergable
*
* @returns {boolean}
*/
public mergeable(): boolean {
return typeof this.tool.merge === 'function';
}
/**
* Check block for emptiness
*
* @returns {boolean}
*/
public get isEmpty(): boolean {
const emptyText = $.isEmpty(this.pluginsContent);
const emptyMedia = !this.hasMedia;
return emptyText && emptyMedia;
}
/**
* Check if block has a media content such as images, iframes and other
*
* @returns {boolean}
*/
public get hasMedia(): boolean {
/**
* This tags represents media-content
*
* @type {string[]}
*/
const mediaTags = [
'img',
'iframe',
'video',
'audio',
'source',
'input',
'textarea',
'twitterwidget',
];
return !!this.holder.querySelector(mediaTags.join(','));
}
/**
* Set focused state
*
* @param {boolean} state - 'true' to select, 'false' to remove selection
*/
public set focused(state: boolean) {
this.holder.classList.toggle(Block.CSS.focused, state);
}
/**
* Get Block's focused state
*/
public get focused(): boolean {
return this.holder.classList.contains(Block.CSS.focused);
}
/**
* Set selected state
* We don't need to mark Block as Selected when it is empty
*
* @param {boolean} state - 'true' to select, 'false' to remove selection
*/
public set selected(state: boolean) {
if (state) {
this.holder.classList.add(Block.CSS.selected);
} else {
this.holder.classList.remove(Block.CSS.selected);
}
}
/**
* Returns True if it is Selected
*
* @returns {boolean}
*/
public get selected(): boolean {
return this.holder.classList.contains(Block.CSS.selected);
}
/**
* Set stretched state
*
* @param {boolean} state - 'true' to enable, 'false' to disable stretched statte
*/
public set stretched(state: boolean) {
this.holder.classList.toggle(Block.CSS.wrapperStretched, state);
}
/**
* Return Block's stretched state
*
* @returns {boolean}
*/
public get stretched(): boolean {
return this.holder.classList.contains(Block.CSS.wrapperStretched);
}
/**
* Toggle drop target state
*
* @param {boolean} state - 'true' if block is drop target, false otherwise
*/
public set dropTarget(state) {
this.holder.classList.toggle(Block.CSS.dropTarget, state);
}
/**
* Returns Plugins content
*
* @returns {HTMLElement}
*/
public get pluginsContent(): HTMLElement {
const blockContentNodes = this.holder.querySelector(`.${Block.CSS.content}`);
if (blockContentNodes && blockContentNodes.childNodes.length) {
/**
* Editors Block content can contain different Nodes from extensions
* We use DOM isExtensionNode to ignore such Nodes and return first Block that does not match filtering list
*/
for (let child = blockContentNodes.childNodes.length - 1; child >= 0; child--) {
const contentNode = blockContentNodes.childNodes[child];
if (!$.isExtensionNode(contentNode)) {
return contentNode as HTMLElement;
}
}
}
return null;
}
/** /**
* Calls Tool's method * Calls Tool's method
* *
* Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function * Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function
* *
* @param {String} methodName * @param {string} methodName - method to call
* @param {Object} params * @param {object} params - method argument
*/ */
public call(methodName: string, params?: object) { public call(methodName: string, params?: object): void {
/** /**
* call Tool's method with the instance context * call Tool's method with the instance context
*/ */
if (this.tool[methodName] && this.tool[methodName] instanceof Function) { if (this.tool[methodName] && this.tool[methodName] instanceof Function) {
if (methodName === BlockToolAPI.APPEND_CALLBACK) {
_.log(
'`appendCallback` hook is deprecated and will be removed in the next major release. ' +
'Use `rendered` hook instead',
'warn'
);
}
try { try {
// eslint-disable-next-line no-useless-call
this.tool[methodName].call(this.tool, params); this.tool[methodName].call(this.tool, params);
} catch (e) { } catch (e) {
_.log(`Error during '${methodName}' call: ${e.message}`, 'error'); _.log(`Error during '${methodName}' call: ${e.message}`, 'error');
@ -439,15 +514,18 @@ export default class Block {
/** /**
* Call plugins merge method * Call plugins merge method
* @param {Object} data *
* @param {BlockToolData} data - data to merge
*/ */
public async mergeWith(data: BlockToolData): Promise<void> { public async mergeWith(data: BlockToolData): Promise<void> {
await this.tool.merge(data); await this.tool.merge(data);
} }
/** /**
* Extracts data from Block * Extracts data from Block
* Groups Tool's save processing time * Groups Tool's save processing time
* @return {Object} *
* @returns {object}
*/ */
public async save(): Promise<void|SavedData> { public async save(): Promise<void|SavedData> {
const extractedBlock = await this.tool.save(this.pluginsContent as HTMLElement); const extractedBlock = await this.tool.save(this.pluginsContent as HTMLElement);
@ -480,7 +558,7 @@ export default class Block {
* *
* @description Method returns true|false whether data passed the validation or not * @description Method returns true|false whether data passed the validation or not
* *
* @param {BlockToolData} data * @param {BlockToolData} data - data to validate
* @returns {Promise<boolean>} valid * @returns {Promise<boolean>} valid
*/ */
public async validate(data: BlockToolData): Promise<boolean> { public async validate(data: BlockToolData): Promise<boolean> {
@ -496,28 +574,43 @@ export default class Block {
/** /**
* Make an array with default settings * Make an array with default settings
* Each block has default tune instance that have states * Each block has default tune instance that have states
* @return {BlockTune[]} *
* @returns {BlockTune[]}
*/ */
public makeTunes(): BlockTune[] { public makeTunes(): BlockTune[] {
const tunesList = [MoveUpTune, DeleteTune, MoveDownTune]; const tunesList = [
{
name: 'moveUp',
Tune: MoveUpTune,
},
{
name: 'delete',
Tune: DeleteTune,
},
{
name: 'moveDown',
Tune: MoveDownTune,
},
];
// Pluck tunes list and return tune instances with passed Editor API and settings // Pluck tunes list and return tune instances with passed Editor API and settings
return tunesList.map( (tune: BlockTuneConstructable) => { return tunesList.map(({ name, Tune }: {name: string; Tune: BlockTuneConstructable}) => {
return new tune({ return new Tune({
api: this.api, api: this.api.getMethodsForTool(name, ToolType.Tune),
settings: this.settings, settings: this.config,
}); });
}); });
} }
/** /**
* Enumerates initialized tunes and returns fragment that can be appended to the toolbars area * Enumerates initialized tunes and returns fragment that can be appended to the toolbars area
* @return {DocumentFragment} *
* @returns {DocumentFragment}
*/ */
public renderTunes(): DocumentFragment { public renderTunes(): DocumentFragment {
const tunesElement = document.createDocumentFragment(); const tunesElement = document.createDocumentFragment();
this.tunes.forEach( (tune) => { this.tunes.forEach((tune) => {
$.append(tunesElement, tune.render()); $.append(tunesElement, tune.render());
}); });
@ -545,28 +638,30 @@ export default class Block {
subtree: true, subtree: true,
characterData: true, characterData: true,
attributes: true, attributes: true,
}, }
); );
} }
/** /**
* Is fired when Block will be unselected * Is fired when Block will be unselected
*/ */
public willUnselect() { public willUnselect(): void {
this.mutationObserver.disconnect(); this.mutationObserver.disconnect();
} }
/** /**
* Make default Block wrappers and put Tool`s content there * Make default Block wrappers and put Tool`s content there
*
* @returns {HTMLDivElement} * @returns {HTMLDivElement}
*/ */
private compose(): HTMLDivElement { private compose(): HTMLDivElement {
const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement, const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement,
contentNode = $.make('div', Block.CSS.content), contentNode = $.make('div', Block.CSS.content),
pluginsContent = this.tool.render(); pluginsContent = this.tool.render();
contentNode.appendChild(pluginsContent); contentNode.appendChild(pluginsContent);
wrapper.appendChild(contentNode); wrapper.appendChild(contentNode);
return wrapper; return wrapper;
} }
} }

View file

@ -1,7 +1,7 @@
import * as _ from './utils'; import * as _ from './utils';
import $ from './dom'; import $ from './dom';
import Block, { BlockToolAPI } from './block'; import Block, { BlockToolAPI } from './block';
import {MoveEvent, MoveEventDetail} from '../../types/tools'; import { MoveEvent } from '../../types/tools';
/** /**
* @class Blocks * @class Blocks
@ -13,10 +13,30 @@ import {MoveEvent, MoveEventDetail} from '../../types/tools';
* *
*/ */
export default class Blocks { export default class Blocks {
/**
* Array of Block instances in order of addition
*/
public blocks: Block[];
/**
* Editor`s area where to add Block`s HTML
*/
public workingArea: HTMLElement;
/**
* @class
*
* @param {HTMLElement} workingArea editor`s working node
*/
constructor(workingArea: HTMLElement) {
this.blocks = [];
this.workingArea = workingArea;
}
/** /**
* Get length of Block instances array * Get length of Block instances array
* *
* @returns {Number} * @returns {number}
*/ */
public get length(): number { public get length(): number {
return this.blocks.length; return this.blocks.length;
@ -47,17 +67,17 @@ export default class Blocks {
* blocks[0] = new Block(...) * blocks[0] = new Block(...)
* *
* @param {Blocks} instance Blocks instance * @param {Blocks} instance Blocks instance
* @param {Number|String} property block index or any Blocks class property to set * @param {PropertyKey} property block index or any Blocks class property key to set
* @param {Block} value value to set * @param {Block} value value to set
* @returns {Boolean} * @returns {boolean}
*/ */
public static set(instance: Blocks, property: number | string, value: Block | any) { public static set(instance: Blocks, property: PropertyKey, value: Block | unknown): boolean {
/** /**
* If property name is not a number (method or other property, access it via reflect * If property name is not a number (method or other property, access it via reflect
*/ */
if (isNaN(Number(property))) { if (isNaN(Number(property))) {
Reflect.set(instance, property, value); Reflect.set(instance, property, value);
return true; return true;
} }
@ -67,7 +87,7 @@ export default class Blocks {
* @example * @example
* blocks[0] = new Block(); * blocks[0] = new Block();
*/ */
instance.insert(+property, value); instance.insert(+(property as number), value as Block);
return true; return true;
} }
@ -76,11 +96,10 @@ export default class Blocks {
* Proxy trap to implement array-like getter * Proxy trap to implement array-like getter
* *
* @param {Blocks} instance Blocks instance * @param {Blocks} instance Blocks instance
* @param {Number|String} property Blocks class property * @param {PropertyKey} property Blocks class property key
* @returns {Block|*} * @returns {Block|*}
*/ */
public static get(instance: Blocks, property: any | number) { public static get(instance: Blocks, property: PropertyKey): Block | unknown {
/** /**
* If property is not a number, get it via Reflect object * If property is not a number, get it via Reflect object
*/ */
@ -91,33 +110,13 @@ export default class Blocks {
/** /**
* If property is a number (Block index) return Block by passed index * If property is a number (Block index) return Block by passed index
*/ */
return instance.get(+property); return instance.get(+(property as number));
}
/**
* Array of Block instances in order of addition
*/
public blocks: Block[];
/**
* Editor`s area where to add Block`s HTML
*/
public workingArea: HTMLElement;
/**
* @constructor
*
* @param {HTMLElement} workingArea editor`s working node
*/
constructor(workingArea: HTMLElement) {
this.blocks = [];
this.workingArea = workingArea;
} }
/** /**
* Push new Block to the blocks array and append it to working area * Push new Block to the blocks array and append it to working area
* *
* @param {Block} block * @param {Block} block - Block to add
*/ */
public push(block: Block): void { public push(block: Block): void {
this.blocks.push(block); this.blocks.push(block);
@ -126,8 +125,9 @@ export default class Blocks {
/** /**
* Swaps blocks with indexes first and second * Swaps blocks with indexes first and second
* @param {Number} first - first block index *
* @param {Number} second - second block index * @param {number} first - first block index
* @param {number} second - second block index
* @deprecated use 'move' instead * @deprecated use 'move' instead
*/ */
public swap(first: number, second: number): void { public swap(first: number, second: number): void {
@ -147,13 +147,15 @@ export default class Blocks {
/** /**
* Move a block from one to another index * Move a block from one to another index
* @param {Number} toIndex - new index of the block *
* @param {Number} fromIndex - block to move * @param {number} toIndex - new index of the block
* @param {number} fromIndex - block to move
*/ */
public move(toIndex: number, fromIndex: number): void { public move(toIndex: number, fromIndex: number): void {
/** /**
* cut out the block, move the DOM element and insert at the desired index * cut out the block, move the DOM element and insert at the desired index
* again (the shifting within the blocks array will happen automatically). * again (the shifting within the blocks array will happen automatically).
*
* @see https://stackoverflow.com/a/44932690/1238150 * @see https://stackoverflow.com/a/44932690/1238150
*/ */
const block = this.blocks.splice(fromIndex, 1)[0]; const block = this.blocks.splice(fromIndex, 1)[0];
@ -184,13 +186,14 @@ export default class Blocks {
/** /**
* Insert new Block at passed index * Insert new Block at passed index
* *
* @param {Number} index index to insert Block * @param {number} index index to insert Block
* @param {Block} block Block to insert * @param {Block} block Block to insert
* @param {Boolean} replace it true, replace block on given index * @param {boolean} replace it true, replace block on given index
*/ */
public insert(index: number, block: Block, replace: boolean = false): void { public insert(index: number, block: Block, replace = false): void {
if (!this.length) { if (!this.length) {
this.push(block); this.push(block);
return; return;
} }
@ -224,7 +227,8 @@ export default class Blocks {
/** /**
* Remove block * Remove block
* @param {Number|null} index *
* @param {number} index - index of Block to remove
*/ */
public remove(index: number): void { public remove(index: number): void {
if (isNaN(index)) { if (isNaN(index)) {
@ -254,7 +258,7 @@ export default class Blocks {
* *
* @todo decide if this method is necessary * @todo decide if this method is necessary
* *
* @param {Block} targetBlock target after wich Block should be inserted * @param {Block} targetBlock target after which Block should be inserted
* @param {Block} newBlock Block to insert * @param {Block} newBlock Block to insert
*/ */
public insertAfter(targetBlock: Block, newBlock: Block): void { public insertAfter(targetBlock: Block, newBlock: Block): void {
@ -266,7 +270,7 @@ export default class Blocks {
/** /**
* Get Block by index * Get Block by index
* *
* @param {Number} index Block index * @param {number} index Block index
* @returns {Block} * @returns {Block}
*/ */
public get(index: number): Block { public get(index: number): Block {
@ -276,8 +280,8 @@ export default class Blocks {
/** /**
* Return index of passed Block * Return index of passed Block
* *
* @param {Block} block * @param {Block} block - Block to find
* @returns {Number} * @returns {number}
*/ */
public indexOf(block: Block): number { public indexOf(block: Block): number {
return this.blocks.indexOf(block); return this.blocks.indexOf(block);
@ -303,13 +307,12 @@ export default class Blocks {
/** /**
* Composes Block event with passed type and details * Composes Block event with passed type and details
* *
* @param {String} type * @param {string} type - event type
* @param {MoveEventDetail} detail * @param {object} detail - event detail
*/ */
private composeBlockEvent(type: string, detail: MoveEventDetail): MoveEvent { private composeBlockEvent(type: string, detail: object): MoveEvent {
return new CustomEvent(type, { return new CustomEvent(type, {
detail, detail,
}, }) as MoveEvent;
) as MoveEvent;
} }
} }

View file

@ -1,8 +1,11 @@
import $ from './dom'; import $ from './dom';
// eslint-disable-next-line import/no-duplicates
import * as _ from './utils'; import * as _ from './utils';
import {EditorConfig, OutputData, SanitizerConfig} from '../../types'; // eslint-disable-next-line import/no-duplicates
import {EditorModules} from '../types-internal/editor-modules'; import { LogLevels } from './utils';
import {LogLevels} from './utils'; import { EditorConfig, OutputData, SanitizerConfig } from '../../types';
import { EditorModules } from '../types-internal/editor-modules';
import I18n from './i18n';
/** /**
* @typedef {Core} Core - editor core class * @typedef {Core} Core - editor core class
@ -31,13 +34,12 @@ contextRequire.keys().forEach((filename) => {
* *
* @classdesc Editor.js core class * @classdesc Editor.js core class
* *
* @property this.config - all settings * @property {EditorConfig} config - all settings
* @property this.moduleInstances - constructed editor components * @property {EditorModules} moduleInstances - constructed editor components
* *
* @type {Core} * @type {Core}
*/ */
export default class Core { export default class Core {
/** /**
* Editor configuration passed by user to the constructor * Editor configuration passed by user to the constructor
*/ */
@ -82,9 +84,10 @@ export default class Core {
await this.render(); await this.render();
if ((this.configuration as EditorConfig).autofocus) { if ((this.configuration as EditorConfig).autofocus) {
const {BlockManager, Caret} = this.moduleInstances; const { BlockManager, Caret } = this.moduleInstances;
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START); Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
BlockManager.highlightCurrentNode();
} }
/** /**
@ -110,9 +113,10 @@ export default class Core {
/** /**
* Setting for configuration * Setting for configuration
* @param {EditorConfig|string|undefined} config *
* @param {EditorConfig|string} config - Editor's config to set
*/ */
set configuration(config: EditorConfig|string) { public set configuration(config: EditorConfig|string) {
/** /**
* Process zero-configuration or with only holderId * Process zero-configuration or with only holderId
* Make config object * Make config object
@ -129,11 +133,13 @@ export default class Core {
if (config.holderId && !config.holder) { if (config.holderId && !config.holder) {
config.holder = config.holderId; config.holder = config.holderId;
config.holderId = null; config.holderId = null;
_.log('holderId property will deprecated in next major release, use holder property instead.', 'warn'); _.log('holderId property is deprecated and will be removed in the next major release. ' +
'Use holder property instead.', 'warn');
} }
/** /**
* Place config into the class property * Place config into the class property
*
* @type {EditorConfig} * @type {EditorConfig}
*/ */
this.config = config; this.config = config;
@ -158,18 +164,20 @@ export default class Core {
/** /**
* Height of Editor's bottom area that allows to set focus on the last Block * Height of Editor's bottom area that allows to set focus on the last Block
*
* @type {number} * @type {number}
*/ */
this.config.minHeight = this.config.minHeight !== undefined ? this.config.minHeight : 300 ; this.config.minHeight = this.config.minHeight !== undefined ? this.config.minHeight : 300;
/** /**
* Initial block type * Initial block type
* Uses in case when there is no blocks passed * Uses in case when there is no blocks passed
*
* @type {{type: (*), data: {text: null}}} * @type {{type: (*), data: {text: null}}}
*/ */
const initialBlockData = { const initialBlockData = {
type : this.config.initialBlock, type: this.config.initialBlock,
data : {}, data: {},
}; };
this.config.placeholder = this.config.placeholder || false; this.config.placeholder = this.config.placeholder || false;
@ -182,8 +190,10 @@ export default class Core {
this.config.hideToolbar = this.config.hideToolbar ? this.config.hideToolbar : false; this.config.hideToolbar = this.config.hideToolbar ? this.config.hideToolbar : false;
this.config.tools = this.config.tools || {}; this.config.tools = this.config.tools || {};
this.config.data = this.config.data || {} as OutputData; this.config.data = this.config.data || {} as OutputData;
this.config.onReady = this.config.onReady || (() => {}); // eslint-disable-next-line @typescript-eslint/no-empty-function
this.config.onChange = this.config.onChange || (() => {}); this.config.onReady = this.config.onReady || ((): void => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.config.onChange = this.config.onChange || ((): void => {});
/** /**
* Initialize Blocks to pass data to the Renderer * Initialize Blocks to pass data to the Renderer
@ -196,18 +206,27 @@ export default class Core {
this.config.data.blocks = [ initialBlockData ]; this.config.data.blocks = [ initialBlockData ];
} }
} }
/**
* Adjust i18n
*/
if (config.i18n && config.i18n.messages) {
I18n.setDictionary(config.i18n.messages);
}
} }
/** /**
* Returns private property * Returns private property
*
* @returns {EditorConfig} * @returns {EditorConfig}
*/ */
get configuration(): EditorConfig|string { public get configuration(): EditorConfig|string {
return this.config; return this.config;
} }
/** /**
* Checks for required fields in Editor's config * Checks for required fields in Editor's config
*
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async validate(): Promise<void> { public async validate(): Promise<void> {
@ -234,7 +253,7 @@ export default class Core {
* - make and save instances * - make and save instances
* - configure * - configure
*/ */
public init() { public init(): void {
/** /**
* Make modules instances and save it to the @property this.moduleInstances * Make modules instances and save it to the @property this.moduleInstances
*/ */
@ -250,9 +269,10 @@ export default class Core {
* Start Editor! * Start Editor!
* *
* Get list of modules that needs to be prepared and return a sequence (Promise) * Get list of modules that needs to be prepared and return a sequence (Promise)
* @return {Promise} *
* @returns {Promise<void>}
*/ */
public async start() { public async start(): Promise<void> {
const modulesToPrepare = [ const modulesToPrepare = [
'Tools', 'Tools',
'UI', 'UI',
@ -275,7 +295,7 @@ export default class Core {
} }
// _.log(`Preparing ${module} module`, 'timeEnd'); // _.log(`Preparing ${module} module`, 'timeEnd');
}), }),
Promise.resolve(), Promise.resolve()
); );
} }
@ -290,26 +310,26 @@ export default class Core {
* Make modules instances and save it to the @property this.moduleInstances * Make modules instances and save it to the @property this.moduleInstances
*/ */
private constructModules(): void { private constructModules(): void {
modules.forEach( (module) => { modules.forEach((module) => {
/** /**
* If module has non-default exports, passed object contains them all and default export as 'default' property * 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 = typeof module === 'function' ? module : module.default;
try { try {
/** /**
* We use class name provided by displayName property * We use class name provided by displayName property
* *
* On build, Babel will transform all Classes to the Functions so, name will always be 'Function' * On build, Babel will transform all Classes to the Functions so, name will always be 'Function'
* To prevent this, we use 'babel-plugin-class-display-name' plugin * To prevent this, we use 'babel-plugin-class-display-name' plugin
*
* @see https://www.npmjs.com/package/babel-plugin-class-display-name * @see https://www.npmjs.com/package/babel-plugin-class-display-name
*/ */
this.moduleInstances[Module.displayName] = new Module({ this.moduleInstances[Module.displayName] = new Module({
config : this.configuration, config: this.configuration,
}); });
} catch ( e ) { } catch (e) {
_.log(`Module ${Module.displayName} skipped because`, 'warn', e); _.log(`Module ${Module.displayName} skipped because`, 'warn', e);
} }
}); });
} }
@ -321,7 +341,7 @@ export default class Core {
*/ */
private configureModules(): void { private configureModules(): void {
for (const name in this.moduleInstances) { for (const name in this.moduleInstances) {
if (this.moduleInstances.hasOwnProperty(name)) { if (Object.prototype.hasOwnProperty.call(this.moduleInstances, name)) {
/** /**
* Module does not need self-instance * Module does not need self-instance
*/ */
@ -332,6 +352,7 @@ export default class Core {
/** /**
* Return modules without passed name * Return modules without passed name
*
* @param {string} name - module for witch modules difference should be calculated * @param {string} name - module for witch modules difference should be calculated
*/ */
private getModulesDiff(name: string): EditorModules { private getModulesDiff(name: string): EditorModules {

View file

@ -1,11 +1,14 @@
import * as _ from './utils';
/** /**
* DOM manipulations helper * DOM manipulations helper
*/ */
export default class Dom { export default class Dom {
/** /**
* Check if passed tag has no closed tag * Check if passed tag has no closed tag
* @param {HTMLElement} tag *
* @return {Boolean} * @param {HTMLElement} tag - element to check
* @returns {boolean}
*/ */
public static isSingleTag(tag: HTMLElement): boolean { public static isSingleTag(tag: HTMLElement): boolean {
return tag.tagName && [ return tag.tagName && [
@ -31,10 +34,10 @@ export default class Dom {
/** /**
* Check if element is BR or WBR * Check if element is BR or WBR
* *
* @param {HTMLElement} element * @param {HTMLElement} element - element to check
* @return {boolean} * @returns {boolean}
*/ */
public static isLineBreakTag(element: HTMLElement) { public static isLineBreakTag(element: HTMLElement): element is HTMLBRElement {
return element && element.tagName && [ return element && element.tagName && [
'BR', 'BR',
'WBR', 'WBR',
@ -44,22 +47,23 @@ export default class Dom {
/** /**
* Helper for making Elements with classname and attributes * Helper for making Elements with classname and attributes
* *
* @param {string} tagName - new Element tag name * @param {string} tagName - new Element tag name
* @param {array|string} classNames - list or name of CSS classname(s) * @param {string[]|string} [classNames] - list or name of CSS classname(s)
* @param {Object} attributes - any attributes * @param {object} [attributes] - any attributes
* @return {HTMLElement} *
* @returns {HTMLElement}
*/ */
public static make(tagName: string, classNames: string|string[] = null, attributes: object = {}): HTMLElement { public static make(tagName: string, classNames: string|string[] = null, attributes: object = {}): HTMLElement {
const el = document.createElement(tagName); const el = document.createElement(tagName);
if ( Array.isArray(classNames) ) { if (Array.isArray(classNames)) {
el.classList.add(...classNames); el.classList.add(...classNames);
} else if ( classNames ) { } else if (classNames) {
el.classList.add(classNames); el.classList.add(classNames);
} }
for (const attrName in attributes) { for (const attrName in attributes) {
if (attributes.hasOwnProperty(attrName)) { if (Object.prototype.hasOwnProperty.call(attributes, attrName)) {
el[attrName] = attributes[attrName]; el[attrName] = attributes[attrName];
} }
} }
@ -69,8 +73,10 @@ export default class Dom {
/** /**
* Creates Text Node with the passed content * Creates Text Node with the passed content
* @param {String} content - text content *
* @return {Text} * @param {string} content - text content
*
* @returns {Text}
*/ */
public static text(content: string): Text { public static text(content: string): Text {
return document.createTextNode(content); return document.createTextNode(content);
@ -78,12 +84,14 @@ export default class Dom {
/** /**
* Creates SVG icon linked to the sprite * Creates SVG icon linked to the sprite
*
* @param {string} name - name (id) of icon from sprite * @param {string} name - name (id) of icon from sprite
* @param {number} width * @param {number} [width] - icon width
* @param {number} height * @param {number} [height] - icon height
* @return {SVGElement} *
* @returns {SVGElement}
*/ */
public static svg(name: string, width: number = 14, height: number = 14): SVGElement { public static svg(name: string, width = 14, height = 14): SVGElement {
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
icon.classList.add('icon', 'icon--' + name); icon.classList.add('icon', 'icon--' + name);
@ -97,15 +105,15 @@ export default class Dom {
/** /**
* Append one or several elements to the parent * Append one or several elements to the parent
* *
* @param {Element|DocumentFragment} parent - where to append * @param {Element|DocumentFragment} parent - where to append
* @param {Element|Element[]|Text|Text[]} elements - element or elements list * @param {Element|Element[]|DocumentFragment|Text|Text[]} elements - element or elements list
*/ */
public static append( public static append(
parent: Element|DocumentFragment, parent: Element|DocumentFragment,
elements: Element|Element[]|DocumentFragment|Text|Text[], elements: Element|Element[]|DocumentFragment|Text|Text[]
): void { ): void {
if ( Array.isArray(elements) ) { if (Array.isArray(elements)) {
elements.forEach( (el) => parent.appendChild(el) ); elements.forEach((el) => parent.appendChild(el));
} else { } else {
parent.appendChild(elements); parent.appendChild(elements);
} }
@ -118,9 +126,9 @@ export default class Dom {
* @param {Element|Element[]} elements - element or elements list * @param {Element|Element[]} elements - element or elements list
*/ */
public static prepend(parent: Element, elements: Element|Element[]): void { public static prepend(parent: Element, elements: Element|Element[]): void {
if ( Array.isArray(elements) ) { if (Array.isArray(elements)) {
elements = elements.reverse(); elements = elements.reverse();
elements.forEach( (el) => parent.prepend(el) ); elements.forEach((el) => parent.prepend(el));
} else { } else {
parent.prepend(elements); parent.prepend(elements);
} }
@ -128,6 +136,7 @@ export default class Dom {
/** /**
* Swap two elements in parent * Swap two elements in parent
*
* @param {HTMLElement} el1 - from * @param {HTMLElement} el1 - from
* @param {HTMLElement} el2 - to * @param {HTMLElement} el2 - to
* @deprecated * @deprecated
@ -135,7 +144,7 @@ export default class Dom {
public static swap(el1: HTMLElement, el2: HTMLElement): void { public static swap(el1: HTMLElement, el2: HTMLElement): void {
// create marker element and insert it where el1 is // create marker element and insert it where el1 is
const temp = document.createElement('div'), const temp = document.createElement('div'),
parent = el1.parentNode; parent = el1.parentNode;
parent.insertBefore(temp, el1); parent.insertBefore(temp, el1);
@ -155,7 +164,7 @@ export default class Dom {
* Returns first match * Returns first match
* *
* @param {Element} el - element we searching inside. Default - DOM Document * @param {Element} el - element we searching inside. Default - DOM Document
* @param {String} selector - searching string * @param {string} selector - searching string
* *
* @returns {Element} * @returns {Element}
*/ */
@ -166,10 +175,10 @@ export default class Dom {
/** /**
* Get Element by Id * Get Element by Id
* *
* @param {string} id * @param {string} id - id to find
* @returns {HTMLElement | null} * @returns {HTMLElement | null}
*/ */
public static get(id: string): HTMLElement { public static get(id: string): HTMLElement | null {
return document.getElementById(id); return document.getElementById(id);
} }
@ -178,14 +187,44 @@ export default class Dom {
* *
* Returns all matches * Returns all matches
* *
* @param {Element} el - element we searching inside. Default - DOM Document * @param {Element|Document} el - element we searching inside. Default - DOM Document
* @param {String} selector - searching string * @param {string} selector - searching string
*
* @returns {NodeList} * @returns {NodeList}
*/ */
public static findAll(el: Element|Document = document, selector: string): NodeList { public static findAll(el: Element|Document = document, selector: string): NodeList {
return el.querySelectorAll(selector); return el.querySelectorAll(selector);
} }
/**
* Returns CSS selector for all text inputs
*/
public static get allInputsSelector(): string {
const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];
return '[contenteditable], textarea, input:not([type]), ' +
allowedInputTypes.map((type) => `input[type="${type}"]`).join(', ');
}
/**
* Find all contendeditable, textarea and editable input elements passed holder contains
*
* @param holder - element where to find inputs
*/
public static findAllInputs(holder: Element): HTMLElement[] {
return _.array(holder.querySelectorAll(Dom.allInputsSelector))
/**
* If contenteditable element contains block elements, treat them as inputs.
*/
.reduce((result, input) => {
if (Dom.isNativeInput(input) || Dom.containsOnlyInlineElements(input)) {
return [...result, input];
}
return [...result, ...Dom.getDeepestBlockElements(input)];
}, []);
}
/** /**
* Search for deepest node which is Leaf. * Search for deepest node which is Leaf.
* Leaf is the vertex that doesn't have any child nodes * Leaf is the vertex that doesn't have any child nodes
@ -194,18 +233,20 @@ export default class Dom {
* *
* @param {Node} node - root Node. From this vertex we start Deep-first search * @param {Node} node - root Node. From this vertex we start Deep-first search
* {@link https://en.wikipedia.org/wiki/Depth-first_search} * {@link https://en.wikipedia.org/wiki/Depth-first_search}
* @param {Boolean} atLast - find last text node * @param {boolean} [atLast] - find last text node
* @return {Node} - it can be text Node or Element Node, so that caret will able to work with it *
* @returns {Node} - it can be text Node or Element Node, so that caret will able to work with it
*/ */
public static getDeepestNode(node: Node, atLast: boolean = false): Node { public static getDeepestNode(node: Node, atLast = false): Node {
/** /**
* Current function have two directions: * Current function have two directions:
* - starts from first child and every time gets first or nextSibling in special cases * - starts from first child and every time gets first or nextSibling in special cases
* - starts from last child and gets last or previousSibling * - starts from last child and gets last or previousSibling
*
* @type {string} * @type {string}
*/ */
const child = atLast ? 'lastChild' : 'firstChild', const child = atLast ? 'lastChild' : 'firstChild',
sibling = atLast ? 'previousSibling' : 'nextSibling'; sibling = atLast ? 'previousSibling' : 'nextSibling';
if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) { if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) {
let nodeChild = node[child] as Node; let nodeChild = node[child] as Node;
@ -245,9 +286,11 @@ export default class Dom {
/** /**
* Check if object is DOM node * Check if object is DOM node
* *
* @param {Object} node * @param {*} node - object to check
*
* @returns {boolean} * @returns {boolean}
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static isElement(node: any): node is Element { public static isElement(node: any): node is Element {
return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.ELEMENT_NODE; return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.ELEMENT_NODE;
} }
@ -255,17 +298,20 @@ export default class Dom {
/** /**
* Check if object is DocumentFragmemt node * Check if object is DocumentFragmemt node
* *
* @param {Object} node * @param {object} node - object to check
* @returns {boolean} * @returns {boolean}
*/ */
public static isFragment(node: any): boolean { // eslint-disable-next-line @typescript-eslint/no-explicit-any
public static isFragment(node: any): node is DocumentFragment {
return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE; return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
} }
/** /**
* Check if passed element is contenteditable * Check if passed element is contenteditable
* @param {HTMLElement} element *
* @return {boolean} * @param {HTMLElement} element - html element to check
*
* @returns {boolean}
*/ */
public static isContentEditable(element: HTMLElement): boolean { public static isContentEditable(element: HTMLElement): boolean {
return element.contentEditable === 'true'; return element.contentEditable === 'true';
@ -273,10 +319,13 @@ export default class Dom {
/** /**
* Checks target if it is native input * Checks target if it is native input
* @param {Element|String|Node} target - HTML element or string *
* @return {Boolean} * @param {*} target - HTML element or string
*
* @returns {boolean}
*/ */
public static isNativeInput(target: any): boolean { // eslint-disable-next-line @typescript-eslint/no-explicit-any
public static isNativeInput(target: any): target is HTMLInputElement | HTMLTextAreaElement {
const nativeInputs = [ const nativeInputs = [
'INPUT', 'INPUT',
'TEXTAREA', 'TEXTAREA',
@ -287,14 +336,16 @@ export default class Dom {
/** /**
* Checks if we can set caret * Checks if we can set caret
* @param {HTMLElement} target *
* @return {boolean} * @param {HTMLElement} target - target to check
*
* @returns {boolean}
*/ */
public static canSetCaret(target: HTMLElement): boolean { public static canSetCaret(target: HTMLElement): boolean {
let result = true; let result = true;
if (Dom.isNativeInput(target)) { if (Dom.isNativeInput(target)) {
const inputElement = target as HTMLInputElement; switch (target.type) {
switch (inputElement.type) {
case 'file': case 'file':
case 'checkbox': case 'checkbox':
case 'radio': case 'radio':
@ -309,6 +360,7 @@ export default class Dom {
} else { } else {
result = Dom.isContentEditable(target); result = Dom.isContentEditable(target);
} }
return result; return result;
} }
@ -318,8 +370,9 @@ export default class Dom {
* @description Method checks simple Node without any childs for emptiness * @description Method checks simple Node without any childs for emptiness
* If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method * If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method
* *
* @param {Node} node * @param {Node} node - node to check
* @return {Boolean} true if it is empty *
* @returns {boolean} true if it is empty
*/ */
public static isNodeEmpty(node: Node): boolean { public static isNodeEmpty(node: Node): boolean {
let nodeText; let nodeText;
@ -328,7 +381,7 @@ export default class Dom {
return false; return false;
} }
if ( this.isElement(node) && this.isNativeInput(node) ) { if (this.isElement(node) && this.isNativeInput(node)) {
nodeText = (node as HTMLInputElement).value; nodeText = (node as HTMLInputElement).value;
} else { } else {
nodeText = node.textContent.replace('\u200B', ''); nodeText = node.textContent.replace('\u200B', '');
@ -339,8 +392,10 @@ export default class Dom {
/** /**
* checks node if it is doesn't have any child nodes * checks node if it is doesn't have any child nodes
* @param {Node} node *
* @return {boolean} * @param {Node} node - node to check
*
* @returns {boolean}
*/ */
public static isLeaf(node: Node): boolean { public static isLeaf(node: Node): boolean {
if (!node) { if (!node) {
@ -356,63 +411,42 @@ export default class Dom {
* *
* @description Pushes to stack all DOM leafs and checks for emptiness * @description Pushes to stack all DOM leafs and checks for emptiness
* *
* @param {Node} node * @param {Node} node - node to check
* @return {boolean} * @returns {boolean}
*/ */
public static isEmpty(node: Node): boolean { public static isEmpty(node: Node): boolean {
const treeWalker = [],
leafs = [];
if (!node) {
return true;
}
if (!node.childNodes.length) {
return this.isNodeEmpty(node);
}
/** /**
* Normalize node to merge several text nodes to one to reduce tree walker iterations * Normalize node to merge several text nodes to one to reduce tree walker iterations
*/ */
node.normalize(); node.normalize();
treeWalker.push(node.firstChild); const treeWalker = [ node ];
while ( treeWalker.length > 0 ) { while (treeWalker.length > 0) {
node = treeWalker.shift(); node = treeWalker.shift();
if (!node) { continue; } if (!node) {
continue;
if ( this.isLeaf(node) ) {
leafs.push(node);
} else {
treeWalker.push(node.firstChild);
} }
while ( node && node.nextSibling ) { if (this.isLeaf(node) && !this.isNodeEmpty(node)) {
node = node.nextSibling;
if (!node) { continue; }
treeWalker.push(node);
}
/**
* If one of childs is not empty, checked Node is not empty too
*/
if (node && !this.isNodeEmpty(node)) {
return false; return false;
} }
if (node.childNodes) {
treeWalker.push(...Array.from(node.childNodes));
}
} }
return leafs.every( (leaf) => this.isNodeEmpty(leaf) ); return true;
} }
/** /**
* Check if string contains html elements * Check if string contains html elements
* *
* @param {string} str - string to check
*
* @returns {boolean} * @returns {boolean}
* @param {String} str
*/ */
public static isHTMLString(str: string): boolean { public static isHTMLString(str: string): boolean {
const wrapper = Dom.make('div'); const wrapper = Dom.make('div');
@ -425,7 +459,8 @@ export default class Dom {
/** /**
* Return length of node`s text content * Return length of node`s text content
* *
* @param {Node} node * @param {Node} node - node with content
*
* @returns {number} * @returns {number}
*/ */
public static getContentLength(node: Node): number { public static getContentLength(node: Node): number {
@ -445,7 +480,7 @@ export default class Dom {
* *
* @returns {string[]} * @returns {string[]}
*/ */
static get blockElements(): string[] { public static get blockElements(): string[] {
return [ return [
'address', 'address',
'article', 'article',
@ -491,7 +526,8 @@ export default class Dom {
* Check if passed content includes only inline elements * Check if passed content includes only inline elements
* *
* @param {string|HTMLElement} data - element or html string * @param {string|HTMLElement} data - element or html string
* @return {boolean} *
* @returns {boolean}
*/ */
public static containsOnlyInlineElements(data: string | HTMLElement): boolean { public static containsOnlyInlineElements(data: string | HTMLElement): boolean {
let wrapper: HTMLElement; let wrapper: HTMLElement;
@ -503,9 +539,9 @@ export default class Dom {
wrapper = data; wrapper = data;
} }
const check = (element: HTMLElement) => { const check = (element: HTMLElement): boolean => {
return !Dom.blockElements.includes(element.tagName.toLowerCase()) return !Dom.blockElements.includes(element.tagName.toLowerCase()) &&
&& Array.from(element.children).every(check); Array.from(element.children).every(check);
}; };
return Array.from(wrapper.children).every(check); return Array.from(wrapper.children).every(check);
@ -514,13 +550,13 @@ export default class Dom {
/** /**
* Find and return all block elements in the passed parent (including subtree) * Find and return all block elements in the passed parent (including subtree)
* *
* @param {HTMLElement} parent * @param {HTMLElement} parent - root element
* *
* @return {HTMLElement[]} * @returns {HTMLElement[]}
*/ */
public static getDeepestBlockElements(parent: HTMLElement): HTMLElement[] { public static getDeepestBlockElements(parent: HTMLElement): HTMLElement[] {
if (Dom.containsOnlyInlineElements(parent)) { if (Dom.containsOnlyInlineElements(parent)) {
return [parent]; return [ parent ];
} }
return Array.from(parent.children).reduce((result, element) => { return Array.from(parent.children).reduce((result, element) => {
@ -528,18 +564,27 @@ export default class Dom {
}, []); }, []);
} }
/* /**
* Helper for get holder from {string} or return HTMLElement * Helper for get holder from {string} or return HTMLElement
* @param element *
* @param {string | HTMLElement} element - holder's id or holder's HTML Element
*
* @returns {HTMLElement}
*/ */
public static getHolder(element: string | HTMLElement): HTMLElement { public static getHolder(element: string | HTMLElement): HTMLElement {
if (typeof element === 'string') { return document.getElementById(element); } if (typeof element === 'string') {
return document.getElementById(element);
}
return element; return element;
} }
/** /**
* Method checks passed Node if it is some extension Node * Method checks passed Node if it is some extension Node
*
* @param {Node} node - any node * @param {Node} node - any node
*
* @returns {boolean}
*/ */
public static isExtensionNode(node: Node): boolean { public static isExtensionNode(node: Node): boolean {
const extensions = [ const extensions = [
@ -552,9 +597,11 @@ export default class Dom {
/** /**
* Returns true if element is anchor (is A tag) * Returns true if element is anchor (is A tag)
* *
* @param element * @param {Element} element - element to check
*
* @returns {boolean}
*/ */
public static isAnchor(element: Element): boolean { public static isAnchor(element: Element): element is HTMLAnchorElement {
return element.tagName.toLowerCase() === 'a'; return element.tagName.toLowerCase() === 'a';
} }
} }

View file

@ -1,4 +1,6 @@
import Dom from './dom'; import Dom from './dom';
import * as _ from './utils';
import SelectionUtils from './selection';
/** /**
* Iterator above passed Elements list. * Iterator above passed Elements list.
@ -7,6 +9,7 @@ import Dom from './dom';
export default class DomIterator { export default class DomIterator {
/** /**
* This is a static property that defines iteration directions * This is a static property that defines iteration directions
*
* @type {{RIGHT: string, LEFT: string}} * @type {{RIGHT: string, LEFT: string}}
*/ */
public static directions = { public static directions = {
@ -22,9 +25,10 @@ export default class DomIterator {
/** /**
* Focused button index. * Focused button index.
* Default is -1 which means nothing is active * Default is -1 which means nothing is active
*
* @type {number} * @type {number}
*/ */
private cursor: number = -1; private cursor = -1;
/** /**
* Items to flip * Items to flip
@ -37,7 +41,7 @@ export default class DomIterator {
*/ */
constructor( constructor(
nodeList: HTMLElement[], nodeList: HTMLElement[],
focusedCssClass: string, focusedCssClass: string
) { ) {
this.items = nodeList || []; this.items = nodeList || [];
this.focusedCssClass = focusedCssClass; this.focusedCssClass = focusedCssClass;
@ -45,7 +49,8 @@ export default class DomIterator {
/** /**
* Returns Focused button Node * Returns Focused button Node
* @return {HTMLElement} *
* @returns {HTMLElement}
*/ */
public get currentItem(): HTMLElement { public get currentItem(): HTMLElement {
if (this.cursor === -1) { if (this.cursor === -1) {
@ -57,7 +62,8 @@ export default class DomIterator {
/** /**
* Sets items. Can be used when iterable items changed dynamically * Sets items. Can be used when iterable items changed dynamically
* @param {HTMLElement[]} nodeList *
* @param {HTMLElement[]} nodeList - nodes to iterate
*/ */
public setItems(nodeList: HTMLElement[]): void { public setItems(nodeList: HTMLElement[]): void {
this.items = nodeList; this.items = nodeList;
@ -93,7 +99,7 @@ export default class DomIterator {
* Leafs nodes inside the target list from active element * Leafs nodes inside the target list from active element
* *
* @param {string} direction - leaf direction. Can be 'left' or 'right' * @param {string} direction - leaf direction. Can be 'left' or 'right'
* @return {Number} index of focused node * @returns {number} index of focused node
*/ */
private leafNodesAndReturnIndex(direction: string): number { private leafNodesAndReturnIndex(direction: string): number {
/** /**
@ -138,6 +144,7 @@ export default class DomIterator {
if (direction === DomIterator.directions.RIGHT) { if (direction === DomIterator.directions.RIGHT) {
/** /**
* If we go right then choose next (+1) Tool * If we go right then choose next (+1) Tool
*
* @type {number} * @type {number}
*/ */
focusedButtonIndex = (focusedButtonIndex + 1) % this.items.length; focusedButtonIndex = (focusedButtonIndex + 1) % this.items.length;
@ -145,16 +152,17 @@ export default class DomIterator {
/** /**
* If we go left then choose previous (-1) Tool * If we go left then choose previous (-1) Tool
* Before counting module we need to add length before because of "The JavaScript Modulo Bug" * Before counting module we need to add length before because of "The JavaScript Modulo Bug"
*
* @type {number} * @type {number}
*/ */
focusedButtonIndex = (this.items.length + focusedButtonIndex - 1) % this.items.length; focusedButtonIndex = (this.items.length + focusedButtonIndex - 1) % this.items.length;
} }
if (Dom.isNativeInput(this.items[focusedButtonIndex])) { if (Dom.canSetCaret(this.items[focusedButtonIndex])) {
/** /**
* Focus input * Focus input with micro-delay to ensure DOM is updated
*/ */
this.items[focusedButtonIndex].focus(); _.delay(() => SelectionUtils.setCursor(this.items[focusedButtonIndex]), 50)();
} }
/** /**

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

View file

@ -3,6 +3,8 @@ import * as _ from './utils';
/** /**
* Flipper construction options * Flipper construction options
*
* @interface FlipperOptions
*/ */
export interface FlipperOptions { export interface FlipperOptions {
/** /**
@ -29,28 +31,30 @@ export interface FlipperOptions {
/** /**
* Optional callback for button click * Optional callback for button click
*/ */
activateCallback?: () => void; activateCallback?: (item: HTMLElement) => void;
} }
/** /**
* Flipper is a component that iterates passed items array by TAB or Arrows and clicks it by ENTER * Flipper is a component that iterates passed items array by TAB or Arrows and clicks it by ENTER
*/ */
export default class Flipper { export default class Flipper {
/** /**
* Instance of flipper iterator * Instance of flipper iterator
*
* @type {DomIterator|null} * @type {DomIterator|null}
*/ */
private readonly iterator: DomIterator = null; private readonly iterator: DomIterator = null;
/** /**
* Flag that defines activation status * Flag that defines activation status
*
* @type {boolean} * @type {boolean}
*/ */
private activated: boolean = false; private activated = false;
/** /**
* Flag that allows arrows usage to flip items * Flag that allows arrows usage to flip items
*
* @type {boolean} * @type {boolean}
*/ */
private readonly allowArrows: boolean = true; private readonly allowArrows: boolean = true;
@ -58,62 +62,21 @@ export default class Flipper {
/** /**
* Call back for button click/enter * Call back for button click/enter
*/ */
private readonly activateCallback: () => void; private readonly activateCallback: (item: HTMLElement) => void;
/** /**
* @constructor
*
* @param {FlipperOptions} options - different constructing settings * @param {FlipperOptions} options - different constructing settings
* @
*/ */
constructor(options: FlipperOptions) { constructor(options: FlipperOptions) {
this.allowArrows = typeof options.allowArrows === 'boolean' ? options.allowArrows : true; this.allowArrows = typeof options.allowArrows === 'boolean' ? options.allowArrows : true;
this.iterator = new DomIterator(options.items, options.focusedItemClass); this.iterator = new DomIterator(options.items, options.focusedItemClass);
this.activateCallback = options.activateCallback; this.activateCallback = options.activateCallback;
/**
* Listening all keydowns on document and react on TAB/Enter press
* TAB will leaf iterator items
* ENTER will click the focused item
*/
document.addEventListener('keydown', (event) => {
const isReady = this.isEventReadyForHandling(event);
if (!isReady) {
return;
}
/**
* Prevent only used keys default behaviour
* (allows to navigate by ARROW DOWN, for example)
*/
if (Flipper.usedKeys.includes(event.keyCode)) {
event.preventDefault();
}
switch (event.keyCode) {
case _.keyCodes.TAB:
this.handleTabPress(event);
break;
case _.keyCodes.LEFT:
case _.keyCodes.UP:
this.flipLeft();
break;
case _.keyCodes.RIGHT:
case _.keyCodes.DOWN:
this.flipRight();
break;
case _.keyCodes.ENTER:
this.handleEnterPress(event);
break;
}
}, false);
} }
/** /**
* Array of keys (codes) that is handled by Flipper * Array of keys (codes) that is handled by Flipper
* Used to: * Used to:
* - preventDefault only for this keys, not all keywdowns (@see constructor) * - preventDefault only for this keys, not all keydowns (@see constructor)
* - to skip external behaviours only for these keys, when filler is activated (@see BlockEvents@arrowRightAndDown) * - to skip external behaviours only for these keys, when filler is activated (@see BlockEvents@arrowRightAndDown)
*/ */
public static get usedKeys(): number[] { public static get usedKeys(): number[] {
@ -129,6 +92,7 @@ export default class Flipper {
/** /**
* Active tab/arrows handling by flipper * Active tab/arrows handling by flipper
*
* @param {HTMLElement[]} items - Some modules (like, InlineToolbar, BlockSettings) might refresh buttons dynamically * @param {HTMLElement[]} items - Some modules (like, InlineToolbar, BlockSettings) might refresh buttons dynamically
*/ */
public activate(items?: HTMLElement[]): void { public activate(items?: HTMLElement[]): void {
@ -137,6 +101,13 @@ export default class Flipper {
if (items) { if (items) {
this.iterator.setItems(items); this.iterator.setItems(items);
} }
/**
* Listening all keydowns on document and react on TAB/Enter press
* TAB will leaf iterator items
* ENTER will click the focused item
*/
document.addEventListener('keydown', this.onKeyDown);
} }
/** /**
@ -145,11 +116,14 @@ export default class Flipper {
public deactivate(): void { public deactivate(): void {
this.activated = false; this.activated = false;
this.dropCursor(); this.dropCursor();
document.removeEventListener('keydown', this.onKeyDown);
} }
/** /**
* Return current focused button * Return current focused button
* @return {HTMLElement|null} *
* @returns {HTMLElement|null}
*/ */
public get currentItem(): HTMLElement|null { public get currentItem(): HTMLElement|null {
return this.iterator.currentItem; return this.iterator.currentItem;
@ -163,19 +137,73 @@ export default class Flipper {
this.flipRight(); this.flipRight();
} }
/**
* Focuses previous flipper iterator item
*/
public flipLeft(): void {
this.iterator.previous();
}
/**
* Focuses next flipper iterator item
*/
public flipRight(): void {
this.iterator.next();
}
/** /**
* Drops flipper's iterator cursor * Drops flipper's iterator cursor
*
* @see DomIterator#dropCursor * @see DomIterator#dropCursor
*/ */
private dropCursor(): void { private dropCursor(): void {
this.iterator.dropCursor(); this.iterator.dropCursor();
} }
/**
* KeyDown event handler
*
* @param event - keydown event
*/
private onKeyDown = (event): void => {
const isReady = this.isEventReadyForHandling(event);
if (!isReady) {
return;
}
/**
* Prevent only used keys default behaviour
* (allows to navigate by ARROW DOWN, for example)
*/
if (Flipper.usedKeys.includes(event.keyCode)) {
event.preventDefault();
}
switch (event.keyCode) {
case _.keyCodes.TAB:
this.handleTabPress(event);
break;
case _.keyCodes.LEFT:
case _.keyCodes.UP:
this.flipLeft();
break;
case _.keyCodes.RIGHT:
case _.keyCodes.DOWN:
this.flipRight();
break;
case _.keyCodes.ENTER:
this.handleEnterPress(event);
break;
}
};
/** /**
* This function is fired before handling flipper keycodes * This function is fired before handling flipper keycodes
* The result of this function defines if it is need to be handled or not * The result of this function defines if it is need to be handled or not
* @param {KeyboardEvent} event *
* @return {boolean} * @param {KeyboardEvent} event - keydown keyboard event
* @returns {boolean}
*/ */
private isEventReadyForHandling(event: KeyboardEvent): boolean { private isEventReadyForHandling(event: KeyboardEvent): boolean {
const handlingKeyCodeList = [ const handlingKeyCodeList = [
@ -183,30 +211,29 @@ export default class Flipper {
_.keyCodes.ENTER, _.keyCodes.ENTER,
]; ];
if (this.allowArrows) { const isCurrentItemIsFocusedInput = this.iterator.currentItem == document.activeElement;
if (this.allowArrows && !isCurrentItemIsFocusedInput) {
handlingKeyCodeList.push( handlingKeyCodeList.push(
_.keyCodes.LEFT, _.keyCodes.LEFT,
_.keyCodes.RIGHT, _.keyCodes.RIGHT,
_.keyCodes.UP, _.keyCodes.UP,
_.keyCodes.DOWN, _.keyCodes.DOWN
); );
} }
if (!this.activated || handlingKeyCodeList.indexOf(event.keyCode) === -1) { return this.activated && handlingKeyCodeList.indexOf(event.keyCode) !== -1;
return false;
}
return true;
} }
/** /**
* When flipper is activated tab press will leaf the items * When flipper is activated tab press will leaf the items
* @param {KeyboardEvent} event *
* @param {KeyboardEvent} event - tab keydown event
*/ */
private handleTabPress(event: KeyboardEvent): void { private handleTabPress(event: KeyboardEvent): void {
/** this property defines leaf direction */ /** this property defines leaf direction */
const shiftKey = event.shiftKey, const shiftKey = event.shiftKey,
direction = shiftKey ? DomIterator.directions.LEFT : DomIterator.directions.RIGHT; direction = shiftKey ? DomIterator.directions.LEFT : DomIterator.directions.RIGHT;
switch (direction) { switch (direction) {
case DomIterator.directions.RIGHT: case DomIterator.directions.RIGHT:
@ -218,23 +245,10 @@ export default class Flipper {
} }
} }
/**
* Focuses previous flipper iterator item
*/
private flipLeft(): void {
this.iterator.previous();
}
/**
* Focuses next flipper iterator item
*/
private flipRight(): void {
this.iterator.next();
}
/** /**
* Enter press will click current item if flipper is activated * Enter press will click current item if flipper is activated
* @param {KeyboardEvent} event *
* @param {KeyboardEvent} event - enter keydown event
*/ */
private handleEnterPress(event: KeyboardEvent): void { private handleEnterPress(event: KeyboardEvent): void {
if (!this.activated) { if (!this.activated) {
@ -246,7 +260,7 @@ export default class Flipper {
} }
if (typeof this.activateCallback === 'function') { if (typeof this.activateCallback === 'function') {
this.activateCallback(); this.activateCallback(this.iterator.currentItem);
} }
event.preventDefault(); event.preventDefault();

View file

@ -0,0 +1,90 @@
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';
/**
* Type for all available internal dictionary strings
*/
type DictKeys = LeavesDictKeys<typeof defaultDictionary>;
/**
* This class will responsible for the translation through the language dictionary
*/
export default class I18n {
/**
* Property that stores messages dictionary
*/
private static currentDictionary: I18nDictionary = defaultDictionary;
/**
* Type-safe translation for internal UI texts:
* Perform translation of the string by namespace and a key
*
* @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune')
*
* @param internalNamespace - path to translated string in dictionary
* @param dictKey - dictionary key. Better to use default locale original text
*/
public static ui(internalNamespace: string, dictKey: DictKeys): string {
return I18n._t(internalNamespace, dictKey);
}
/**
* Translate for external strings that is not presented in default dictionary.
* For example, for user-specified tool names
*
* @param namespace - path to translated string in dictionary
* @param dictKey - dictionary key. Better to use default locale original text
*/
public static t(namespace: string, dictKey: string): string {
return I18n._t(namespace, dictKey);
}
/**
* Adjust module for using external dictionary
*
* @param dictionary - new messages list to override default
*/
public static setDictionary(dictionary: I18nDictionary): void {
I18n.currentDictionary = dictionary;
}
/**
* Perform translation both for internal and external namespaces
* If there is no translation found, returns passed key as a translated message
*
* @param namespace - path to translated string in dictionary
* @param dictKey - dictionary key. Better to use default locale original text
*/
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);
}
if (!section || !section[dictKey]) {
return dictKey;
}
return section[dictKey] as string;
}
/**
* Find messages section by namespace path
*
* @param namespace - path to section
*/
private static getNamespace(namespace: string): Dictionary {
const parts = namespace.split('.');
return parts.reduce((section, part) => {
if (!section || !Object.keys(section).length) {
return {};
}
return section[part];
}, I18n.currentDictionary);
}
}

View file

@ -0,0 +1,45 @@
{
"ui": {
"blockTunes": {
"toggler": {
"Click to tune": "",
"or drag to move": ""
}
},
"inlineToolbar": {
"converter": {
"Convert to": ""
}
},
"toolbar": {
"toolbox": {
"Add": ""
}
}
},
"toolNames": {
"Text": "",
"Link": "",
"Bold": "",
"Italic": ""
},
"tools": {
"link": {
"Add a link": ""
},
"stub": {
"The block can not be displayed correctly.": ""
}
},
"blockTunes": {
"delete": {
"Delete": ""
},
"moveUp": {
"Move up": ""
},
"moveDown": {
"Move down": ""
}
}
}

View file

@ -0,0 +1,52 @@
import defaultDictionary from './locales/en/messages.json';
import { DictNamespaces } from '../../types-internal/i18n-internal-namespace';
import { typeOf } from '../utils';
/**
* Evaluate messages dictionary and return object for namespace chaining
*
* @param dict - Messages dictionary
* @param [keyPath] - subsection path (used in recursive call)
*/
function getNamespaces(dict: object, keyPath?: string): DictNamespaces<typeof defaultDictionary> {
const result = {};
Object.entries(dict).forEach(([key, section]) => {
if (typeOf(section) === 'object') {
const newPath = keyPath ? `${keyPath}.${key}` : key;
/**
* Check current section values, if all of them are strings, so there is the last section
*/
const isLastSection = Object.values(section).every((sectionValue) => {
return typeOf(sectionValue) === 'string';
});
/**
* In last section, we substitute namespace path instead of object with translates
*
* ui.toolbar.toolbox "ui.toolbar.toolbox"
* instead of
* ui.toolbar.toolbox {"Add": ""}
*/
if (isLastSection) {
result[key] = newPath;
} else {
result[key] = getNamespaces(section, newPath);
}
return;
}
result[key] = section;
});
return result as DictNamespaces<typeof defaultDictionary>;
}
/**
* Type safe access to the internal messages dictionary sections
*
* @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune');
*/
export const I18nInternalNS = getNamespaces(defaultDictionary);

View file

@ -1,5 +1,5 @@
import $ from '../dom'; import $ from '../dom';
import {API, InlineTool, SanitizerConfig} from '../../../types'; import { InlineTool, SanitizerConfig } from '../../../types';
/** /**
* Bold Tool * Bold Tool
@ -9,25 +9,25 @@ import {API, InlineTool, SanitizerConfig} from '../../../types';
* Makes selected text bolder * Makes selected text bolder
*/ */
export default class BoldInlineTool implements InlineTool { export default class BoldInlineTool implements InlineTool {
/** /**
* Specifies Tool as Inline Toolbar Tool * Specifies Tool as Inline Toolbar Tool
* *
* @return {boolean} * @returns {boolean}
*/ */
public static isInline = true; public static isInline = true;
/** /**
* Title for hover-tooltip * Title for hover-tooltip
*/ */
public static title: string = 'Bold'; public static title = 'Bold';
/** /**
* Sanitizer Rule * Sanitizer Rule
* Leave <b> tags * Leave <b> tags
* @return {object} *
* @returns {object}
*/ */
static get sanitize(): SanitizerConfig { public static get sanitize(): SanitizerConfig {
return { return {
b: {}, b: {},
} as SanitizerConfig; } as SanitizerConfig;
@ -62,12 +62,14 @@ export default class BoldInlineTool implements InlineTool {
this.nodes.button.type = 'button'; this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('bold', 12, 14)); this.nodes.button.appendChild($.svg('bold', 12, 14));
return this.nodes.button; return this.nodes.button;
} }
/** /**
* Wrap range with <b> tag * Wrap range with <b> tag
* @param {Range} range *
* @param {Range} range - range to wrap
*/ */
public surround(range: Range): void { public surround(range: Range): void {
document.execCommand(this.commandName); document.execCommand(this.commandName);
@ -75,17 +77,23 @@ export default class BoldInlineTool implements InlineTool {
/** /**
* Check selection and set activated state to button if there are <b> tag * Check selection and set activated state to button if there are <b> tag
* @param {Selection} selection *
* @param {Selection} selection - selection to check
*
* @returns {boolean}
*/ */
public checkState(selection: Selection): boolean { public checkState(selection: Selection): boolean {
const isActive = document.queryCommandState(this.commandName); const isActive = document.queryCommandState(this.commandName);
this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive); this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);
return isActive; return isActive;
} }
/** /**
* Set a shortcut * Set a shortcut
*
* @returns {boolean}
*/ */
public get shortcut(): string { public get shortcut(): string {
return 'CMD+B'; return 'CMD+B';

View file

@ -1,5 +1,5 @@
import $ from '../dom'; import $ from '../dom';
import {InlineTool, SanitizerConfig} from '../../../types'; import { InlineTool, SanitizerConfig } from '../../../types';
/** /**
* Italic Tool * Italic Tool
@ -9,25 +9,25 @@ import {InlineTool, SanitizerConfig} from '../../../types';
* Style selected text with italic * Style selected text with italic
*/ */
export default class ItalicInlineTool implements InlineTool { export default class ItalicInlineTool implements InlineTool {
/** /**
* Specifies Tool as Inline Toolbar Tool * Specifies Tool as Inline Toolbar Tool
* *
* @return {boolean} * @returns {boolean}
*/ */
public static isInline = true; public static isInline = true;
/** /**
* Title for hover-tooltip * Title for hover-tooltip
*/ */
public static title: string = 'Italic'; public static title = 'Italic';
/** /**
* Sanitizer Rule * Sanitizer Rule
* Leave <i> tags * Leave <i> tags
* @return {object} *
* @returns {object}
*/ */
static get sanitize(): SanitizerConfig { public static get sanitize(): SanitizerConfig {
return { return {
i: {}, i: {},
} as SanitizerConfig; } as SanitizerConfig;
@ -62,12 +62,14 @@ export default class ItalicInlineTool implements InlineTool {
this.nodes.button.type = 'button'; this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('italic', 4, 11)); this.nodes.button.appendChild($.svg('italic', 4, 11));
return this.nodes.button; return this.nodes.button;
} }
/** /**
* Wrap range with <i> tag * Wrap range with <i> tag
* @param {Range} range *
* @param {Range} range - range to wrap
*/ */
public surround(range: Range): void { public surround(range: Range): void {
document.execCommand(this.commandName); document.execCommand(this.commandName);
@ -75,12 +77,14 @@ export default class ItalicInlineTool implements InlineTool {
/** /**
* Check selection and set activated state to button if there are <i> tag * Check selection and set activated state to button if there are <i> tag
* @param {Selection} selection *
* @param {Selection} selection - selection to check
*/ */
public checkState(selection: Selection): boolean { public checkState(selection: Selection): boolean {
const isActive = document.queryCommandState(this.commandName); const isActive = document.queryCommandState(this.commandName);
this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive); this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);
return isActive; return isActive;
} }

View file

@ -2,8 +2,8 @@ import SelectionUtils from '../selection';
import $ from '../dom'; import $ from '../dom';
import * as _ from '../utils'; import * as _ from '../utils';
import {API, InlineTool, SanitizerConfig} from '../../../types'; import { API, InlineTool, SanitizerConfig } from '../../../types';
import {Notifier, Toolbar} from '../../../types/api'; import { Notifier, Toolbar, I18n } from '../../../types/api';
/** /**
* Link Tool * Link Tool
@ -13,25 +13,25 @@ import {Notifier, Toolbar} from '../../../types/api';
* Wrap selected text with <a> tag * Wrap selected text with <a> tag
*/ */
export default class LinkInlineTool implements InlineTool { export default class LinkInlineTool implements InlineTool {
/** /**
* Specifies Tool as Inline Toolbar Tool * Specifies Tool as Inline Toolbar Tool
* *
* @return {boolean} * @returns {boolean}
*/ */
public static isInline = true; public static isInline = true;
/** /**
* Title for hover-tooltip * Title for hover-tooltip
*/ */
public static title: string = 'Link'; public static title = 'Link';
/** /**
* Sanitizer Rule * Sanitizer Rule
* Leave <a> tags * Leave <a> tags
* @return {object} *
* @returns {object}
*/ */
static get sanitize(): SanitizerConfig { public static get sanitize(): SanitizerConfig {
return { return {
a: { a: {
href: true, href: true,
@ -83,7 +83,7 @@ export default class LinkInlineTool implements InlineTool {
/** /**
* Input opening state * Input opening state
*/ */
private inputOpened: boolean = false; private inputOpened = false;
/** /**
* Available Toolbar methods (open/close) * Available Toolbar methods (open/close)
@ -101,12 +101,18 @@ export default class LinkInlineTool implements InlineTool {
private notifier: Notifier; private notifier: Notifier;
/** /**
* @param {{api: API}} - Editor.js API * I18n API
*/ */
constructor({api}) { private i18n: I18n;
/**
* @param {API} api - Editor.js API
*/
constructor({ api }) {
this.toolbar = api.toolbar; this.toolbar = api.toolbar;
this.inlineToolbar = api.inlineToolbar; this.inlineToolbar = api.inlineToolbar;
this.notifier = api.notifier; this.notifier = api.notifier;
this.i18n = api.i18n;
this.selection = new SelectionUtils(); this.selection = new SelectionUtils();
} }
@ -119,6 +125,7 @@ export default class LinkInlineTool implements InlineTool {
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier); this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('link', 14, 10)); this.nodes.button.appendChild($.svg('link', 14, 10));
this.nodes.button.appendChild($.svg('unlink', 15, 11)); this.nodes.button.appendChild($.svg('unlink', 15, 11));
return this.nodes.button; return this.nodes.button;
} }
@ -127,19 +134,21 @@ export default class LinkInlineTool implements InlineTool {
*/ */
public renderActions(): HTMLElement { public renderActions(): HTMLElement {
this.nodes.input = document.createElement('input') as HTMLInputElement; this.nodes.input = document.createElement('input') as HTMLInputElement;
this.nodes.input.placeholder = 'Add a link'; this.nodes.input.placeholder = this.i18n.t('Add a link');
this.nodes.input.classList.add(this.CSS.input); this.nodes.input.classList.add(this.CSS.input);
this.nodes.input.addEventListener('keydown', (event: KeyboardEvent) => { this.nodes.input.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.keyCode === this.ENTER_KEY) { if (event.keyCode === this.ENTER_KEY) {
this.enterPressed(event); this.enterPressed(event);
} }
}); });
return this.nodes.input; return this.nodes.input;
} }
/** /**
* Handle clicks on the Inline Toolbar icon * Handle clicks on the Inline Toolbar icon
* @param {Range} range *
* @param {Range} range - range to wrap with link
*/ */
public surround(range: Range): void { public surround(range: Range): void {
/** /**
@ -168,6 +177,7 @@ export default class LinkInlineTool implements InlineTool {
this.closeActions(); this.closeActions();
this.checkState(); this.checkState();
this.toolbar.close(); this.toolbar.close();
return; return;
} }
} }
@ -177,7 +187,8 @@ export default class LinkInlineTool implements InlineTool {
/** /**
* Check selection and set activated state to button if there are <a> tag * Check selection and set activated state to button if there are <a> tag
* @param {Selection} selection *
* @param {Selection} selection - selection to check
*/ */
public checkState(selection?: Selection): boolean { public checkState(selection?: Selection): boolean {
const anchorTag = this.selection.findParentTag('A'); const anchorTag = this.selection.findParentTag('A');
@ -191,6 +202,7 @@ export default class LinkInlineTool implements InlineTool {
* Fill input value with link href * Fill input value with link href
*/ */
const hrefAttr = anchorTag.getAttribute('href'); const hrefAttr = anchorTag.getAttribute('href');
this.nodes.input.value = hrefAttr !== 'null' ? hrefAttr : ''; this.nodes.input.value = hrefAttr !== 'null' ? hrefAttr : '';
this.selection.save(); this.selection.save();
@ -216,6 +228,9 @@ export default class LinkInlineTool implements InlineTool {
return 'CMD+K'; return 'CMD+K';
} }
/**
* Show/close link input
*/
private toggleActions(): void { private toggleActions(): void {
if (!this.inputOpened) { if (!this.inputOpened) {
this.openActions(true); this.openActions(true);
@ -227,7 +242,7 @@ export default class LinkInlineTool implements InlineTool {
/** /**
* @param {boolean} needFocus - on link creation we need to focus input. On editing - nope. * @param {boolean} needFocus - on link creation we need to focus input. On editing - nope.
*/ */
private openActions(needFocus: boolean = false): void { private openActions(needFocus = false): void {
this.nodes.input.classList.add(this.CSS.inputShowed); this.nodes.input.classList.add(this.CSS.inputShowed);
if (needFocus) { if (needFocus) {
this.nodes.input.focus(); this.nodes.input.focus();
@ -237,13 +252,15 @@ export default class LinkInlineTool implements InlineTool {
/** /**
* Close input * Close input
*
* @param {boolean} clearSavedSelection we don't need to clear saved selection * @param {boolean} clearSavedSelection we don't need to clear saved selection
* on toggle-clicks on the icon of opened Toolbar * on toggle-clicks on the icon of opened Toolbar
*/ */
private closeActions(clearSavedSelection: boolean = true): void { private closeActions(clearSavedSelection = true): void {
if (this.selection.isFakeBackgroundEnabled) { if (this.selection.isFakeBackgroundEnabled) {
// if actions is broken by other selection We need to save new selection // if actions is broken by other selection We need to save new selection
const currentSelection = new SelectionUtils(); const currentSelection = new SelectionUtils();
currentSelection.save(); currentSelection.save();
this.selection.restore(); this.selection.restore();
@ -263,7 +280,8 @@ export default class LinkInlineTool implements InlineTool {
/** /**
* Enter pressed on input * Enter pressed on input
* @param {KeyboardEvent} event *
* @param {KeyboardEvent} event - enter keydown event
*/ */
private enterPressed(event: KeyboardEvent): void { private enterPressed(event: KeyboardEvent): void {
let value = this.nodes.input.value || ''; let value = this.nodes.input.value || '';
@ -276,13 +294,13 @@ export default class LinkInlineTool implements InlineTool {
} }
if (!this.validateURL(value)) { if (!this.validateURL(value)) {
this.notifier.show({ this.notifier.show({
message: 'Pasted link is not valid.', message: 'Pasted link is not valid.',
style: 'error', style: 'error',
}); });
_.log('Incorrect Link pasted', 'warn', value); _.log('Incorrect Link pasted', 'warn', value);
return; return;
} }
@ -305,8 +323,9 @@ export default class LinkInlineTool implements InlineTool {
/** /**
* Detects if passed string is URL * Detects if passed string is URL
* @param {string} str *
* @return {Boolean} * @param {string} str - string to validate
* @returns {boolean}
*/ */
private validateURL(str: string): boolean { private validateURL(str: string): boolean {
/** /**
@ -319,17 +338,20 @@ export default class LinkInlineTool implements InlineTool {
* Process link before injection * Process link before injection
* - sanitize * - sanitize
* - add protocol for links like 'google.com' * - add protocol for links like 'google.com'
*
* @param {string} link - raw user input * @param {string} link - raw user input
*/ */
private prepareLink(link: string): string { private prepareLink(link: string): string {
link = link.trim(); link = link.trim();
link = this.addProtocol(link); link = this.addProtocol(link);
return link; return link;
} }
/** /**
* Add 'http' protocol to the links like 'vc.ru', 'google.com' * Add 'http' protocol to the links like 'vc.ru', 'google.com'
* @param {String} link *
* @param {string} link - string to process
*/ */
private addProtocol(link: string): string { private addProtocol(link: string): string {
/** /**
@ -345,9 +367,9 @@ export default class LinkInlineTool implements InlineTool {
* 2) Anchors looks like "#results" * 2) Anchors looks like "#results"
* 3) Protocol-relative URLs like "//google.com" * 3) Protocol-relative URLs like "//google.com"
*/ */
const isInternal = /^\/[^\/\s]/.test(link), const isInternal = /^\/[^/\s]/.test(link),
isAnchor = link.substring(0, 1) === '#', isAnchor = link.substring(0, 1) === '#',
isProtocolRelative = /^\/\/[^\/\s]/.test(link); isProtocolRelative = /^\/\/[^/\s]/.test(link);
if (!isInternal && !isAnchor && !isProtocolRelative) { if (!isInternal && !isAnchor && !isProtocolRelative) {
link = 'http://' + link; link = 'http://' + link;
@ -358,10 +380,10 @@ export default class LinkInlineTool implements InlineTool {
/** /**
* Inserts <a> tag with "href" * Inserts <a> tag with "href"
*
* @param {string} link - "href" value * @param {string} link - "href" value
*/ */
private insertLink(link: string): void { private insertLink(link: string): void {
/** /**
* Edit all link, not selected part * Edit all link, not selected part
*/ */

View file

@ -1,8 +1,9 @@
import Module from '../../__module'; import Module from '../../__module';
import {Blocks} from '../../../../types/api'; import { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';
import {BlockToolData, OutputData, ToolConfig} from '../../../../types'; import { BlockToolData, OutputData, ToolConfig } from '../../../../types';
import * as _ from './../../utils'; import * as _ from './../../utils';
import BlockAPI from '../../block/api';
/** /**
* @class BlocksAPI * @class BlocksAPI
@ -11,28 +12,30 @@ import * as _ from './../../utils';
export default class BlocksAPI extends Module { export default class BlocksAPI extends Module {
/** /**
* Available methods * Available methods
* @return {Blocks} *
* @returns {Blocks}
*/ */
get methods(): Blocks { public get methods(): Blocks {
return { return {
clear: () => this.clear(), clear: (): void => this.clear(),
render: (data: OutputData) => this.render(data), render: (data: OutputData): Promise<void> => this.render(data),
renderFromHTML: (data: string) => this.renderFromHTML(data), renderFromHTML: (data: string): Promise<void> => this.renderFromHTML(data),
delete: () => this.delete(), delete: (index?: number): void => this.delete(index),
swap: (fromIndex: number, toIndex: number) => this.swap(fromIndex, toIndex), swap: (fromIndex: number, toIndex: number): void => this.swap(fromIndex, toIndex),
move: (toIndex: number, fromIndex?: number) => this.move(toIndex, fromIndex), move: (toIndex: number, fromIndex?: number): void => this.move(toIndex, fromIndex),
getBlockByIndex: (index: number) => this.getBlockByIndex(index), getBlockByIndex: (index: number): BlockAPIInterface => this.getBlockByIndex(index),
getCurrentBlockIndex: () => this.getCurrentBlockIndex(), getCurrentBlockIndex: (): number => this.getCurrentBlockIndex(),
getBlocksCount: () => this.getBlocksCount(), getBlocksCount: (): number => this.getBlocksCount(),
stretchBlock: (index: number, status: boolean = true) => this.stretchBlock(index, status), stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status),
insertNewBlock: () => this.insertNewBlock(), insertNewBlock: (): void => this.insertNewBlock(),
insert: this.insert, insert: this.insert,
}; };
} }
/** /**
* Returns Blocks count * Returns Blocks count
* @return {number} *
* @returns {number}
*/ */
public getBlocksCount(): number { public getBlocksCount(): number {
return this.Editor.BlockManager.blocks.length; return this.Editor.BlockManager.blocks.length;
@ -40,7 +43,8 @@ export default class BlocksAPI extends Module {
/** /**
* Returns current block index * Returns current block index
* @return {number} *
* @returns {number}
*/ */
public getCurrentBlockIndex(): number { public getCurrentBlockIndex(): number {
return this.Editor.BlockManager.currentBlockIndex; return this.Editor.BlockManager.currentBlockIndex;
@ -48,22 +52,31 @@ export default class BlocksAPI extends Module {
/** /**
* Returns Block holder by Block index * Returns Block holder by Block index
* @param {Number} index
* *
* @return {HTMLElement} * @param {number} index - index to get
*
* @returns {HTMLElement}
*/ */
public getBlockByIndex(index: number): HTMLElement { public getBlockByIndex(index: number): BlockAPIInterface {
const block = this.Editor.BlockManager.getBlockByIndex(index); const block = this.Editor.BlockManager.getBlockByIndex(index);
return block.holder;
return new BlockAPI(block);
} }
/** /**
* Call Block Manager method that swap Blocks * Call Block Manager method that swap Blocks
*
* @param {number} fromIndex - position of first Block * @param {number} fromIndex - position of first Block
* @param {number} toIndex - position of second Block * @param {number} toIndex - position of second Block
* @deprecated use 'move' instead * @deprecated use 'move' instead
*/ */
public swap(fromIndex: number, toIndex: number): void { public swap(fromIndex: number, toIndex: number): void {
_.log(
'`blocks.swap()` method is deprecated and will be removed in the next major release. ' +
'Use `block.move()` method instead',
'info'
);
this.Editor.BlockManager.swap(fromIndex, toIndex); this.Editor.BlockManager.swap(fromIndex, toIndex);
/** /**
@ -75,8 +88,9 @@ export default class BlocksAPI extends Module {
/** /**
* Move block from one index to another * Move block from one index to another
* @param {Number} toIndex *
* @param {number} fromIndex * @param {number} toIndex - index to move to
* @param {number} fromIndex - index to move from
*/ */
public move(toIndex: number, fromIndex?: number): void { public move(toIndex: number, fromIndex?: number): void {
this.Editor.BlockManager.move(toIndex, fromIndex); this.Editor.BlockManager.move(toIndex, fromIndex);
@ -90,10 +104,17 @@ export default class BlocksAPI extends Module {
/** /**
* Deletes Block * Deletes Block
* @param blockIndex *
* @param {number} blockIndex - index of Block to delete
*/ */
public delete(blockIndex?: number): void { public delete(blockIndex?: number): void {
this.Editor.BlockManager.removeBlock(blockIndex); try {
this.Editor.BlockManager.removeBlock(blockIndex);
} catch (e) {
_.logLabeled(e, 'warn');
return;
}
/** /**
* in case of last block deletion * in case of last block deletion
@ -121,29 +142,42 @@ export default class BlocksAPI extends Module {
/** /**
* Fills Editor with Blocks data * Fills Editor with Blocks data
*
* @param {OutputData} data Saved Editor data * @param {OutputData} data Saved Editor data
*/ */
public render(data: OutputData): Promise<void> { public render(data: OutputData): Promise<void> {
this.Editor.BlockManager.clear(); this.Editor.BlockManager.clear();
return this.Editor.Renderer.render(data.blocks); return this.Editor.Renderer.render(data.blocks);
} }
/** /**
* Render passed HTML string * Render passed HTML string
* @param {string} data *
* @return {Promise<void>} * @param {string} data - HTML string to render
* @returns {Promise<void>}
*/ */
public renderFromHTML(data: string): Promise<void> { public renderFromHTML(data: string): Promise<void> {
this.Editor.BlockManager.clear(); this.Editor.BlockManager.clear();
return this.Editor.Paste.processText(data, true); return this.Editor.Paste.processText(data, true);
} }
/** /**
* Stretch Block's content * Stretch Block's content
* @param {number} index *
* @param {number} index - index of Block to stretch
* @param {boolean} status - true to enable, false to disable * @param {boolean} status - true to enable, false to disable
*
* @deprecated Use BlockAPI interface to stretch Blocks
*/ */
public stretchBlock(index: number, status: boolean = true): void { 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'
);
const block = this.Editor.BlockManager.getBlockByIndex(index); const block = this.Editor.BlockManager.getBlockByIndex(index);
if (!block) { if (!block) {
@ -167,27 +201,26 @@ export default class BlocksAPI extends Module {
data: BlockToolData = {}, data: BlockToolData = {},
config: ToolConfig = {}, config: ToolConfig = {},
index?: number, index?: number,
needToFocus?: boolean, needToFocus?: boolean
): void => { ): void => {
this.Editor.BlockManager.insert( this.Editor.BlockManager.insert({
type, tool: type,
data, data,
config,
index, index,
needToFocus, needToFocus,
); });
} }
/** /**
* Insert new Block * Insert new Block
* After set caret to this Block * After set caret to this Block
* *
* @todo: remove in 3.0.0 * @todo remove in 3.0.0
* *
* @deprecated with insert() method * @deprecated with insert() method
*/ */
public insertNewBlock(): void { public insertNewBlock(): void {
_.log('Method blocks.insertNewBlock() is deprecated and it will be removed in next major release. ' + _.log('Method blocks.insertNewBlock() is deprecated and it will be removed in the next major release. ' +
'Use blocks.insert() instead.', 'warn'); 'Use blocks.insert() instead.', 'warn');
this.insert(); this.insert();
} }

View file

@ -1,5 +1,5 @@
import Module from '../../__module'; import Module from '../../__module';
import {Caret} from '../../../../types/api'; import { Caret } from '../../../../types/api';
/** /**
* @class CaretAPI * @class CaretAPI
@ -8,9 +8,10 @@ import {Caret} from '../../../../types/api';
export default class CaretAPI extends Module { export default class CaretAPI extends Module {
/** /**
* Available methods * Available methods
* @return {Caret} *
* @returns {Caret}
*/ */
get methods(): Caret { public get methods(): Caret {
return { return {
setToFirstBlock: this.setToFirstBlock, setToFirstBlock: this.setToFirstBlock,
setToLastBlock: this.setToLastBlock, setToLastBlock: this.setToLastBlock,
@ -27,14 +28,15 @@ export default class CaretAPI extends Module {
* @param {string} position - position where to set caret * @param {string} position - position where to set caret
* @param {number} offset - caret offset * @param {number} offset - caret offset
* *
* @return {boolean} * @returns {boolean}
*/ */
private setToFirstBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset: number = 0): boolean => { private setToFirstBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset = 0): boolean => {
if (!this.Editor.BlockManager.firstBlock) { if (!this.Editor.BlockManager.firstBlock) {
return false; return false;
} }
this.Editor.Caret.setToBlock(this.Editor.BlockManager.firstBlock, position, offset); this.Editor.Caret.setToBlock(this.Editor.BlockManager.firstBlock, position, offset);
return true; return true;
} }
@ -44,14 +46,15 @@ export default class CaretAPI extends Module {
* @param {string} position - position where to set caret * @param {string} position - position where to set caret
* @param {number} offset - caret offset * @param {number} offset - caret offset
* *
* @return {boolean} * @returns {boolean}
*/ */
private setToLastBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset: number = 0): boolean => { private setToLastBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset = 0): boolean => {
if (!this.Editor.BlockManager.lastBlock) { if (!this.Editor.BlockManager.lastBlock) {
return false; return false;
} }
this.Editor.Caret.setToBlock(this.Editor.BlockManager.lastBlock, position, offset); this.Editor.Caret.setToBlock(this.Editor.BlockManager.lastBlock, position, offset);
return true; return true;
} }
@ -61,17 +64,18 @@ export default class CaretAPI extends Module {
* @param {string} position - position where to set caret * @param {string} position - position where to set caret
* @param {number} offset - caret offset * @param {number} offset - caret offset
* *
* @return {boolean} * @returns {boolean}
*/ */
private setToPreviousBlock = ( private setToPreviousBlock = (
position: string = this.Editor.Caret.positions.DEFAULT, position: string = this.Editor.Caret.positions.DEFAULT,
offset: number = 0, offset = 0
): boolean => { ): boolean => {
if (!this.Editor.BlockManager.previousBlock) { if (!this.Editor.BlockManager.previousBlock) {
return false; return false;
} }
this.Editor.Caret.setToBlock(this.Editor.BlockManager.previousBlock, position, offset); this.Editor.Caret.setToBlock(this.Editor.BlockManager.previousBlock, position, offset);
return true; return true;
} }
@ -81,14 +85,15 @@ export default class CaretAPI extends Module {
* @param {string} position - position where to set caret * @param {string} position - position where to set caret
* @param {number} offset - caret offset * @param {number} offset - caret offset
* *
* @return {boolean} * @returns {boolean}
*/ */
private setToNextBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset: number = 0): boolean => { private setToNextBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset = 0): boolean => {
if (!this.Editor.BlockManager.nextBlock) { if (!this.Editor.BlockManager.nextBlock) {
return false; return false;
} }
this.Editor.Caret.setToBlock(this.Editor.BlockManager.nextBlock, position, offset); this.Editor.Caret.setToBlock(this.Editor.BlockManager.nextBlock, position, offset);
return true; return true;
} }
@ -99,18 +104,19 @@ export default class CaretAPI extends Module {
* @param {string} position - position where to set caret * @param {string} position - position where to set caret
* @param {number} offset - caret offset * @param {number} offset - caret offset
* *
* @return {boolean} * @returns {boolean}
*/ */
private setToBlock = ( private setToBlock = (
index: number, index: number,
position: string = this.Editor.Caret.positions.DEFAULT, position: string = this.Editor.Caret.positions.DEFAULT,
offset: number = 0, offset = 0
): boolean => { ): boolean => {
if (!this.Editor.BlockManager.blocks[index]) { if (!this.Editor.BlockManager.blocks[index]) {
return false; return false;
} }
this.Editor.Caret.setToBlock(this.Editor.BlockManager.blocks[index], position, offset); this.Editor.Caret.setToBlock(this.Editor.BlockManager.blocks[index], position, offset);
return true; return true;
} }
@ -119,9 +125,9 @@ export default class CaretAPI extends Module {
* *
* @param {boolean} atEnd - if true, set Caret to the end of the Editor * @param {boolean} atEnd - if true, set Caret to the end of the Editor
* *
* @return {boolean} * @returns {boolean}
*/ */
private focus = (atEnd: boolean = false) => { private focus = (atEnd = false): boolean => {
if (atEnd) { if (atEnd) {
return this.setToLastBlock(this.Editor.Caret.positions.END); return this.setToLastBlock(this.Editor.Caret.positions.END);
} }

View file

@ -1,5 +1,5 @@
import Module from '../../__module'; import Module from '../../__module';
import {Events} from '../../../../types/api'; import { Events } from '../../../../types/api';
/** /**
* @class EventsAPI * @class EventsAPI
@ -8,20 +8,22 @@ import {Events} from '../../../../types/api';
export default class EventsAPI extends Module { export default class EventsAPI extends Module {
/** /**
* Available methods * Available methods
* @return {Events} *
* @returns {Events}
*/ */
get methods(): Events { public get methods(): Events {
return { return {
emit: (eventName: string, data: object) => this.emit(eventName, data), emit: (eventName: string, data: object): void => this.emit(eventName, data),
off: (eventName: string, callback: () => void) => this.off(eventName, callback), off: (eventName: string, callback: () => void): void => this.off(eventName, callback),
on: (eventName: string, callback: () => void) => this.on(eventName, callback), on: (eventName: string, callback: () => void): void => this.on(eventName, callback),
}; };
} }
/** /**
* Subscribe on Events * Subscribe on Events
* @param {String} eventName *
* @param {Function} callback * @param {string} eventName - event name to subscribe
* @param {Function} callback - event handler
*/ */
public on(eventName, callback): void { public on(eventName, callback): void {
this.Editor.Events.on(eventName, callback); this.Editor.Events.on(eventName, callback);
@ -29,8 +31,9 @@ export default class EventsAPI extends Module {
/** /**
* Emit event with data * Emit event with data
* @param {String} eventName *
* @param {Object} data * @param {string} eventName - event to emit
* @param {object} data - event's data
*/ */
public emit(eventName, data): void { public emit(eventName, data): void {
this.Editor.Events.emit(eventName, data); this.Editor.Events.emit(eventName, data);
@ -38,11 +41,11 @@ export default class EventsAPI extends Module {
/** /**
* Unsubscribe from Event * Unsubscribe from Event
* @param {String} eventName *
* @param {Function} callback * @param {string} eventName - event to unsubscribe
* @param {Function} callback - event handler
*/ */
public off(eventName, callback): void { public off(eventName, callback): void {
this.Editor.Events.off(eventName, callback); this.Editor.Events.off(eventName, callback);
} }
} }

View file

@ -0,0 +1,55 @@
import Module from '../../__module';
import { I18n } from '../../../../types/api';
import I18nInternal from '../../i18n';
import { ToolType } from '../tools';
import { logLabeled } from '../../utils';
/**
* Provides methods for working with i18n
*/
export default class I18nAPI extends Module {
/**
* Return namespace section for tool or block tune
*
* @param toolName - name of tool. Used to provide dictionary only for this tool
* @param toolType - 'block' for Block Tool, 'inline' for Inline Tool, 'tune' for Block Tunes
*/
private static getNamespace(toolName: string, toolType: ToolType): string {
switch (toolType) {
case ToolType.Block:
case ToolType.Inline:
return `tools.${toolName}`;
case ToolType.Tune:
return `blockTunes.${toolName}`;
}
}
/**
* Return I18n API methods with global dictionary access
*/
public get methods(): I18n {
return {
t: (): string | undefined => {
logLabeled('I18n.t() method can be accessed only from Tools', 'warn');
return undefined;
},
};
}
/**
* Return I18n API methods with tool namespaced dictionary
*
* @param toolName - name of tool. Used to provide dictionary only for this tool
* @param toolType - 'block' for Block Tool, 'inline' for Inline Tool, 'tune' for Block Tunes
*/
public getMethodsForTool(toolName: string, toolType: ToolType): I18n {
return Object.assign(
this.methods,
{
t: (dictKey: string): string => {
return I18nInternal.t(I18nAPI.getNamespace(toolName, toolType), dictKey);
},
});
}
}

View file

@ -1,17 +1,21 @@
/** /**
* @module API * @module API
* @copyright <CodeX Team> 2018 * @copyright <CodeX> 2018
* *
* Each block has an Editor API instance to use provided public methods * Each block has an Editor API instance to use provided public methods
* if you cant to read more about how API works, please see docs * if you cant to read more about how API works, please see docs
*/ */
import Module from '../../__module'; import Module from '../../__module';
import {API as APIInterfaces} from '../../../../types'; import { API as APIInterfaces } from '../../../../types';
import { ToolType } from '../tools';
/** /**
* @class API * @class API
*/ */
export default class API extends Module { export default class API extends Module {
/**
* Editor.js Core API modules
*/
public get methods(): APIInterfaces { public get methods(): APIInterfaces {
return { return {
blocks: this.Editor.BlocksAPI.methods, blocks: this.Editor.BlocksAPI.methods,
@ -26,6 +30,24 @@ export default class API extends Module {
toolbar: this.Editor.ToolbarAPI.methods, toolbar: this.Editor.ToolbarAPI.methods,
inlineToolbar: this.Editor.InlineToolbarAPI.methods, inlineToolbar: this.Editor.InlineToolbarAPI.methods,
tooltip: this.Editor.TooltipAPI.methods, tooltip: this.Editor.TooltipAPI.methods,
i18n: this.Editor.I18nAPI.methods,
} as APIInterfaces; } as APIInterfaces;
} }
/**
* Returns Editor.js Core API methods for passed tool
*
* @param toolName - how user name tool. It can be used in some API logic,
* for example in i18n to provide namespaced dictionary
*
* @param toolType - 'block' for Block Tool, 'inline' for Inline Tool, 'tune' for Block Tunes
*/
public getMethodsForTool(toolName: string, toolType = ToolType.Block): APIInterfaces {
return Object.assign(
this.methods,
{
i18n: this.Editor.I18nAPI.getMethodsForTool(toolName, toolType),
}
) as APIInterfaces;
}
} }

View file

@ -8,12 +8,13 @@ import { InlineToolbar } from '../../../../types/api/inline-toolbar';
export default class InlineToolbarAPI extends Module { export default class InlineToolbarAPI extends Module {
/** /**
* Available methods * Available methods
* @return {InlineToolbar} *
* @returns {InlineToolbar}
*/ */
get methods(): InlineToolbar { public get methods(): InlineToolbar {
return { return {
close: () => this.close(), close: (): void => this.close(),
open: () => this.open(), open: (): void => this.open(),
}; };
} }

View file

@ -1,5 +1,5 @@
import Module from '../../__module'; import Module from '../../__module';
import {Listeners} from '../../../../types/api'; import { Listeners } from '../../../../types/api';
/** /**
* @class ListenersAPI * @class ListenersAPI
@ -8,22 +8,23 @@ import {Listeners} from '../../../../types/api';
export default class ListenersAPI extends Module { export default class ListenersAPI extends Module {
/** /**
* Available methods * Available methods
* @return {Listeners} *
* @returns {Listeners}
*/ */
get methods(): Listeners { public get methods(): Listeners {
return { return {
on: (element: HTMLElement, eventType, handler, useCapture) => this.on(element, eventType, handler, useCapture), on: (element: HTMLElement, eventType, handler, useCapture): void => this.on(element, eventType, handler, useCapture),
off: (element, eventType, handler) => this.off(element, eventType, handler), off: (element, eventType, handler, useCapture): void => this.off(element, eventType, handler, useCapture),
}; };
} }
/** /**
* adds DOM event listener * adds DOM event listener
* *
* @param {HTMLElement} element * @param {HTMLElement} element - Element to set handler to
* @param {string} eventType * @param {string} eventType - event type
* @param {() => void} handler * @param {() => void} handler - event handler
* @param {boolean} useCapture * @param {boolean} useCapture - capture event or not
*/ */
public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void { public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): void {
this.Editor.Listeners.on(element, eventType, handler, useCapture); this.Editor.Listeners.on(element, eventType, handler, useCapture);
@ -32,11 +33,12 @@ export default class ListenersAPI extends Module {
/** /**
* Removes DOM listener from element * Removes DOM listener from element
* *
* @param element * @param {Element} element - Element to remove handler from
* @param eventType * @param eventType - event type
* @param handler * @param handler - event handler
* @param {boolean} useCapture - capture event or not
*/ */
public off(element, eventType, handler): void { public off(element: Element, eventType: string, handler: () => void, useCapture?: boolean): void {
this.Editor.Listeners.off(element, eventType, handler); this.Editor.Listeners.off(element, eventType, handler, useCapture);
} }
} }

View file

@ -1,19 +1,26 @@
import Module from '../../__module'; import Module from '../../__module';
import {Notifier} from '../../../../types/api'; import { Notifier } from '../../../../types/api';
import {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier'; import { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier';
/**
*
*/
export default class NotifierAPI extends Module { export default class NotifierAPI extends Module {
/** /**
* Available methods * Available methods
*/ */
get methods(): Notifier { public get methods(): Notifier {
return { return {
show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => this.show(options), show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void => this.show(options),
}; };
} }
public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) { /**
* Show notification
*
* @param {NotifierOptions} options - message option
*/
public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void {
return this.Editor.Notifier.show(options); return this.Editor.Notifier.show(options);
} }
} }

View file

@ -1,5 +1,6 @@
import Module from '../../__module'; import Module from '../../__module';
import {Sanitizer} from '../../../../types/api'; import { Sanitizer } from '../../../../types/api';
import { SanitizerConfig } from '../../../../types/configs';
/** /**
* @class SanitizerAPI * @class SanitizerAPI
@ -8,16 +9,24 @@ import {Sanitizer} from '../../../../types/api';
export default class SanitizerAPI extends Module { export default class SanitizerAPI extends Module {
/** /**
* Available methods * Available methods
* @return {Sanitizer} *
* @returns {Sanitizer}
*/ */
get methods(): Sanitizer { public get methods(): Sanitizer {
return { return {
clean: (taintString, config) => this.clean(taintString, config), clean: (taintString, config): string => this.clean(taintString, config),
}; };
} }
public clean(taintString, config) { /**
* Perform sanitizing of a string
*
* @param {string} taintString - what to sanitize
* @param {SanitizerConfig} config - sanitizer config
*
* @returns {string}
*/
public clean(taintString: string, config: SanitizerConfig): string {
return this.Editor.Sanitizer.clean(taintString, config); return this.Editor.Sanitizer.clean(taintString, config);
} }
} }

View file

@ -1,6 +1,6 @@
import Module from '../../__module'; import Module from '../../__module';
import {Saver} from '../../../../types/api'; import { Saver } from '../../../../types/api';
import {OutputData} from '../../../../types'; import { OutputData } from '../../../../types';
/** /**
* @class SaverAPI * @class SaverAPI
@ -9,11 +9,12 @@ import {OutputData} from '../../../../types';
export default class SaverAPI extends Module { export default class SaverAPI extends Module {
/** /**
* Available methods * Available methods
* @return {Saver} *
* @returns {Saver}
*/ */
get methods(): Saver { public get methods(): Saver {
return { return {
save: () => this.save(), save: (): Promise<OutputData> => this.save(),
}; };
} }

View file

@ -1,6 +1,6 @@
import Module from '../../__module'; import Module from '../../__module';
import SelectionUtils from '../../selection'; import SelectionUtils from '../../selection';
import {Selection as SelectionAPIInterface} from '../../../../types/api'; import { Selection as SelectionAPIInterface } from '../../../../types/api';
/** /**
* @class SelectionAPI * @class SelectionAPI
@ -9,31 +9,34 @@ import {Selection as SelectionAPIInterface} from '../../../../types/api';
export default class SelectionAPI extends Module { export default class SelectionAPI extends Module {
/** /**
* Available methods * Available methods
* @return {SelectionAPIInterface} *
* @returns {SelectionAPIInterface}
*/ */
get methods(): SelectionAPIInterface { public get methods(): SelectionAPIInterface {
return { return {
findParentTag: (tagName: string, className?: string) => this.findParentTag(tagName, className), findParentTag: (tagName: string, className?: string): HTMLElement | null => this.findParentTag(tagName, className),
expandToTag: (node: HTMLElement) => this.expandToTag(node), expandToTag: (node: HTMLElement): void => this.expandToTag(node),
}; };
} }
/** /**
* Looks ahead from selection and find passed tag with class name * Looks ahead from selection and find passed tag with class name
*
* @param {string} tagName - tag to find * @param {string} tagName - tag to find
* @param {string} className - tag's class name * @param {string} className - tag's class name
* @return {HTMLElement|null} *
* @returns {HTMLElement|null}
*/ */
public findParentTag(tagName: string, className?: string): HTMLElement|null { public findParentTag(tagName: string, className?: string): HTMLElement | null {
return new SelectionUtils().findParentTag(tagName, className); return new SelectionUtils().findParentTag(tagName, className);
} }
/** /**
* Expand selection to passed tag * Expand selection to passed tag
*
* @param {HTMLElement} node - tag that should contain selection * @param {HTMLElement} node - tag that should contain selection
*/ */
public expandToTag(node: HTMLElement): void { public expandToTag(node: HTMLElement): void {
new SelectionUtils().expandToTag(node); new SelectionUtils().expandToTag(node);
} }
} }

View file

@ -1,11 +1,14 @@
import Module from '../../__module'; import Module from '../../__module';
import {Styles} from '../../../../types/api'; import { Styles } from '../../../../types/api';
/** /**
* *
*/ */
export default class StylesAPI extends Module { export default class StylesAPI extends Module {
get classes(): Styles { /**
* Exported classes
*/
public get classes(): Styles {
return { return {
/** /**
* Base Block styles * Base Block styles

View file

@ -1,5 +1,5 @@
import Module from '../../__module'; import Module from '../../__module';
import {Toolbar} from '../../../../types/api'; import { Toolbar } from '../../../../types/api';
/** /**
* @class ToolbarAPI * @class ToolbarAPI
@ -8,12 +8,13 @@ import {Toolbar} from '../../../../types/api';
export default class ToolbarAPI extends Module { export default class ToolbarAPI extends Module {
/** /**
* Available methods * Available methods
* @return {Toolbar} *
* @returns {Toolbar}
*/ */
get methods(): Toolbar { public get methods(): Toolbar {
return { return {
close: () => this.close(), close: (): void => this.close(),
open: () => this.open(), open: (): void => this.open(),
}; };
} }
@ -30,5 +31,4 @@ export default class ToolbarAPI extends Module {
public close(): void { public close(): void {
this.Editor.Toolbar.close(); this.Editor.Toolbar.close();
} }
} }

View file

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

View file

@ -6,10 +6,13 @@ import * as _ from '../utils';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
import Flipper from '../flipper'; import Flipper from '../flipper';
/**
*
*/
export default class BlockEvents extends Module { export default class BlockEvents extends Module {
/** /**
* All keydowns on Block * All keydowns on Block
*
* @param {KeyboardEvent} event - keydown * @param {KeyboardEvent} event - keydown
*/ */
public keydown(event: KeyboardEvent): void { public keydown(event: KeyboardEvent): void {
@ -43,18 +46,12 @@ export default class BlockEvents extends Module {
case _.keyCodes.TAB: case _.keyCodes.TAB:
this.tabPressed(event); this.tabPressed(event);
break; break;
case _.keyCodes.ESC:
this.escapePressed(event);
break;
default:
this.defaultHandler();
break;
} }
} }
/** /**
* Fires on keydown before event processing * Fires on keydown before event processing
*
* @param {KeyboardEvent} event - keydown * @param {KeyboardEvent} event - keydown
*/ */
public beforeKeydownProcessing(event: KeyboardEvent): void { public beforeKeydownProcessing(event: KeyboardEvent): void {
@ -77,6 +74,7 @@ export default class BlockEvents extends Module {
/** /**
* Allow to use shortcuts with selected blocks * Allow to use shortcuts with selected blocks
*
* @type {boolean} * @type {boolean}
*/ */
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey; const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
@ -92,9 +90,10 @@ export default class BlockEvents extends Module {
* Key up on Block: * Key up on Block:
* - shows Inline Toolbar if something selected * - shows Inline Toolbar if something selected
* - shows conversion toolbar with 85% of block selection * - shows conversion toolbar with 85% of block selection
*
* @param {KeyboardEvent} event - keyup event
*/ */
public keyup(event): void { public keyup(event): void {
/** /**
* If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows) * If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows)
*/ */
@ -108,16 +107,10 @@ export default class BlockEvents extends Module {
this.Editor.UI.checkEmptiness(); this.Editor.UI.checkEmptiness();
} }
/**
* Mouse up on Block:
*/
public mouseUp(): void {
}
/** /**
* Set up mouse selection handlers * Set up mouse selection handlers
* *
* @param {MouseEvent} event * @param {MouseEvent} event - mouse down event
*/ */
public mouseDown(event: MouseEvent): void { public mouseDown(event: MouseEvent): void {
/** /**
@ -131,7 +124,8 @@ export default class BlockEvents extends Module {
/** /**
* Open Toolbox to leaf Tools * Open Toolbox to leaf Tools
* @param {KeyboardEvent} event *
* @param {KeyboardEvent} event - tab keydown event
*/ */
public tabPressed(event): void { public tabPressed(event): void {
/** /**
@ -160,37 +154,12 @@ export default class BlockEvents extends Module {
} }
} }
/**
* Escape pressed
* If some of Toolbar components are opened, then close it otherwise close Toolbar
*
* @param {Event} event
*/
public escapePressed(event): void {
/**
* Clear blocks selection by ESC
*/
this.Editor.BlockSelection.clearSelection(event);
if (this.Editor.Toolbox.opened) {
this.Editor.Toolbox.close();
} else if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close();
} else if (this.Editor.ConversionToolbar.opened) {
this.Editor.ConversionToolbar.close();
} else if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.close();
} else {
this.Editor.Toolbar.close();
}
}
/** /**
* Add drop target styles * Add drop target styles
* *
* @param {DragEvent} e * @param {DragEvent} e - drag over event
*/ */
public dragOver(e: DragEvent) { public dragOver(e: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node); const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
block.dropTarget = true; block.dropTarget = true;
@ -199,9 +168,9 @@ export default class BlockEvents extends Module {
/** /**
* Remove drop target style * Remove drop target style
* *
* @param {DragEvent} e * @param {DragEvent} e - drag leave event
*/ */
public dragLeave(e: DragEvent) { public dragLeave(e: DragEvent): void {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node); const block = this.Editor.BlockManager.getBlockByChildNode(e.target as Node);
block.dropTarget = false; block.dropTarget = false;
@ -211,7 +180,7 @@ export default class BlockEvents extends Module {
* Copying selected blocks * Copying selected blocks
* Before putting to the clipboard we sanitize all blocks and then copy to the clipboard * Before putting to the clipboard we sanitize all blocks and then copy to the clipboard
* *
* @param {ClipboardEvent} event * @param {ClipboardEvent} event - clipboard event
*/ */
public handleCommandC(event: ClipboardEvent): void { public handleCommandC(event: ClipboardEvent): void {
const { BlockSelection } = this.Editor; const { BlockSelection } = this.Editor;
@ -226,7 +195,8 @@ export default class BlockEvents extends Module {
/** /**
* Copy and Delete selected Blocks * Copy and Delete selected Blocks
* @param {ClipboardEvent} event *
* @param {ClipboardEvent} event - clipboard event
*/ */
public handleCommandX(event: ClipboardEvent): void { public handleCommandX(event: ClipboardEvent): void {
const { BlockSelection, BlockManager, Caret } = this.Editor; const { BlockSelection, BlockManager, Caret } = this.Editor;
@ -247,6 +217,7 @@ export default class BlockEvents extends Module {
/** /**
* ENTER pressed on block * ENTER pressed on block
*
* @param {KeyboardEvent} event - keydown * @param {KeyboardEvent} event - keydown
*/ */
private enter(event: KeyboardEvent): void { private enter(event: KeyboardEvent): void {
@ -314,6 +285,7 @@ export default class BlockEvents extends Module {
/** /**
* Handle backspace keydown on Block * Handle backspace keydown on Block
*
* @param {KeyboardEvent} event - keydown * @param {KeyboardEvent} event - keydown
*/ */
private backspace(event: KeyboardEvent): void { private backspace(event: KeyboardEvent): void {
@ -324,7 +296,7 @@ export default class BlockEvents extends Module {
/** /**
* Check if Block should be removed by current Backspace keydown * Check if Block should be removed by current Backspace keydown
*/ */
if (currentBlock.selected || currentBlock.isEmpty && currentBlock.currentInput === currentBlock.firstInput) { if (currentBlock.selected || (currentBlock.isEmpty && currentBlock.currentInput === currentBlock.firstInput)) {
event.preventDefault(); event.preventDefault();
const index = BlockManager.currentBlockIndex; const index = BlockManager.currentBlockIndex;
@ -339,7 +311,7 @@ export default class BlockEvents extends Module {
Caret.setToBlock( Caret.setToBlock(
BlockManager.currentBlock, BlockManager.currentBlock,
index ? Caret.positions.END : Caret.positions.START, index ? Caret.positions.END : Caret.positions.START
); );
/** Close Toolbar */ /** Close Toolbar */
@ -347,6 +319,7 @@ export default class BlockEvents extends Module {
/** Clear selection */ /** Clear selection */
BlockSelection.clearSelection(event); BlockSelection.clearSelection(event);
return; return;
} }
@ -382,7 +355,7 @@ export default class BlockEvents extends Module {
/** /**
* Merge current and previous Blocks if they have the same type * Merge current and previous Blocks if they have the same type
*/ */
private mergeBlocks() { private mergeBlocks(): void {
const { BlockManager, Caret, Toolbar } = this.Editor; const { BlockManager, Caret, Toolbar } = this.Editor;
const targetBlock = BlockManager.previousBlock; const targetBlock = BlockManager.previousBlock;
const blockToMerge = BlockManager.currentBlock; const blockToMerge = BlockManager.currentBlock;
@ -401,6 +374,7 @@ export default class BlockEvents extends Module {
Caret.setToBlock(BlockManager.currentBlock); Caret.setToBlock(BlockManager.currentBlock);
Toolbar.close(); Toolbar.close();
return; return;
} }
@ -413,7 +387,7 @@ export default class BlockEvents extends Module {
Caret.createShadow(targetBlock.pluginsContent); Caret.createShadow(targetBlock.pluginsContent);
BlockManager.mergeBlocks(targetBlock, blockToMerge) BlockManager.mergeBlocks(targetBlock, blockToMerge)
.then( () => { .then(() => {
/** Restore caret position after merge */ /** Restore caret position after merge */
Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement); Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
targetBlock.pluginsContent.normalize(); targetBlock.pluginsContent.normalize();
@ -423,6 +397,8 @@ export default class BlockEvents extends Module {
/** /**
* Handle right and down keyboard keys * Handle right and down keyboard keys
*
* @param {KeyboardEvent} event - keyboard event
*/ */
private arrowRightAndDown(event: KeyboardEvent): void { private arrowRightAndDown(event: KeyboardEvent): void {
const isFlipperCombination = Flipper.usedKeys.includes(event.keyCode) && const isFlipperCombination = Flipper.usedKeys.includes(event.keyCode) &&
@ -446,6 +422,7 @@ export default class BlockEvents extends Module {
if (event.shiftKey && event.keyCode === _.keyCodes.DOWN && shouldEnableCBS) { if (event.shiftKey && event.keyCode === _.keyCodes.DOWN && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState(); this.Editor.CrossBlockSelection.toggleBlockSelectedState();
return; return;
} }
@ -474,6 +451,8 @@ export default class BlockEvents extends Module {
/** /**
* Handle left and up keyboard keys * Handle left and up keyboard keys
*
* @param {KeyboardEvent} event - keyboard event
*/ */
private arrowLeftAndUp(event: KeyboardEvent): void { private arrowLeftAndUp(event: KeyboardEvent): void {
/** /**
@ -498,6 +477,7 @@ export default class BlockEvents extends Module {
if (event.shiftKey && event.keyCode === _.keyCodes.UP && shouldEnableCBS) { if (event.shiftKey && event.keyCode === _.keyCodes.UP && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState(false); this.Editor.CrossBlockSelection.toggleBlockSelectedState(false);
return; return;
} }
@ -524,20 +504,17 @@ export default class BlockEvents extends Module {
this.Editor.BlockSelection.clearSelection(event); this.Editor.BlockSelection.clearSelection(event);
} }
/**
* Default keydown handler
*/
private defaultHandler(): void {}
/** /**
* Cases when we need to close Toolbar * Cases when we need to close Toolbar
*
* @param {KeyboardEvent} event - keyboard event
*/ */
private needToolbarClosing(event) { private needToolbarClosing(event: KeyboardEvent): boolean {
const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbox.opened), const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbox.opened),
blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened), blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened),
inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened), inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened),
conversionToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.ConversionToolbar.opened), conversionToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.ConversionToolbar.opened),
flippingToolbarItems = event.keyCode === _.keyCodes.TAB; flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
/** /**
* Do not close Toolbar in cases: * Do not close Toolbar in cases:
@ -545,12 +522,12 @@ export default class BlockEvents extends Module {
* 2. When Toolbar is opened and Tab leafs its Tools * 2. When Toolbar is opened and Tab leafs its Tools
* 3. When Toolbar's component is opened and some its item selected * 3. When Toolbar's component is opened and some its item selected
*/ */
return !(event.shiftKey return !(event.shiftKey ||
|| flippingToolbarItems flippingToolbarItems ||
|| toolboxItemSelected toolboxItemSelected ||
|| blockSettingsItemSelected blockSettingsItemSelected ||
|| inlineToolbarItemSelected inlineToolbarItemSelected ||
|| conversionToolbarItemSelected conversionToolbarItemSelected
); );
} }
@ -559,7 +536,7 @@ export default class BlockEvents extends Module {
*/ */
private activateToolbox(): void { private activateToolbox(): void {
if (!this.Editor.Toolbar.opened) { if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(false , false); this.Editor.Toolbar.open(false, false);
this.Editor.Toolbar.plusButton.show(); this.Editor.Toolbar.plusButton.show();
} }

View file

@ -6,23 +6,23 @@
* *
* @version 2.0.0 * @version 2.0.0
*/ */
import Block, {BlockToolAPI} from '../block'; import Block, { BlockToolAPI } from '../block';
import Module from '../__module'; import Module from '../__module';
import $ from '../dom'; import $ from '../dom';
import * as _ from '../utils'; import * as _ from '../utils';
import Blocks from '../blocks'; import Blocks from '../blocks';
import {BlockTool, BlockToolConstructable, BlockToolData, PasteEvent, ToolConfig} from '../../../types'; import { BlockToolConstructable, BlockToolData, PasteEvent } from '../../../types';
/** /**
* @typedef {BlockManager} BlockManager * @typedef {BlockManager} BlockManager
* @property {Number} currentBlockIndex - Index of current working block * @property {number} currentBlockIndex - Index of current working block
* @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks} * @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks}
*/ */
export default class BlockManager extends Module { export default class BlockManager extends Module {
/** /**
* Returns current Block index * Returns current Block index
* @return {number} *
* @returns {number}
*/ */
public get currentBlockIndex(): number { public get currentBlockIndex(): number {
return this._currentBlockIndex; return this._currentBlockIndex;
@ -30,7 +30,8 @@ export default class BlockManager extends Module {
/** /**
* Set current Block index and fire Block lifecycle callbacks * Set current Block index and fire Block lifecycle callbacks
* @param newIndex *
* @param {number} newIndex - index of Block to set as current
*/ */
public set currentBlockIndex(newIndex: number) { public set currentBlockIndex(newIndex: number) {
if (this._blocks[this._currentBlockIndex]) { if (this._blocks[this._currentBlockIndex]) {
@ -46,7 +47,8 @@ export default class BlockManager extends Module {
/** /**
* returns first Block * returns first Block
* @return {Block} *
* @returns {Block}
*/ */
public get firstBlock(): Block { public get firstBlock(): Block {
return this._blocks[0]; return this._blocks[0];
@ -54,7 +56,8 @@ export default class BlockManager extends Module {
/** /**
* returns last Block * returns last Block
* @return {Block} *
* @returns {Block}
*/ */
public get lastBlock(): Block { public get lastBlock(): Block {
return this._blocks[this._blocks.length - 1]; return this._blocks[this._blocks.length - 1];
@ -63,7 +66,7 @@ export default class BlockManager extends Module {
/** /**
* Get current Block instance * Get current Block instance
* *
* @return {Block} * @returns {Block}
*/ */
public get currentBlock(): Block { public get currentBlock(): Block {
return this._blocks[this.currentBlockIndex]; return this._blocks[this.currentBlockIndex];
@ -71,7 +74,8 @@ export default class BlockManager extends Module {
/** /**
* Returns next Block instance * Returns next Block instance
* @return {Block|null} *
* @returns {Block|null}
*/ */
public get nextBlock(): Block { public get nextBlock(): Block {
const isLastBlock = this.currentBlockIndex === (this._blocks.length - 1); const isLastBlock = this.currentBlockIndex === (this._blocks.length - 1);
@ -107,7 +111,8 @@ export default class BlockManager extends Module {
/** /**
* Returns previous Block instance * Returns previous Block instance
* @return {Block|null} *
* @returns {Block|null}
*/ */
public get previousBlock(): Block { public get previousBlock(): Block {
const isFirstBlock = this.currentBlockIndex === 0; const isFirstBlock = this.currentBlockIndex === 0;
@ -142,7 +147,7 @@ export default class BlockManager extends Module {
* *
* @type {number} * @type {number}
*/ */
private _currentBlockIndex: number = -1; private _currentBlockIndex = -1;
/** /**
* Proxy for Blocks instance {@link Blocks} * Proxy for Blocks instance {@link Blocks}
@ -158,7 +163,7 @@ export default class BlockManager extends Module {
* *
* @returns {Promise} * @returns {Promise}
*/ */
public async prepare() { public async prepare(): Promise<void> {
const blocks = new Blocks(this.Editor.UI.nodes.redactor); const blocks = new Blocks(this.Editor.UI.nodes.redactor);
const { BlockEvents, Listeners } = this.Editor; const { BlockEvents, Listeners } = this.Editor;
@ -185,30 +190,36 @@ export default class BlockManager extends Module {
Listeners.on( Listeners.on(
document, document,
'copy', 'copy',
(e: ClipboardEvent) => BlockEvents.handleCommandC(e), (e: ClipboardEvent) => BlockEvents.handleCommandC(e)
); );
/** Copy and cut */ /** Copy and cut */
Listeners.on( Listeners.on(
document, document,
'cut', 'cut',
(e: ClipboardEvent) => BlockEvents.handleCommandX(e), (e: ClipboardEvent) => BlockEvents.handleCommandX(e)
); );
} }
/** /**
* Creates Block instance by tool name * Creates Block instance by tool name
* *
* @param {String} toolName - tools passed in editor config {@link EditorConfig#tools} * @param {object} options - block creation options
* @param {Object} data - constructor params * @param {string} options.tool - tools passed in editor config {@link EditorConfig#tools}
* @param {Object} settings - block settings * @param {BlockToolData} [options.data] - constructor params
* *
* @return {Block} * @returns {Block}
*/ */
public composeBlock(toolName: string, data: BlockToolData = {}, settings: ToolConfig = {}): Block { public composeBlock({ tool, data = {} }: {tool: string; data?: BlockToolData}): Block {
const toolInstance = this.Editor.Tools.construct(toolName, data) as BlockTool; const settings = this.Editor.Tools.getToolSettings(tool);
const toolClass = this.Editor.Tools.available[toolName] as BlockToolConstructable; const Tool = this.Editor.Tools.available[tool] as BlockToolConstructable;
const block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API.methods); const block = new Block({
name: tool,
data,
Tool,
settings,
api: this.Editor.API,
});
this.bindEvents(block); this.bindEvents(block);
@ -218,57 +229,94 @@ export default class BlockManager extends Module {
/** /**
* Insert new block into _blocks * Insert new block into _blocks
* *
* @param {String} toolName plugin name, by default method inserts initial block type * @param {object} options - insert options
* @param {Object} data plugin data * @param {string} options.tool - plugin name, by default method inserts initial block type
* @param {Object} settings - default settings * @param {object} options.data - plugin data
* @param {number} index - index where to insert new Block * @param {number} options.index - index where to insert new Block
* @param {boolean} needToFocus - flag shows if needed to update current Block index * @param {boolean} options.needToFocus - flag shows if needed to update current Block index
* @param {boolean} options.replace - flag shows if block by passed index should be replaced with inserted one
* *
* @return {Block} * @returns {Block}
*/ */
public insert( public insert({
toolName: string = this.config.initialBlock, tool = this.config.initialBlock,
data: BlockToolData = {}, data = {},
settings: ToolConfig = {}, index,
index: number = this.currentBlockIndex + 1, needToFocus = true,
needToFocus: boolean = true, replace = false,
): Block { }: {
const block = this.composeBlock(toolName, data, settings); tool?: string;
data?: BlockToolData;
index?: number;
needToFocus?: boolean;
replace?: boolean;
} = {}): Block {
let newIndex = index;
this._blocks[index] = block; if (newIndex === undefined) {
newIndex = this.currentBlockIndex + (replace ? 0 : 1);
}
const block = this.composeBlock({
tool,
data,
});
this._blocks.insert(newIndex, block, replace);
if (needToFocus) { if (needToFocus) {
this.currentBlockIndex = index; this.currentBlockIndex = newIndex;
} else if (newIndex <= this.currentBlockIndex) {
this.currentBlockIndex++;
} }
return block; return block;
} }
/**
* Replace current working block
*
* @param {object} options - replace options
* @param {string} options.tool plugin name
* @param {BlockToolData} options.data plugin data
*
* @returns {Block}
*/
public replace({
tool = this.config.initialBlock,
data = {},
}): Block {
return this.insert({
tool,
data,
index: this.currentBlockIndex,
replace: true,
});
}
/** /**
* Insert pasted content. Call onPaste callback after insert. * Insert pasted content. Call onPaste callback after insert.
* *
* @param {string} toolName * @param {string} toolName - name of Tool to insert
* @param {PasteEvent} pasteEvent - pasted data * @param {PasteEvent} pasteEvent - pasted data
* @param {boolean} replace - should replace current block * @param {boolean} replace - should replace current block
*/ */
public paste( public paste(
toolName: string, toolName: string,
pasteEvent: PasteEvent, pasteEvent: PasteEvent,
replace: boolean = false, replace = false
): Block { ): Block {
let block; const block = this.insert({
tool: toolName,
if (replace) { replace,
block = this.replace(toolName); });
} else {
block = this.insert(toolName);
}
try { try {
block.call(BlockToolAPI.ON_PASTE, pasteEvent); block.call(BlockToolAPI.ON_PASTE, pasteEvent);
} catch (e) { } catch (e) {
_.log(`${toolName}: onPaste callback call is failed`, 'error', e); _.log(`${toolName}: onPaste callback call is failed`, 'error', e);
} }
return block; return block;
} }
@ -280,10 +328,10 @@ export default class BlockManager extends Module {
* *
* TODO: Remove method and use insert() with index instead (?) * TODO: Remove method and use insert() with index instead (?)
* *
* @return {Block} inserted Block * @returns {Block} inserted Block
*/ */
public insertInitialBlockAtIndex(index: number, needToFocus: boolean = false) { public insertInitialBlockAtIndex(index: number, needToFocus = false): Block {
const block = this.composeBlock(this.config.initialBlock, {}, {}); const block = this.composeBlock({ tool: this.config.initialBlock });
this._blocks[index] = block; this._blocks[index] = block;
@ -298,7 +346,8 @@ export default class BlockManager extends Module {
/** /**
* Always inserts at the end * Always inserts at the end
* @return {Block} *
* @returns {Block}
*/ */
public insertAtEnd(): Block { public insertAtEnd(): Block {
/** /**
@ -314,10 +363,11 @@ export default class BlockManager extends Module {
/** /**
* Merge two blocks * Merge two blocks
*
* @param {Block} targetBlock - previous block will be append to this block * @param {Block} targetBlock - previous block will be append to this block
* @param {Block} blockToMerge - block that will be merged with target block * @param {Block} blockToMerge - block that will be merged with target block
* *
* @return {Promise} - the sequence that can be continued * @returns {Promise} - the sequence that can be continued
*/ */
public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise<void> { public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise<void> {
const blockToMergeIndex = this._blocks.indexOf(blockToMerge); const blockToMergeIndex = this._blocks.indexOf(blockToMerge);
@ -338,12 +388,18 @@ export default class BlockManager extends Module {
/** /**
* Remove block with passed index or remove last * Remove block with passed index or remove last
* @param {Number|null} index *
* @param {number|null} index - index of Block to remove
* @throws {Error} if Block to remove is not found
*/ */
public removeBlock(index?: number): void { public removeBlock(index = this.currentBlockIndex): void {
if (index === undefined) { /**
index = this.currentBlockIndex; * If index is not passed and there is no block selected, show a warning
*/
if (!this.validateIndex(index)) {
throw new Error('Can\'t find a Block to remove');
} }
this._blocks.remove(index); this._blocks.remove(index);
if (this.currentBlockIndex >= index) { if (this.currentBlockIndex >= index) {
@ -356,7 +412,6 @@ export default class BlockManager extends Module {
if (!this.blocks.length) { if (!this.blocks.length) {
this.currentBlockIndex = -1; this.currentBlockIndex = -1;
this.insert(); this.insert();
return;
} else if (index === 0) { } else if (index === 0) {
this.currentBlockIndex = 0; this.currentBlockIndex = 0;
} }
@ -365,9 +420,10 @@ export default class BlockManager extends Module {
/** /**
* Remove only selected Blocks * Remove only selected Blocks
* and returns first Block index where started removing... * and returns first Block index where started removing...
* @return number|undefined *
* @returns {number|undefined}
*/ */
public removeSelectedBlocks(): number|undefined { public removeSelectedBlocks(): number | undefined {
let firstSelectedBlockIndex; let firstSelectedBlockIndex;
/** /**
@ -405,7 +461,7 @@ export default class BlockManager extends Module {
* 1. Extract content from Caret position to the Block`s end * 1. Extract content from Caret position to the Block`s end
* 2. Insert a new Block below current one with extracted content * 2. Insert a new Block below current one with extracted content
* *
* @return {Block} * @returns {Block}
*/ */
public split(): Block { public split(): Block {
const extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition(); const extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition();
@ -422,36 +478,18 @@ export default class BlockManager extends Module {
/** /**
* Renew current Block * Renew current Block
*
* @type {Block} * @type {Block}
*/ */
return this.insert(this.config.initialBlock, data); return this.insert({ data });
}
/**
* Replace current working block
*
* @param {String} toolName plugin name
* @param {BlockToolData} data plugin data
* @param {ToolConfig} settings plugin config
*
* @return {Block}
*/
public replace(
toolName: string = this.config.initialBlock,
data: BlockToolData = {},
settings: ToolConfig = {},
): Block {
const block = this.composeBlock(toolName, data, settings);
this._blocks.insert(this.currentBlockIndex, block, true);
return block;
} }
/** /**
* Returns Block by passed index * Returns Block by passed index
* @param {Number} index *
* @return {Block} * @param {number} index - index to get
*
* @returns {Block}
*/ */
public getBlockByIndex(index): Block { public getBlockByIndex(index): Block {
return this._blocks[index]; return this._blocks[index];
@ -459,7 +497,9 @@ export default class BlockManager extends Module {
/** /**
* Get Block instance by html element * Get Block instance by html element
* @param {Node} element *
* @param {Node} element - html element to get Block by
*
* @returns {Block} * @returns {Block}
*/ */
public getBlock(element: HTMLElement): Block { public getBlock(element: HTMLElement): Block {
@ -468,8 +508,8 @@ export default class BlockManager extends Module {
} }
const nodes = this._blocks.nodes, const nodes = this._blocks.nodes,
firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`), firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`),
index = nodes.indexOf(firstLevelBlock as HTMLElement); index = nodes.indexOf(firstLevelBlock as HTMLElement);
if (index >= 0) { if (index >= 0) {
return this._blocks[index]; return this._blocks[index];
@ -487,6 +527,7 @@ export default class BlockManager extends Module {
/** /**
* Mark current Block as selected * Mark current Block as selected
*
* @type {boolean} * @type {boolean}
*/ */
this.currentBlock.focused = true; this.currentBlock.focused = true;
@ -496,7 +537,9 @@ export default class BlockManager extends Module {
* Remove selection from all Blocks * Remove selection from all Blocks
*/ */
public clearFocused(): void { public clearFocused(): void {
this.blocks.forEach( (block) => block.focused = false); this.blocks.forEach((block) => {
block.focused = false;
});
} }
/** /**
@ -504,7 +547,7 @@ export default class BlockManager extends Module {
* 2) Mark it as current * 2) Mark it as current
* *
* @param {Node} childNode - look ahead from this node. * @param {Node} childNode - look ahead from this node.
* @param {string} caretPosition - position where to set caret *
* @throws Error - when passed Node is not included at the Block * @throws Error - when passed Node is not included at the Block
*/ */
public setCurrentBlockByChildNode(childNode: Node): Block { public setCurrentBlockByChildNode(childNode: Node): Block {
@ -520,9 +563,11 @@ export default class BlockManager extends Module {
if (parentFirstLevelBlock) { if (parentFirstLevelBlock) {
/** /**
* Update current Block's index * Update current Block's index
*
* @type {number} * @type {number}
*/ */
this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock as HTMLElement); this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock as HTMLElement);
return this.currentBlock; return this.currentBlock;
} else { } else {
throw new Error('Can not find a Block from this child Node'); throw new Error('Can not find a Block from this child Node');
@ -532,8 +577,9 @@ export default class BlockManager extends Module {
/** /**
* Return block which contents passed node * Return block which contents passed node
* *
* @param {Node} childNode * @param {Node} childNode - node to get Block by
* @return {Block} *
* @returns {Block}
*/ */
public getBlockByChildNode(childNode: Node): Block { public getBlockByChildNode(childNode: Node): Block {
/** /**
@ -550,8 +596,10 @@ export default class BlockManager extends Module {
/** /**
* Swap Blocks Position * Swap Blocks Position
* @param {Number} fromIndex *
* @param {Number} toIndex * @param {number} fromIndex - index of first block
* @param {number} toIndex - index of second block
*
* @deprecated use 'move' instead * @deprecated use 'move' instead
*/ */
public swap(fromIndex, toIndex): void { public swap(fromIndex, toIndex): void {
@ -564,18 +612,21 @@ export default class BlockManager extends Module {
/** /**
* Move a block to a new index * Move a block to a new index
* @param {Number} toIndex *
* @param {Number} fromIndex * @param {number} toIndex - index where to move Block
* @param {number} fromIndex - index of Block to move
*/ */
public move(toIndex, fromIndex = this.currentBlockIndex): void { public move(toIndex, fromIndex = this.currentBlockIndex): void {
// make sure indexes are valid and within a valid range // make sure indexes are valid and within a valid range
if (isNaN(toIndex) || isNaN(fromIndex)) { if (isNaN(toIndex) || isNaN(fromIndex)) {
_.log(`Warning during 'move' call: incorrect indices provided.`, 'warn'); _.log(`Warning during 'move' call: incorrect indices provided.`, 'warn');
return; return;
} }
if (!this.validateIndex(toIndex) || !this.validateIndex(fromIndex)) { if (!this.validateIndex(toIndex) || !this.validateIndex(fromIndex)) {
_.log(`Warning during 'move' call: indices cannot be lower than 0 or greater than the amount of blocks.`, 'warn'); _.log(`Warning during 'move' call: indices cannot be lower than 0 or greater than the amount of blocks.`, 'warn');
return; return;
} }
@ -597,16 +648,17 @@ export default class BlockManager extends Module {
/** /**
* Clears Editor * Clears Editor
*
* @param {boolean} needAddInitialBlock - 1) in internal calls (for example, in api.blocks.render) * @param {boolean} needAddInitialBlock - 1) in internal calls (for example, in api.blocks.render)
* we don't need to add empty initial block * we don't need to add empty initial block
* 2) in api.blocks.clear we should add empty block * 2) in api.blocks.clear we should add empty block
*/ */
public clear(needAddInitialBlock: boolean = false): void { public clear(needAddInitialBlock = false): void {
this._blocks.removeAll(); this._blocks.removeAll();
this.dropPointer(); this.dropPointer();
if (needAddInitialBlock) { if (needAddInitialBlock) {
this.insert(this.config.initialBlock); this.insert();
} }
/** /**
@ -617,13 +669,13 @@ export default class BlockManager extends Module {
/** /**
* Bind Events * Bind Events
* @param {Object} block *
* @param {Block} block - Block to which event should be bound
*/ */
private bindEvents(block: Block): void { private bindEvents(block: Block): void {
const {BlockEvents, Listeners} = this.Editor; const { BlockEvents, Listeners } = this.Editor;
Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true); Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), false);
Listeners.on(block.holder, 'mouseup', (event) => BlockEvents.mouseUp());
Listeners.on(block.holder, 'mousedown', (event: MouseEvent) => BlockEvents.mouseDown(event)); Listeners.on(block.holder, 'mousedown', (event: MouseEvent) => BlockEvents.mouseDown(event));
Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event)); Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event));
Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent)); Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));
@ -632,13 +684,12 @@ export default class BlockManager extends Module {
/** /**
* Validates that the given index is not lower than 0 or higher than the amount of blocks * Validates that the given index is not lower than 0 or higher than the amount of blocks
*
* @param {number} index - index of blocks array to validate * @param {number} index - index of blocks array to validate
*
* @returns {boolean}
*/ */
private validateIndex(index: number): boolean { private validateIndex(index: number): boolean {
if (index < 0 || index >= this._blocks.length) { return !(index < 0 || index >= this._blocks.length);
return false;
}
return true;
} }
} }

View file

@ -11,14 +11,18 @@ import * as _ from '../utils';
import $ from '../dom'; import $ from '../dom';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
import { SanitizerConfig } from '../../../types/configs';
/**
*
*/
export default class BlockSelection extends Module { export default class BlockSelection extends Module {
/** /**
* Sanitizer Config * Sanitizer Config
* @return {SanitizerConfig} *
* @returns {SanitizerConfig}
*/ */
private get sanitizerConfig() { private get sanitizerConfig(): SanitizerConfig {
return { return {
p: {}, p: {},
h1: {}, h1: {},
@ -47,37 +51,43 @@ export default class BlockSelection extends Module {
/** /**
* Flag that identifies all Blocks selection * Flag that identifies all Blocks selection
* @return {boolean} *
* @returns {boolean}
*/ */
public get allBlocksSelected(): boolean { public get allBlocksSelected(): boolean {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
return BlockManager.blocks.every((block) => block.selected === true); return BlockManager.blocks.every((block) => block.selected === true);
} }
/** /**
* Set selected all blocks * Set selected all blocks
* @param {boolean} state *
* @param {boolean} state - state to set
*/ */
public set allBlocksSelected(state: boolean) { public set allBlocksSelected(state: boolean) {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
BlockManager.blocks.forEach((block) => block.selected = state); BlockManager.blocks.forEach((block) => {
block.selected = state;
});
} }
/** /**
* Flag that identifies any Block selection * Flag that identifies any Block selection
* @return {boolean} *
* @returns {boolean}
*/ */
public get anyBlockSelected(): boolean { public get anyBlockSelected(): boolean {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
return BlockManager.blocks.some((block) => block.selected === true); return BlockManager.blocks.some((block) => block.selected === true);
} }
/** /**
* Return selected Blocks array * Return selected Blocks array
* @return {Block[]} *
* @returns {Block[]}
*/ */
public get selectedBlocks(): Block[] { public get selectedBlocks(): Block[] {
return this.Editor.BlockManager.blocks.filter((block: Block) => block.selected); return this.Editor.BlockManager.blocks.filter((block: Block) => block.selected);
@ -86,26 +96,30 @@ export default class BlockSelection extends Module {
/** /**
* Flag used to define block selection * Flag used to define block selection
* First CMD+A defines it as true and then second CMD+A selects all Blocks * First CMD+A defines it as true and then second CMD+A selects all Blocks
*
* @type {boolean} * @type {boolean}
*/ */
private needToSelectAll: boolean = false; private needToSelectAll = false;
/** /**
* Flag used to define native input selection * Flag used to define native input selection
* In this case we allow double CMD+A to select Block * In this case we allow double CMD+A to select Block
*
* @type {boolean} * @type {boolean}
*/ */
private nativeInputSelected: boolean = false; private nativeInputSelected = false;
/** /**
* Flag identifies any input selection * Flag identifies any input selection
* That means we can select whole Block * That means we can select whole Block
*
* @type {boolean} * @type {boolean}
*/ */
private readyToBlockSelection: boolean = false; private readyToBlockSelection = false;
/** /**
* SelectionUtils instance * SelectionUtils instance
*
* @type {SelectionUtils} * @type {SelectionUtils}
*/ */
private selection: SelectionUtils; private selection: SelectionUtils;
@ -116,13 +130,14 @@ export default class BlockSelection extends Module {
* to select all and copy them * to select all and copy them
*/ */
public prepare(): void { public prepare(): void {
const {Shortcuts} = this.Editor; const { Shortcuts } = this.Editor;
/** Selection shortcut */ /** Selection shortcut */
Shortcuts.add({ Shortcuts.add({
name: 'CMD+A', name: 'CMD+A',
handler: (event) => { handler: (event) => {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
/** /**
* When one page consist of two or more EditorJS instances * 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
@ -143,10 +158,11 @@ export default class BlockSelection extends Module {
/** /**
* Remove selection of Block * Remove selection of Block
*
* @param {number?} index - Block index according to the BlockManager's indexes * @param {number?} index - Block index according to the BlockManager's indexes
*/ */
public unSelectBlockByIndex(index?) { public unSelectBlockByIndex(index?): void {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
let block; let block;
@ -165,24 +181,27 @@ export default class BlockSelection extends Module {
* @param {Event} reason - event caused clear of selection * @param {Event} reason - event caused clear of selection
* @param {boolean} restoreSelection - if true, restore saved selection * @param {boolean} restoreSelection - if true, restore saved selection
*/ */
public clearSelection(reason?: Event, restoreSelection = false) { public clearSelection(reason?: Event, restoreSelection = false): void {
const {BlockManager, Caret, RectangleSelection} = this.Editor; const { BlockManager, Caret, RectangleSelection } = this.Editor;
this.needToSelectAll = false; this.needToSelectAll = false;
this.nativeInputSelected = false; this.nativeInputSelected = false;
this.readyToBlockSelection = false; this.readyToBlockSelection = false;
const isKeyboard = reason && (reason instanceof KeyboardEvent);
const isPrintableKey = isKeyboard && _.isPrintableKey((reason as KeyboardEvent).keyCode);
/** /**
* If reason caused clear of the selection was printable key and any block is selected, * If reason caused clear of the selection was printable key and any block is selected,
* remove selected blocks and insert pressed key * remove selected blocks and insert pressed key
*/ */
if (this.anyBlockSelected && reason && reason instanceof KeyboardEvent && _.isPrintableKey(reason.keyCode)) { if (this.anyBlockSelected && isKeyboard && isPrintableKey && !SelectionUtils.isSelectionExists) {
const indexToInsert = BlockManager.removeSelectedBlocks(); const indexToInsert = BlockManager.removeSelectedBlocks();
BlockManager.insertInitialBlockAtIndex(indexToInsert, true); BlockManager.insertInitialBlockAtIndex(indexToInsert, true);
Caret.setToBlock(BlockManager.currentBlock); Caret.setToBlock(BlockManager.currentBlock);
_.delay(() => { _.delay(() => {
Caret.insertContentAtCaretPosition(reason.key); Caret.insertContentAtCaretPosition((reason as KeyboardEvent).key);
}, 20)(); }, 20)();
} }
@ -190,6 +209,7 @@ export default class BlockSelection extends Module {
if (!this.anyBlockSelected || RectangleSelection.isRectActivated()) { if (!this.anyBlockSelected || RectangleSelection.isRectActivated()) {
this.Editor.RectangleSelection.clearSelection(); this.Editor.RectangleSelection.clearSelection();
return; return;
} }
@ -210,7 +230,7 @@ export default class BlockSelection extends Module {
* *
* @param {ClipboardEvent} e - copy/cut event * @param {ClipboardEvent} e - copy/cut event
* *
* @return Promise<void> * @returns {Promise<void>}
*/ */
public async copySelectedBlocks(e: ClipboardEvent): Promise<void> { public async copySelectedBlocks(e: ClipboardEvent): Promise<void> {
/** /**
@ -221,19 +241,20 @@ export default class BlockSelection extends Module {
const fakeClipboard = $.make('div'); const fakeClipboard = $.make('div');
this.selectedBlocks.forEach((block) => { this.selectedBlocks.forEach((block) => {
/** /**
* Make <p> tag that holds clean HTML * Make <p> tag that holds clean HTML
*/ */
const cleanHTML = this.Editor.Sanitizer.clean(block.holder.innerHTML, this.sanitizerConfig); const cleanHTML = this.Editor.Sanitizer.clean(block.holder.innerHTML, this.sanitizerConfig);
const fragment = $.make('p'); const fragment = $.make('p');
fragment.innerHTML = cleanHTML; fragment.innerHTML = cleanHTML;
fakeClipboard.appendChild(fragment); fakeClipboard.appendChild(fragment);
}); });
const savedData = await Promise.all(this.selectedBlocks.map((block) => block.save())); const savedData = await Promise.all(this.selectedBlocks.map((block) => block.save()));
const textPlain = Array.from(fakeClipboard.childNodes).map((node) => node.textContent).join('\n\n'); const textPlain = Array.from(fakeClipboard.childNodes).map((node) => node.textContent)
.join('\n\n');
const textHTML = fakeClipboard.innerHTML; const textHTML = fakeClipboard.innerHTML;
e.clipboardData.setData('text/plain', textPlain); e.clipboardData.setData('text/plain', textPlain);
@ -243,10 +264,11 @@ export default class BlockSelection extends Module {
/** /**
* select Block * select Block
*
* @param {number?} index - Block index according to the BlockManager's indexes * @param {number?} index - Block index according to the BlockManager's indexes
*/ */
public selectBlockByIndex(index?) { public selectBlockByIndex(index?): void {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
/** /**
* Remove previous focused Block's state * Remove previous focused Block's state
@ -272,11 +294,22 @@ export default class BlockSelection extends Module {
this.Editor.InlineToolbar.close(); this.Editor.InlineToolbar.close();
} }
/**
* Module destruction
* De-registers Shortcut CMD+A
*/
public destroy(): void {
const { Shortcuts } = this.Editor;
/** Selection shortcut */
Shortcuts.remove('CMD+A');
}
/** /**
* First CMD+A selects all input content by native behaviour, * First CMD+A selects all input content by native behaviour,
* next CMD+A keypress selects all blocks * next CMD+A keypress selects all blocks
* *
* @param {KeyboardEvent} event * @param {KeyboardEvent} event - keyboard event
*/ */
private handleCommandA(event: KeyboardEvent): void { private handleCommandA(event: KeyboardEvent): void {
this.Editor.RectangleSelection.clearSelection(); this.Editor.RectangleSelection.clearSelection();
@ -284,6 +317,7 @@ export default class BlockSelection extends Module {
/** allow default selection on native inputs */ /** allow default selection on native inputs */
if ($.isNativeInput(event.target) && !this.readyToBlockSelection) { if ($.isNativeInput(event.target) && !this.readyToBlockSelection) {
this.readyToBlockSelection = true; this.readyToBlockSelection = true;
return; return;
} }
@ -296,11 +330,13 @@ export default class BlockSelection extends Module {
*/ */
if (inputs.length > 1 && !this.readyToBlockSelection) { if (inputs.length > 1 && !this.readyToBlockSelection) {
this.readyToBlockSelection = true; this.readyToBlockSelection = true;
return; return;
} }
if (inputs.length === 1 && !this.needToSelectAll) { if (inputs.length === 1 && !this.needToSelectAll) {
this.needToSelectAll = true; this.needToSelectAll = true;
return; return;
} }
@ -344,7 +380,7 @@ export default class BlockSelection extends Module {
* Select All Blocks * Select All Blocks
* Each Block has selected setter that makes Block copyable * Each Block has selected setter that makes Block copyable
*/ */
private selectAllBlocks() { private selectAllBlocks(): void {
/** /**
* Save selection * Save selection
* Will be restored when closeSelection fired * Will be restored when closeSelection fired

View file

@ -19,14 +19,13 @@ import * as _ from '../utils';
* @typedef {Caret} Caret * @typedef {Caret} Caret
*/ */
export default class Caret extends Module { export default class Caret extends Module {
/** /**
* Allowed caret positions in input * Allowed caret positions in input
* *
* @static * @static
* @returns {{START: string, END: string, DEFAULT: string}} * @returns {{START: string, END: string, DEFAULT: string}}
*/ */
public get positions(): {START: string, END: string, DEFAULT: string} { public get positions(): {START: string; END: string; DEFAULT: string} {
return { return {
START: 'start', START: 'start',
END: 'end', END: 'end',
@ -45,7 +44,8 @@ export default class Caret extends Module {
/** /**
* Get's deepest first node and checks if offset is zero * Get's deepest first node and checks if offset is zero
* @return {boolean} *
* @returns {boolean}
*/ */
public get isAtStart(): boolean { public get isAtStart(): boolean {
const selection = Selection.get(); const selection = Selection.get();
@ -65,6 +65,7 @@ export default class Caret extends Module {
/** /**
* Workaround case when caret in the text like " |Hello!" * Workaround case when caret in the text like " |Hello!"
* selection.anchorOffset is 1, but real caret visible position is 0 * selection.anchorOffset is 1, but real caret visible position is 0
*
* @type {number} * @type {number}
*/ */
@ -84,6 +85,7 @@ export default class Caret extends Module {
* So we use child with focusOffset index as new anchorNode. * So we use child with focusOffset index as new anchorNode.
*/ */
let focusOffset = selection.focusOffset; let focusOffset = selection.focusOffset;
if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) { if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {
if (focusNode.childNodes[focusOffset]) { if (focusNode.childNodes[focusOffset]) {
focusNode = focusNode.childNodes[focusOffset]; focusNode = focusNode.childNodes[focusOffset];
@ -106,6 +108,7 @@ export default class Caret extends Module {
const nothingAtLeft = leftSiblings.every((node) => { const nothingAtLeft = leftSiblings.every((node) => {
/** /**
* Workaround case when block starts with several <br>'s (created by SHIFT+ENTER) * Workaround case when block starts with several <br>'s (created by SHIFT+ENTER)
*
* @see https://github.com/codex-team/editor.js/issues/726 * @see https://github.com/codex-team/editor.js/issues/726
* We need to allow to delete such linebreaks, so in this case caret IS NOT AT START * We need to allow to delete such linebreaks, so in this case caret IS NOT AT START
*/ */
@ -128,12 +131,13 @@ export default class Caret extends Module {
* We use <= comparison for case: * We use <= comparison for case:
* "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1 * "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1
*/ */
return firstNode === null || focusNode === firstNode && focusOffset <= firstLetterPosition; return firstNode === null || (focusNode === firstNode && focusOffset <= firstLetterPosition);
} }
/** /**
* Get's deepest last node and checks if offset is last node text length * Get's deepest last node and checks if offset is last node text length
* @return {boolean} *
* @returns {boolean}
*/ */
public get isAtEnd(): boolean { public get isAtEnd(): boolean {
const selection = Selection.get(); const selection = Selection.get();
@ -161,6 +165,7 @@ export default class Caret extends Module {
* So we use child with anchofocusOffset - 1 as new focusNode. * So we use child with anchofocusOffset - 1 as new focusNode.
*/ */
let focusOffset = selection.focusOffset; let focusOffset = selection.focusOffset;
if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) { if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {
if (focusNode.childNodes[focusOffset - 1]) { if (focusNode.childNodes[focusOffset - 1]) {
focusNode = focusNode.childNodes[focusOffset - 1]; focusNode = focusNode.childNodes[focusOffset - 1];
@ -186,7 +191,7 @@ export default class Caret extends Module {
*/ */
const isLastBR = i === rightSiblings.length - 1 && $.isLineBreakTag(node as HTMLElement); const isLastBR = i === rightSiblings.length - 1 && $.isLineBreakTag(node as HTMLElement);
return (isLastBR) || $.isEmpty(node) && !$.isLineBreakTag(node); return isLastBR || ($.isEmpty(node) && !$.isLineBreakTag(node));
}); });
if (nothingAtRight && focusOffset === focusNode.textContent.length) { if (nothingAtRight && focusOffset === focusNode.textContent.length) {
@ -216,12 +221,12 @@ export default class Caret extends Module {
* - last found text node: sets at the end of the node. Also, you can customize the behaviour * - last found text node: sets at the end of the node. Also, you can customize the behaviour
* *
* @param {Block} block - Block class * @param {Block} block - Block class
* @param {String} position - position where to set caret. * @param {string} position - position where to set caret.
* If default - leave default behaviour and apply offset if it's passed * If default - leave default behaviour and apply offset if it's passed
* @param {Number} offset - caret offset regarding to the text node * @param {number} offset - caret offset regarding to the text node
*/ */
public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset: number = 0): void { public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
let element; let element;
switch (position) { switch (position) {
@ -255,7 +260,7 @@ export default class Caret extends Module {
/** /**
* @todo try to fix via Promises or use querySelectorAll to not to use timeout * @todo try to fix via Promises or use querySelectorAll to not to use timeout
*/ */
_.delay( () => { _.delay(() => {
this.set(nodeToSet as HTMLElement, offset); this.set(nodeToSet as HTMLElement, offset);
}, 20)(); }, 20)();
@ -267,12 +272,12 @@ export default class Caret extends Module {
* Set caret to the current input of current Block. * Set caret to the current input of current Block.
* *
* @param {HTMLElement} input - input where caret should be set * @param {HTMLElement} input - input where caret should be set
* @param {String} position - position of the caret. * @param {string} position - position of the caret.
* If default - leave default behaviour and apply offset if it's passed * If default - leave default behaviour and apply offset if it's passed
* @param {number} offset - caret offset regarding to the text node * @param {number} offset - caret offset regarding to the text node
*/ */
public setToInput(input: HTMLElement, position: string = this.positions.DEFAULT, offset: number = 0): void { public setToInput(input: HTMLElement, position: string = this.positions.DEFAULT, offset = 0): void {
const {currentBlock} = this.Editor.BlockManager; const { currentBlock } = this.Editor.BlockManager;
const nodeToSet = $.getDeepestNode(input); const nodeToSet = $.getDeepestNode(input);
switch (position) { switch (position) {
@ -281,9 +286,7 @@ export default class Caret extends Module {
break; break;
case this.positions.END: case this.positions.END:
const contentLength = $.getContentLength(nodeToSet); this.set(nodeToSet as HTMLElement, $.getContentLength(nodeToSet));
this.set(nodeToSet as HTMLElement, contentLength);
break; break;
default: default:
@ -297,39 +300,24 @@ export default class Caret extends Module {
/** /**
* Creates Document Range and sets caret to the element with offset * Creates Document Range and sets caret to the element with offset
*
* @param {HTMLElement} element - target node. * @param {HTMLElement} element - target node.
* @param {Number} offset - offset * @param {number} offset - offset
*/ */
public set(element: HTMLElement, offset: number = 0): void { public set(element: HTMLElement, offset = 0): void {
const range = document.createRange(), const { top, bottom } = Selection.setCursor(element, offset);
selection = Selection.get();
/** if found deepest node is native input */
if ($.isNativeInput(element)) {
if (!$.canSetCaret(element)) {
return;
}
element.focus();
(element as HTMLInputElement).selectionStart = (element as HTMLInputElement).selectionEnd = offset;
return;
}
range.setStart(element, offset);
range.setEnd(element, offset);
selection.removeAllRanges();
selection.addRange(range);
/** If new cursor position is not visible, scroll to it */ /** If new cursor position is not visible, scroll to it */
const {top, bottom} = element.nodeType === Node.ELEMENT_NODE const { innerHeight } = window;
? element.getBoundingClientRect()
: range.getBoundingClientRect();
const {innerHeight} = window;
if (top < 0) { window.scrollBy(0, top); } if (top < 0) {
if (bottom > innerHeight) { window.scrollBy(0, bottom - innerHeight); } window.scrollBy(0, top);
}
if (bottom > innerHeight) {
window.scrollBy(0, bottom - innerHeight);
}
} }
/** /**
* Set Caret to the last Block * Set Caret to the last Block
* If last block is not empty, append another empty block * If last block is not empty, append another empty block
@ -371,6 +359,7 @@ export default class Caret extends Module {
range.selectNodeContents(currentBlockInput); range.selectNodeContents(currentBlockInput);
range.setStart(selectRange.endContainer, selectRange.endOffset); range.setStart(selectRange.endContainer, selectRange.endOffset);
return range.extractContents(); return range.extractContents();
} }
} }
@ -381,22 +370,36 @@ export default class Caret extends Module {
* Before moving caret, we should check if caret position is at the end of Plugins node * Before moving caret, we should check if caret position is at the end of Plugins node
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection * Using {@link Dom#getDeepestNode} to get a last node and match with current selection
* *
* @param {Boolean} force - force navigation even if caret is not at the end * @param {boolean} force - force navigation even if caret is not at the end
* *
* @return {Boolean} * @returns {boolean}
*/ */
public navigateNext(force: boolean = false): boolean { public navigateNext(force = false): boolean {
const {currentBlock, nextContentfulBlock} = this.Editor.BlockManager; const { BlockManager, Tools } = this.Editor;
const {nextInput} = currentBlock; const { currentBlock, nextContentfulBlock } = BlockManager;
const { nextInput } = currentBlock;
if (!nextContentfulBlock && !nextInput) { let nextBlock = nextContentfulBlock;
return false;
if (!nextBlock && !nextInput) {
/**
* If there is no nextBlock and currentBlock is initial, do not navigate
*/
if (Tools.isInitial(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
*/
nextBlock = BlockManager.insertAtEnd();
} }
if (force || this.isAtEnd) { if (force || this.isAtEnd) {
/** If next Tool`s input exists, focus on it. Otherwise set caret to the next Block */ /** If next Tool`s input exists, focus on it. Otherwise set caret to the next Block */
if (!nextInput) { if (!nextInput) {
this.setToBlock(nextContentfulBlock, this.positions.START); this.setToBlock(nextBlock, this.positions.START);
} else { } else {
this.setToInput(nextInput, this.positions.START); this.setToInput(nextInput, this.positions.START);
} }
@ -412,18 +415,18 @@ export default class Caret extends Module {
* Before moving caret, we should check if caret position is start of the Plugins node * Before moving caret, we should check if caret position is start of the Plugins node
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection * Using {@link Dom#getDeepestNode} to get a last node and match with current selection
* *
* @param {Boolean} force - force navigation even if caret is not at the start * @param {boolean} force - force navigation even if caret is not at the start
* *
* @return {Boolean} * @returns {boolean}
*/ */
public navigatePrevious(force: boolean = false): boolean { public navigatePrevious(force = false): boolean {
const {currentBlock, previousContentfulBlock} = this.Editor.BlockManager; const { currentBlock, previousContentfulBlock } = this.Editor.BlockManager;
if (!currentBlock) { if (!currentBlock) {
return false; return false;
} }
const {previousInput} = currentBlock; const { previousInput } = currentBlock;
if (!previousContentfulBlock && !previousInput) { if (!previousContentfulBlock && !previousInput) {
return false; return false;
@ -432,10 +435,11 @@ export default class Caret extends Module {
if (force || this.isAtStart) { if (force || this.isAtStart) {
/** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */ /** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */
if (!previousInput) { if (!previousInput) {
this.setToBlock( previousContentfulBlock, this.positions.END ); this.setToBlock(previousContentfulBlock, this.positions.END);
} else { } else {
this.setToInput(previousInput, this.positions.END); this.setToInput(previousInput, this.positions.END);
} }
return true; return true;
} }
@ -444,18 +448,20 @@ export default class Caret extends Module {
/** /**
* Inserts shadow element after passed element where caret can be placed * Inserts shadow element after passed element where caret can be placed
* @param {Node} element *
* @param {Element} element - element after which shadow caret should be inserted
*/ */
public createShadow(element): void { public createShadow(element: Element): void {
const shadowCaret = document.createElement('span'); const shadowCaret = document.createElement('span');
shadowCaret.classList.add(Caret.CSS.shadowCaret); shadowCaret.classList.add(Caret.CSS.shadowCaret);
element.insertAdjacentElement('beforeEnd', shadowCaret); element.insertAdjacentElement('beforeend', shadowCaret);
} }
/** /**
* Restores caret position * Restores caret position
* @param {HTMLElement} element *
* @param {HTMLElement} element - element where caret should be restored
*/ */
public restoreCaret(element: HTMLElement): void { public restoreCaret(element: HTMLElement): void {
const shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`); const shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`);
@ -519,18 +525,21 @@ export default class Caret extends Module {
* *
* @example * @example
* <div contenteditable> * <div contenteditable>
* <p></p> | * <p></p> |
* <p></p> | left first-level siblings * <p></p> | left first-level siblings
* <p></p> | * <p></p> |
* <blockquote><a><b>adaddad</b><a><blockquote> <-- passed node for example <b> * <blockquote><a><b>adaddad</b><a><blockquote> <-- passed node for example <b>
* <p></p> | * <p></p> |
* <p></p> | right first-level siblings * <p></p> | right first-level siblings
* <p></p> | * <p></p> |
* </div> * </div>
* *
* @return {Element[]} * @param {HTMLElement} from - element from which siblings should be searched
* @param {'left' | 'right'} direction - direction of search
*
* @returns {HTMLElement[]}
*/ */
private getHigherLevelSiblings(from: HTMLElement, direction?: string): HTMLElement[] { private getHigherLevelSiblings(from: HTMLElement, direction?: 'left' | 'right'): HTMLElement[] {
let current = from; let current = from;
const siblings = []; const siblings = [];

View file

@ -3,6 +3,9 @@ import Block from '../block';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
import * as _ from '../utils'; import * as _ from '../utils';
/**
*
*/
export default class CrossBlockSelection extends Module { export default class CrossBlockSelection extends Module {
/** /**
* Block where selection is started * Block where selection is started
@ -24,7 +27,7 @@ export default class CrossBlockSelection extends Module {
return; return;
} }
const {BlockManager, UI, Listeners} = this.Editor; const { BlockManager, Listeners } = this.Editor;
this.firstSelectedBlock = BlockManager.getBlock(event.target as HTMLElement); this.firstSelectedBlock = BlockManager.getBlock(event.target as HTMLElement);
this.lastSelectedBlock = this.firstSelectedBlock; this.lastSelectedBlock = this.firstSelectedBlock;
@ -37,8 +40,8 @@ export default class CrossBlockSelection extends Module {
* return boolean is cross block selection started * return boolean is cross block selection started
*/ */
public get isCrossBlockSelectionStarted(): boolean { public get isCrossBlockSelectionStarted(): boolean {
return !!this.firstSelectedBlock return !!this.firstSelectedBlock &&
&& !!this.lastSelectedBlock; !!this.lastSelectedBlock;
} }
/** /**
@ -47,8 +50,8 @@ export default class CrossBlockSelection extends Module {
* *
* @param {boolean} next - if true, toggle next block. Previous otherwise * @param {boolean} next - if true, toggle next block. Previous otherwise
*/ */
public toggleBlockSelectedState(next: boolean = true): void { public toggleBlockSelectedState(next = true): void {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
if (!this.lastSelectedBlock) { if (!this.lastSelectedBlock) {
this.lastSelectedBlock = this.firstSelectedBlock = BlockManager.currentBlock; this.lastSelectedBlock = this.firstSelectedBlock = BlockManager.currentBlock;
@ -83,8 +86,8 @@ export default class CrossBlockSelection extends Module {
* *
* @param {Event} reason - event caused clear of selection * @param {Event} reason - event caused clear of selection
*/ */
public clear(reason?: Event) { public clear(reason?: Event): void {
const {BlockManager, BlockSelection, Caret} = this.Editor; const { BlockManager, BlockSelection, Caret } = this.Editor;
const fIndex = BlockManager.blocks.indexOf(this.firstSelectedBlock); const fIndex = BlockManager.blocks.indexOf(this.firstSelectedBlock);
const lIndex = BlockManager.blocks.indexOf(this.lastSelectedBlock); const lIndex = BlockManager.blocks.indexOf(this.lastSelectedBlock);
@ -121,8 +124,8 @@ export default class CrossBlockSelection extends Module {
* Mouse up event handler. * Mouse up event handler.
* Removes the listeners * Removes the listeners
*/ */
private onMouseUp = (): void => { private onMouseUp = (): void => {
const {Listeners} = this.Editor; const { Listeners } = this.Editor;
Listeners.off(document, 'mouseover', this.onMouseOver); Listeners.off(document, 'mouseover', this.onMouseOver);
Listeners.off(document, 'mouseup', this.onMouseUp); Listeners.off(document, 'mouseup', this.onMouseUp);
@ -132,10 +135,10 @@ export default class CrossBlockSelection extends Module {
* Mouse over event handler * Mouse over event handler
* Gets target and related blocks and change selected state for blocks in between * Gets target and related blocks and change selected state for blocks in between
* *
* @param {MouseEvent} event * @param {MouseEvent} event - mouse over event
*/ */
private onMouseOver = (event: MouseEvent): void => { private onMouseOver = (event: MouseEvent): void => {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
const relatedBlock = BlockManager.getBlockByChildNode(event.relatedTarget as Node) || this.lastSelectedBlock; const relatedBlock = BlockManager.getBlockByChildNode(event.relatedTarget as Node) || this.lastSelectedBlock;
const targetBlock = BlockManager.getBlockByChildNode(event.target as Node); const targetBlock = BlockManager.getBlockByChildNode(event.target as Node);
@ -153,12 +156,14 @@ export default class CrossBlockSelection extends Module {
relatedBlock.selected = true; relatedBlock.selected = true;
targetBlock.selected = true; targetBlock.selected = true;
return; return;
} }
if (targetBlock === this.firstSelectedBlock) { if (targetBlock === this.firstSelectedBlock) {
relatedBlock.selected = false; relatedBlock.selected = false;
targetBlock.selected = false; targetBlock.selected = false;
return; return;
} }
@ -171,11 +176,11 @@ export default class CrossBlockSelection extends Module {
/** /**
* Change blocks selection state between passed two blocks. * Change blocks selection state between passed two blocks.
* *
* @param {Block} firstBlock * @param {Block} firstBlock - first block in range
* @param {Block} lastBlock * @param {Block} lastBlock - last block in range
*/ */
private toggleBlocksSelectedState(firstBlock: Block, lastBlock: Block): void { private toggleBlocksSelectedState(firstBlock: Block, lastBlock: Block): void {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
const fIndex = BlockManager.blocks.indexOf(firstBlock); const fIndex = BlockManager.blocks.indexOf(firstBlock);
const lIndex = BlockManager.blocks.indexOf(lastBlock); const lIndex = BlockManager.blocks.indexOf(lastBlock);

View file

@ -1,20 +1,20 @@
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
import Module from '../__module'; import Module from '../__module';
/**
*
*/
export default class DragNDrop extends Module { export default class DragNDrop extends Module {
/** /**
* If drag has been started at editor, we save it * If drag has been started at editor, we save it
* *
* @type Boolean * @type {boolean}
* @private * @private
*/ */
private isStartedAtEditor = false; private isStartedAtEditor = false;
/** /**
* Bind events * Bind events
*
* @private
*/ */
public prepare(): void { public prepare(): void {
this.bindEvents(); this.bindEvents();
@ -22,13 +22,13 @@ export default class DragNDrop extends Module {
/** /**
* Add drag events listeners to editor zone * Add drag events listeners to editor zone
*
* @private * @private
*/ */
private bindEvents(): void { private bindEvents(): void {
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'drop', this.processDrop, true); this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'drop', this.processDrop, true);
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragstart', (dragEvent: DragEvent) => { this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragstart', (dragEvent: DragEvent) => {
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) { if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {
this.isStartedAtEditor = true; this.isStartedAtEditor = true;
} }
@ -43,7 +43,7 @@ export default class DragNDrop extends Module {
/** /**
* Handle drop event * Handle drop event
* *
* @param {DragEvent} dropEvent * @param {DragEvent} dropEvent - drop event
*/ */
private processDrop = async (dropEvent: DragEvent): Promise<void> => { private processDrop = async (dropEvent: DragEvent): Promise<void> => {
const { const {
@ -54,7 +54,9 @@ export default class DragNDrop extends Module {
dropEvent.preventDefault(); dropEvent.preventDefault();
BlockManager.blocks.forEach((block) => block.dropTarget = false); BlockManager.blocks.forEach((block) => {
block.dropTarget = false;
});
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed && this.isStartedAtEditor) { if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed && this.isStartedAtEditor) {
document.execCommand('delete'); document.execCommand('delete');

View file

@ -11,23 +11,23 @@ import Module from '../__module';
* @version 1.0.0 * @version 1.0.0
* *
* @typedef {Events} Events * @typedef {Events} Events
* @property {Object} subscribers - all subscribers grouped by event name * @property {object} subscribers - all subscribers grouped by event name
*/ */
export default class Events extends Module { export default class Events extends Module {
/** /**
* Object with events` names as key and array of callback functions as value * Object with events` names as key and array of callback functions as value
*
* @type {{}} * @type {{}}
*/ */
private subscribers: {[name: string]: Array<(data?: any) => any>} = {}; private subscribers: {[name: string]: Array<(data?: object) => object>} = {};
/** /**
* Subscribe any event on callback * Subscribe any event on callback
* *
* @param {String} eventName - event name * @param {string} eventName - event name
* @param {Function} callback - subscriber * @param {Function} callback - subscriber
*/ */
public on(eventName: string, callback: (data: any) => any) { public on(eventName: string, callback: (data: object) => object): void {
if (!(eventName in this.subscribers)) { if (!(eventName in this.subscribers)) {
this.subscribers[eventName] = []; this.subscribers[eventName] = [];
} }
@ -39,15 +39,15 @@ export default class Events extends Module {
/** /**
* Subscribe any event on callback. Callback will be called once and be removed from subscribers array after call. * Subscribe any event on callback. Callback will be called once and be removed from subscribers array after call.
* *
* @param {String} eventName - event name * @param {string} eventName - event name
* @param {Function} callback - subscriber * @param {Function} callback - subscriber
*/ */
public once(eventName: string, callback: (data: any) => any) { public once(eventName: string, callback: (data: object) => object): void {
if (!(eventName in this.subscribers)) { if (!(eventName in this.subscribers)) {
this.subscribers[eventName] = []; this.subscribers[eventName] = [];
} }
const wrappedCallback = (data: any) => { const wrappedCallback = (data: object): object => {
const result = callback(data); const result = callback(data);
const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback); const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback);
@ -66,10 +66,10 @@ export default class Events extends Module {
/** /**
* Emit callbacks with passed data * Emit callbacks with passed data
* *
* @param {String} eventName - event name * @param {string} eventName - event name
* @param {Object} data - subscribers get this data when they were fired * @param {object} data - subscribers get this data when they were fired
*/ */
public emit(eventName: string, data?: any): void { public emit(eventName: string, data?: object): void {
if (!this.subscribers[eventName]) { if (!this.subscribers[eventName]) {
return; return;
} }
@ -77,17 +77,17 @@ export default class Events extends Module {
this.subscribers[eventName].reduce((previousData, currentHandler) => { this.subscribers[eventName].reduce((previousData, currentHandler) => {
const newData = currentHandler(previousData); const newData = currentHandler(previousData);
return newData ? newData : previousData; return newData || previousData;
}, data); }, data);
} }
/** /**
* Unsubsribe callback from event * Unsubscribe callback from event
* *
* @param eventName * @param {string} eventName - event name
* @param callback * @param {Function} callback - event handler
*/ */
public off(eventName: string, callback: (data: any) => void): void { public off(eventName: string, callback: (data: object) => object): void {
for (let i = 0; i < this.subscribers[eventName].length; i++) { for (let i = 0; i < this.subscribers[eventName].length; i++) {
if (this.subscribers[eventName][i] === callback) { if (this.subscribers[eventName][i] === callback) {
delete this.subscribers[eventName][i]; delete this.subscribers[eventName][i];

View file

@ -2,6 +2,8 @@ import Module from '../__module';
/** /**
* Event listener information * Event listener information
*
* @interface ListenerData
*/ */
export interface ListenerData { export interface ListenerData {
/** /**
@ -17,7 +19,7 @@ export interface ListenerData {
/** /**
* Event handler * Event handler
* *
* @param {Event} event * @param {Event} event - event object
*/ */
handler: (event: Event) => void; handler: (event: Event) => void;
@ -40,12 +42,12 @@ export interface ListenerData {
/** /**
* @typedef {Listeners} Listeners * @typedef {Listeners} Listeners
* @property {Array} allListeners * @property {ListenerData[]} allListeners - listeners store
*/ */
export default class Listeners extends Module { export default class Listeners extends Module {
/** /**
* Stores all listeners data to find/remove/process it * Stores all listeners data to find/remove/process it
*
* @type {ListenerData[]} * @type {ListenerData[]}
*/ */
private allListeners: ListenerData[] = []; private allListeners: ListenerData[] = [];
@ -54,15 +56,15 @@ export default class Listeners extends Module {
* Assigns event listener on element * Assigns event listener on element
* *
* @param {EventTarget} element - DOM element that needs to be listened * @param {EventTarget} element - DOM element that needs to be listened
* @param {String} eventType - event type * @param {string} eventType - event type
* @param {Function} handler - method that will be fired on event * @param {Function} handler - method that will be fired on event
* @param {Boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once} * @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}
*/ */
public on( public on(
element: EventTarget, element: EventTarget,
eventType: string, eventType: string,
handler: (event: Event) => void, handler: (event: Event) => void,
options: boolean | AddEventListenerOptions = false, options: boolean | AddEventListenerOptions = false
): void { ): void {
const assignedEventData = { const assignedEventData = {
element, element,
@ -73,7 +75,9 @@ export default class Listeners extends Module {
const alreadyExist = this.findOne(element, eventType, handler); const alreadyExist = this.findOne(element, eventType, handler);
if (alreadyExist) { return; } if (alreadyExist) {
return;
}
this.allListeners.push(assignedEventData); this.allListeners.push(assignedEventData);
element.addEventListener(eventType, handler, options); element.addEventListener(eventType, handler, options);
@ -83,15 +87,15 @@ export default class Listeners extends Module {
* Removes event listener from element * Removes event listener from element
* *
* @param {EventTarget} element - DOM element that we removing listener * @param {EventTarget} element - DOM element that we removing listener
* @param {String} eventType - event type * @param {string} eventType - event type
* @param {Function} handler - remove handler, if element listens several handlers on the same event type * @param {Function} handler - remove handler, if element listens several handlers on the same event type
* @param {Boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once} * @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}
*/ */
public off( public off(
element: EventTarget, element: EventTarget,
eventType: string, eventType: string,
handler?: (event: Event) => void, handler?: (event: Event) => void,
options?: boolean | AddEventListenerOptions, options?: boolean | AddEventListenerOptions
): void { ): void {
const existingListeners = this.findAll(element, eventType, handler); const existingListeners = this.findAll(element, eventType, handler);
@ -104,14 +108,16 @@ export default class Listeners extends Module {
listener.element.removeEventListener(listener.eventType, listener.handler, listener.options); listener.element.removeEventListener(listener.eventType, listener.handler, listener.options);
} }
}); });
} }
/** /**
* @param {EventTarget} element * Finds and returns first listener by passed params
* @param {String} eventType *
* @param {Function} handler * @param {EventTarget} element - event target
* @return {ListenerData|null} * @param {string} [eventType] - event type
* @param {Function} [handler] - event handler
*
* @returns {ListenerData|null}
*/ */
public findOne(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData { public findOne(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData {
const foundListeners = this.findAll(element, eventType, handler); const foundListeners = this.findAll(element, eventType, handler);
@ -120,19 +126,22 @@ export default class Listeners extends Module {
} }
/** /**
* @param {EventTarget} element * Return all stored listeners by passed params
* @param {String} eventType *
* @param {Function} handler * @param {EventTarget} element - event target
* @return {ListenerData[]} * @param {string} eventType - event type
* @param {Function} handler - event handler
*
* @returns {ListenerData[]}
*/ */
public findAll(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData[] { public findAll(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData[] {
let found; let found;
const foundByEventTargets = element ? this.findByEventTarget(element) : []; const foundByEventTargets = element ? this.findByEventTarget(element) : [];
if (element && eventType && handler) { if (element && eventType && handler) {
found = foundByEventTargets.filter( (event) => event.eventType === eventType && event.handler === handler ); found = foundByEventTargets.filter((event) => event.eventType === eventType && event.handler === handler);
} else if (element && eventType) { } else if (element && eventType) {
found = foundByEventTargets.filter( (event) => event.eventType === eventType); found = foundByEventTargets.filter((event) => event.eventType === eventType);
} else { } else {
found = foundByEventTargets; found = foundByEventTargets;
} }
@ -144,16 +153,25 @@ export default class Listeners extends Module {
* Removes all listeners * Removes all listeners
*/ */
public removeAll(): void { public removeAll(): void {
this.allListeners.map( (current) => { this.allListeners.map((current) => {
current.element.removeEventListener(current.eventType, current.handler, current.options); current.element.removeEventListener(current.eventType, current.handler, current.options);
}); });
this.allListeners = []; this.allListeners = [];
} }
/**
* Module cleanup on destruction
*/
public destroy(): void {
this.removeAll();
}
/** /**
* Search method: looks for listener by passed element * Search method: looks for listener by passed element
*
* @param {EventTarget} element - searching element * @param {EventTarget} element - searching element
*
* @returns {Array} listeners that found on element * @returns {Array} listeners that found on element
*/ */
private findByEventTarget(element: EventTarget): ListenerData[] { private findByEventTarget(element: EventTarget): ListenerData[] {
@ -166,8 +184,10 @@ export default class Listeners extends Module {
/** /**
* Search method: looks for listener by passed event type * Search method: looks for listener by passed event type
* @param {String} eventType *
* @return {Array} listeners that found on element * @param {string} eventType - event type
*
* @returns {ListenerData[]} listeners that found on element
*/ */
private findByType(eventType: string): ListenerData[] { private findByType(eventType: string): ListenerData[] {
return this.allListeners.filter((listener) => { return this.allListeners.filter((listener) => {
@ -179,8 +199,10 @@ export default class Listeners extends Module {
/** /**
* Search method: looks for listener by passed handler * Search method: looks for listener by passed handler
* @param {Function} handler *
* @return {Array} listeners that found on element * @param {Function} handler - event handler
*
* @returns {ListenerData[]} listeners that found on element
*/ */
private findByHandler(handler: (event: Event) => void): ListenerData[] { private findByHandler(handler: (event: Event) => void): ListenerData[] {
return this.allListeners.filter((listener) => { return this.allListeners.filter((listener) => {

View file

@ -9,10 +9,13 @@ import Module from '../__module';
import * as _ from '../utils'; import * as _ from '../utils';
import Block from '../block'; import Block from '../block';
/**
*
*/
export default class ModificationsObserver extends Module { export default class ModificationsObserver extends Module {
/** /**
* Debounce Timer * Debounce Timer
*
* @type {number} * @type {number}
*/ */
public static readonly DebounceTimer = 450; public static readonly DebounceTimer = 450;
@ -29,9 +32,10 @@ export default class ModificationsObserver extends Module {
/** /**
* Used to prevent several mutation callback execution * Used to prevent several mutation callback execution
*
* @type {Function} * @type {Function}
*/ */
private mutationDebouncer = _.debounce( () => { private mutationDebouncer = _.debounce(() => {
this.updateNativeInputs(); this.updateNativeInputs();
this.config.onChange(this.Editor.API.methods); this.config.onChange(this.Editor.API.methods);
}, ModificationsObserver.DebounceTimer); }, ModificationsObserver.DebounceTimer);
@ -45,7 +49,7 @@ export default class ModificationsObserver extends Module {
/** /**
* Clear timeout and set null to mutationDebouncer property * Clear timeout and set null to mutationDebouncer property
*/ */
public destroy() { public destroy(): void {
this.mutationDebouncer = null; this.mutationDebouncer = null;
if (this.observer) { if (this.observer) {
this.observer.disconnect(); this.observer.disconnect();
@ -56,13 +60,14 @@ export default class ModificationsObserver extends Module {
/** /**
* Preparation method * Preparation method
* @return {Promise<void>} *
* @returns {Promise<void>}
*/ */
public async prepare(): Promise<void> { public async prepare(): Promise<void> {
/** /**
* wait till Browser render Editor's Blocks * wait till Browser render Editor's Blocks
*/ */
window.setTimeout( () => { window.setTimeout(() => {
this.setObserver(); this.setObserver();
}, 1000); }, 1000);
} }
@ -71,7 +76,7 @@ export default class ModificationsObserver extends Module {
* Allows to disable observer, * Allows to disable observer,
* for example when Editor wants to stealthy mutate DOM * for example when Editor wants to stealthy mutate DOM
*/ */
public disable() { public disable(): void {
this.disabled = true; this.disabled = true;
} }
@ -79,7 +84,7 @@ export default class ModificationsObserver extends Module {
* Enables mutation handling * Enables mutation handling
* Should be called after .disable() * Should be called after .disable()
*/ */
public enable() { public enable(): void {
this.disabled = false; this.disabled = false;
} }
@ -90,7 +95,7 @@ export default class ModificationsObserver extends Module {
* so that User can handle outside from API * so that User can handle outside from API
*/ */
private setObserver(): void { private setObserver(): void {
const {UI} = this.Editor; const { UI } = this.Editor;
const observerOptions = { const observerOptions = {
childList: true, childList: true,
attributes: true, attributes: true,
@ -107,10 +112,11 @@ export default class ModificationsObserver extends Module {
/** /**
* MutationObserver events handler * MutationObserver events handler
* @param mutationList *
* @param observer * @param {MutationRecord[]} mutationList - list of mutations
* @param {MutationObserver} observer - observer instance
*/ */
private mutationHandler(mutationList, observer) { private mutationHandler(mutationList: MutationRecord[], observer): void {
/** /**
* Skip mutations in stealth mode * Skip mutations in stealth mode
*/ */
@ -128,20 +134,15 @@ export default class ModificationsObserver extends Module {
mutationList.forEach((mutation) => { mutationList.forEach((mutation) => {
switch (mutation.type) { switch (mutation.type) {
case 'childList': case 'childList':
case 'subtree':
case 'characterData': case 'characterData':
case 'characterDataOldValue':
contentMutated = true; contentMutated = true;
break; break;
case 'attributes': case 'attributes':
const mutatedTarget = mutation.target as Element;
/** /**
* Changes on Element.ce-block usually is functional * Changes on Element.ce-block usually is functional
*/ */
if (!mutatedTarget.classList.contains(Block.CSS.wrapper)) { if (!(mutation.target as Element).classList.contains(Block.CSS.wrapper)) {
contentMutated = true; contentMutated = true;
return;
} }
break; break;
} }

View file

@ -5,19 +5,18 @@ import Module from '../__module';
* *
* @see https://github.com/codex-team/js-notifier * @see https://github.com/codex-team/js-notifier
*/ */
import notifier, {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier'; import notifier, { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier';
/** /**
* Notifier module * Notifier module
*/ */
export default class Notifier extends Module { export default class Notifier extends Module {
/** /**
* Show web notification * Show web notification
* *
* @param {NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions} options * @param {NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions} options - notification options
*/ */
public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) { public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void {
notifier.show(options); notifier.show(options);
} }
} }

View file

@ -4,12 +4,12 @@ import * as _ from '../utils';
import { import {
BlockTool, BlockTool,
BlockToolConstructable, BlockToolConstructable,
BlockToolData,
PasteConfig, PasteConfig,
PasteEvent, PasteEvent,
PasteEventDetail, PasteEventDetail
} from '../../../types'; } from '../../../types';
import Block from '../block'; import Block from '../block';
import { SavedData } from '../../types-internal/block-data';
/** /**
* Tag substitute object. * Tag substitute object.
@ -17,6 +17,7 @@ import Block from '../block';
interface TagSubstitute { interface TagSubstitute {
/** /**
* Name of related Tool * Name of related Tool
*
* @type {string} * @type {string}
*/ */
tool: string; tool: string;
@ -28,18 +29,21 @@ interface TagSubstitute {
interface PatternSubstitute { interface PatternSubstitute {
/** /**
* Pattern`s key * Pattern`s key
*
* @type {string} * @type {string}
*/ */
key: string; key: string;
/** /**
* Pattern regexp * Pattern regexp
*
* @type {RegExp} * @type {RegExp}
*/ */
pattern: RegExp; pattern: RegExp;
/** /**
* Name of related Tool * Name of related Tool
*
* @type {string} * @type {string}
*/ */
tool: string; tool: string;
@ -51,12 +55,14 @@ interface PatternSubstitute {
interface FilesSubstitution { interface FilesSubstitution {
/** /**
* Array of file extensions Tool can handle * Array of file extensions Tool can handle
*
* @type {string[]} * @type {string[]}
*/ */
extensions: string[]; extensions: string[];
/** /**
* Array of MIME types Tool can handle * Array of MIME types Tool can handle
*
* @type {string[]} * @type {string[]}
*/ */
mimeTypes: string[]; mimeTypes: string[];
@ -64,16 +70,20 @@ interface FilesSubstitution {
/** /**
* Processed paste data object. * Processed paste data object.
*
* @interface PasteData
*/ */
interface PasteData { interface PasteData {
/** /**
* Name of related Tool * Name of related Tool
*
* @type {string} * @type {string}
*/ */
tool: string; tool: string;
/** /**
* Pasted data. Processed and wrapped to HTML element * Pasted data. Processed and wrapped to HTML element
*
* @type {HTMLElement} * @type {HTMLElement}
*/ */
content: HTMLElement; content: HTMLElement;
@ -85,6 +95,7 @@ interface PasteData {
/** /**
* True if content should be inserted as new Block * True if content should be inserted as new Block
*
* @type {boolean} * @type {boolean}
*/ */
isBlock: boolean; isBlock: boolean;
@ -99,7 +110,6 @@ interface PasteData {
* @version 2.0.0 * @version 2.0.0
*/ */
export default class Paste extends Module { export default class Paste extends Module {
/** If string`s length is greater than this number we don't check paste patterns */ /** If string`s length is greater than this number we don't check paste patterns */
public static readonly PATTERN_PROCESSING_MAX_LENGTH = 450; public static readonly PATTERN_PROCESSING_MAX_LENGTH = 450;
@ -121,7 +131,7 @@ export default class Paste extends Module {
/** Files` substitutions parameters */ /** Files` substitutions parameters */
private toolsFiles: { private toolsFiles: {
[tool: string]: FilesSubstitution, [tool: string]: FilesSubstitution;
} = {}; } = {};
/** /**
@ -143,7 +153,7 @@ export default class Paste extends Module {
* Handle pasted or dropped data transfer object * Handle pasted or dropped data transfer object
* *
* @param {DataTransfer} dataTransfer - pasted or dropped data transfer object * @param {DataTransfer} dataTransfer - pasted or dropped data transfer object
* @param {boolean} isDragNDrop * @param {boolean} isDragNDrop - true if data transfer comes from drag'n'drop events
*/ */
public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise<void> { public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise<void> {
const { Sanitizer } = this.Editor; const { Sanitizer } = this.Editor;
@ -153,16 +163,18 @@ export default class Paste extends Module {
/** /**
* In Microsoft Edge types is DOMStringList. So 'contains' is used to check if 'Files' type included * In Microsoft Edge types is DOMStringList. So 'contains' is used to check if 'Files' type included
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const includesFiles = types.includes ? types.includes('Files') : (types as any).contains('Files'); const includesFiles = types.includes ? types.includes('Files') : (types as any).contains('Files');
if (includesFiles) { if (includesFiles) {
await this.processFiles(dataTransfer.files); await this.processFiles(dataTransfer.files);
return; return;
} }
const editorJSData = dataTransfer.getData(this.MIME_TYPE); const editorJSData = dataTransfer.getData(this.MIME_TYPE);
const plainData = dataTransfer.getData('text/plain'); const plainData = dataTransfer.getData('text/plain');
let htmlData = dataTransfer.getData('text/html'); let htmlData = dataTransfer.getData('text/html');
/** /**
* If EditorJS json is passed, insert it * If EditorJS json is passed, insert it
@ -179,7 +191,7 @@ export default class Paste extends Module {
* If text was drag'n'dropped, wrap content with P tag to insert it as the new Block * If text was drag'n'dropped, wrap content with P tag to insert it as the new Block
*/ */
if (isDragNDrop && plainData.trim() && htmlData.trim()) { if (isDragNDrop && plainData.trim() && htmlData.trim()) {
htmlData = '<p>' + ( htmlData.trim() ? htmlData : plainData ) + '</p>'; htmlData = '<p>' + (htmlData.trim() ? htmlData : plainData) + '</p>';
} }
/** Add all tags that can be substituted to sanitizer configuration */ /** Add all tags that can be substituted to sanitizer configuration */
@ -189,7 +201,7 @@ export default class Paste extends Module {
return result; return result;
}, {}); }, {});
const customConfig = Object.assign({}, toolsTags, Sanitizer.getAllInlineToolsConfig(), {br: {}}); const customConfig = Object.assign({}, toolsTags, Sanitizer.getAllInlineToolsConfig(), { br: {} });
const cleanData = Sanitizer.clean(htmlData, customConfig); const cleanData = Sanitizer.clean(htmlData, customConfig);
@ -207,8 +219,8 @@ export default class Paste extends Module {
* @param {string} data - text to process. Can be HTML or plain. * @param {string} data - text to process. Can be HTML or plain.
* @param {boolean} isHTML - if passed string is HTML, this parameter should be true * @param {boolean} isHTML - if passed string is HTML, this parameter should be true
*/ */
public async processText(data: string, isHTML: boolean = false) { public async processText(data: string, isHTML = false): Promise<void> {
const {Caret, BlockManager, Tools} = this.Editor; const { Caret, BlockManager, Tools } = this.Editor;
const dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data); const dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data);
if (!dataToInsert.length) { if (!dataToInsert.length) {
@ -221,6 +233,7 @@ export default class Paste extends Module {
} else { } else {
this.processSingleBlock(dataToInsert.pop()); this.processSingleBlock(dataToInsert.pop());
} }
return; return;
} }
@ -228,7 +241,7 @@ export default class Paste extends Module {
const needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty; const needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;
dataToInsert.map( dataToInsert.map(
async (content, i) => this.insertBlock(content, i === 0 && needToReplaceCurrentBlock), async (content, i) => this.insertBlock(content, i === 0 && needToReplaceCurrentBlock)
); );
if (BlockManager.currentBlock) { if (BlockManager.currentBlock) {
@ -240,7 +253,7 @@ export default class Paste extends Module {
* Set onPaste callback handler * Set onPaste callback handler
*/ */
private setCallback(): void { private setCallback(): void {
const {Listeners} = this.Editor; const { Listeners } = this.Editor;
Listeners.on(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent); Listeners.on(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);
} }
@ -256,20 +269,18 @@ export default class Paste extends Module {
/** /**
* Process paste config for each tool * Process paste config for each tool
*
* @param {string} name
* @param {Tool} tool
*/ */
private processTool = ([name, tool]: [string, BlockToolConstructable]): void => { private processTool = ([name, tool]: [string, BlockToolConstructable]): void => {
try { try {
const toolInstance = new this.Editor.Tools.blockTools[name]({ const toolInstance = new this.Editor.Tools.blockTools[name]({
api: this.Editor.API.methods, api: this.Editor.API.getMethodsForTool(name),
config: {}, config: {},
data: {}, data: {},
}) as BlockTool; }) as BlockTool;
if (tool.pasteConfig === false) { if (tool.pasteConfig === false) {
this.exceptionList.push(name); this.exceptionList.push(name);
return; return;
} }
@ -286,7 +297,7 @@ export default class Paste extends Module {
_.log( _.log(
`Paste handling for «${name}» Tool hasn't been set up because of the error`, `Paste handling for «${name}» Tool hasn't been set up because of the error`,
'warn', 'warn',
e, e
); );
} }
} }
@ -301,12 +312,13 @@ export default class Paste extends Module {
const tags = toolPasteConfig.tags || []; const tags = toolPasteConfig.tags || [];
tags.forEach((tag) => { tags.forEach((tag) => {
if (this.toolsTags.hasOwnProperty(tag)) { if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
_.log( _.log(
`Paste handler for «${name}» Tool on «${tag}» tag is skipped ` + `Paste handler for «${name}» Tool on «${tag}» tag is skipped ` +
`because it is already used by «${this.toolsTags[tag].tool}» Tool.`, `because it is already used by «${this.toolsTags[tag].tool}» Tool.`,
'warn', 'warn'
); );
return; return;
} }
@ -325,9 +337,8 @@ export default class Paste extends Module {
* @param {PasteConfig} toolPasteConfig - Tool onPaste configuration * @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
*/ */
private getFilesConfig(name: string, toolPasteConfig: PasteConfig): void { private getFilesConfig(name: string, toolPasteConfig: PasteConfig): void {
const { files = {} } = toolPasteConfig;
const {files = {}} = toolPasteConfig; let { extensions, mimeTypes } = files;
let {extensions, mimeTypes} = files;
if (!extensions && !mimeTypes) { if (!extensions && !mimeTypes) {
return; return;
@ -347,6 +358,7 @@ export default class Paste extends Module {
mimeTypes = mimeTypes.filter((type) => { mimeTypes = mimeTypes.filter((type) => {
if (!_.isValidMimeType(type)) { if (!_.isValidMimeType(type)) {
_.log(`MIME type value «${type}» for the «${name}» Tool is not a valid MIME type`, 'warn'); _.log(`MIME type value «${type}» for the «${name}» Tool is not a valid MIME type`, 'warn');
return false; return false;
} }
@ -376,7 +388,7 @@ export default class Paste extends Module {
if (!(pattern instanceof RegExp)) { if (!(pattern instanceof RegExp)) {
_.log( _.log(
`Pattern ${pattern} for «${name}» Tool is skipped because it should be a Regexp instance.`, `Pattern ${pattern} for «${name}» Tool is skipped because it should be a Regexp instance.`,
'warn', 'warn'
); );
} }
@ -392,6 +404,7 @@ export default class Paste extends Module {
* Check if browser behavior suits better * Check if browser behavior suits better
* *
* @param {EventTarget} element - element where content has been pasted * @param {EventTarget} element - element where content has been pasted
*
* @returns {boolean} * @returns {boolean}
*/ */
private isNativeBehaviour(element: EventTarget): boolean { private isNativeBehaviour(element: EventTarget): boolean {
@ -401,15 +414,14 @@ export default class Paste extends Module {
/** /**
* Check if Editor should process pasted data and pass data transfer object to handler * Check if Editor should process pasted data and pass data transfer object to handler
* *
* @param {ClipboardEvent} event * @param {ClipboardEvent} event - clipboard event
*/ */
private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => { private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => {
const {BlockManager, Toolbar} = this.Editor; const { BlockManager, Toolbar } = this.Editor;
/** If target is native input or is not Block, use browser behaviour */ /** If target is native input or is not Block, use browser behaviour */
if ( if (
!BlockManager.currentBlock || !BlockManager.currentBlock || (this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files'))
this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files')
) { ) {
return; return;
} }
@ -433,15 +445,15 @@ export default class Paste extends Module {
* *
* @param {FileList} items - pasted or dropped items * @param {FileList} items - pasted or dropped items
*/ */
private async processFiles(items: FileList) { private async processFiles(items: FileList): Promise<void> {
const {BlockManager, Tools} = this.Editor; const { BlockManager, Tools } = this.Editor;
let dataToInsert: Array<{type: string, event: PasteEvent}>; let dataToInsert: Array<{type: string; event: PasteEvent}>;
dataToInsert = await Promise.all( dataToInsert = await Promise.all(
Array Array
.from(items) .from(items)
.map((item) => this.processFile(item)), .map((item) => this.processFile(item))
); );
dataToInsert = dataToInsert.filter((data) => !!data); dataToInsert = dataToInsert.filter((data) => !!data);
@ -451,21 +463,21 @@ export default class Paste extends Module {
dataToInsert.forEach( dataToInsert.forEach(
(data, i) => { (data, i) => {
BlockManager.paste(data.type, data.event, i === 0 && needToReplaceCurrentBlock); BlockManager.paste(data.type, data.event, i === 0 && needToReplaceCurrentBlock);
}, }
); );
} }
/** /**
* Get information about file and find Tool to handle it * Get information about file and find Tool to handle it
* *
* @param {File} file * @param {File} file - file to process
*/ */
private async processFile(file: File) { private async processFile(file: File): Promise<{event: PasteEvent; type: string}> {
const extension = _.getFileExtension(file); const extension = _.getFileExtension(file);
const foundConfig = Object const foundConfig = Object
.entries(this.toolsFiles) .entries(this.toolsFiles)
.find(([toolName, {mimeTypes, extensions}]) => { .find(([toolName, { mimeTypes, extensions } ]) => {
const [fileType, fileSubtype] = file.type.split('/'); const [fileType, fileSubtype] = file.type.split('/');
const foundExt = extensions.find((ext) => ext.toLowerCase() === extension.toLowerCase()); const foundExt = extensions.find((ext) => ext.toLowerCase() === extension.toLowerCase());
@ -482,7 +494,7 @@ export default class Paste extends Module {
return; return;
} }
const [tool] = foundConfig; const [ tool ] = foundConfig;
const pasteEvent = this.composePasteEvent('file', { const pasteEvent = this.composePasteEvent('file', {
file, file,
}); });
@ -496,11 +508,12 @@ export default class Paste extends Module {
/** /**
* Split HTML string to blocks and return it as array of Block data * Split HTML string to blocks and return it as array of Block data
* *
* @param {string} innerHTML * @param {string} innerHTML - html string to process
*
* @returns {PasteData[]} * @returns {PasteData[]}
*/ */
private processHTML(innerHTML: string): PasteData[] { private processHTML(innerHTML: string): PasteData[] {
const {Tools, Sanitizer} = this.Editor; const { Tools, Sanitizer } = this.Editor;
const initialTool = this.config.initialBlock; const initialTool = this.config.initialBlock;
const wrapper = $.make('DIV'); const wrapper = $.make('DIV');
@ -530,7 +543,7 @@ export default class Paste extends Module {
break; break;
} }
const {tags} = Tools.blockTools[tool].pasteConfig as PasteConfig; const { tags } = Tools.blockTools[tool].pasteConfig as PasteConfig;
const toolTags = tags.reduce((result, tag) => { const toolTags = tags.reduce((result, tag) => {
result[tag.toLowerCase()] = {}; result[tag.toLowerCase()] = {};
@ -545,7 +558,12 @@ export default class Paste extends Module {
data: content, data: content,
}); });
return {content, isBlock, tool, event}; return {
content,
isBlock,
tool,
event,
};
}) })
.filter((data) => !$.isNodeEmpty(data.content) || $.isSingleTag(data.content)); .filter((data) => !$.isNodeEmpty(data.content) || $.isSingleTag(data.content));
} }
@ -553,12 +571,12 @@ export default class Paste extends Module {
/** /**
* Split plain text by new line symbols and return it as array of Block data * Split plain text by new line symbols and return it as array of Block data
* *
* @param {string} plain * @param {string} plain - string to process
*
* @returns {PasteData[]} * @returns {PasteData[]}
*/ */
private processPlain(plain: string): PasteData[] { private processPlain(plain: string): PasteData[] {
const {initialBlock} = this.config as {initialBlock: string}, const { initialBlock } = this.config as {initialBlock: string};
{Tools} = this.Editor;
if (!plain) { if (!plain) {
return []; return [];
@ -578,18 +596,23 @@ export default class Paste extends Module {
data: content, data: content,
}); });
return {content, tool, isBlock: false, event}; return {
content,
tool,
isBlock: false,
event,
};
}); });
} }
/** /**
* Process paste of single Block tool content * Process paste of single Block tool content
* *
* @param {PasteData} dataToInsert * @param {PasteData} dataToInsert - data of Block to inseret
*/ */
private async processSingleBlock(dataToInsert: PasteData): Promise<void> { private async processSingleBlock(dataToInsert: PasteData): Promise<void> {
const {Caret, BlockManager, Tools} = this.Editor; const { Caret, BlockManager, Tools } = this.Editor;
const {currentBlock} = BlockManager; const { currentBlock } = BlockManager;
/** /**
* If pasted tool isn`t equal current Block or if pasted content contains block elements, insert it as new Block * If pasted tool isn`t equal current Block or if pasted content contains block elements, insert it as new Block
@ -600,6 +623,7 @@ export default class Paste extends Module {
!$.containsOnlyInlineElements(dataToInsert.content.innerHTML) !$.containsOnlyInlineElements(dataToInsert.content.innerHTML)
) { ) {
this.insertBlock(dataToInsert, currentBlock && Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty); this.insertBlock(dataToInsert, currentBlock && Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty);
return; return;
} }
@ -612,11 +636,11 @@ export default class Paste extends Module {
* 2. Insert new block if it is not the same type as current one * 2. Insert new block if it is not the same type as current one
* 3. Just insert text if there is no substitutions * 3. Just insert text if there is no substitutions
* *
* @param {PasteData} dataToInsert * @param {PasteData} dataToInsert - data of Block to insert
*/ */
private async processInlinePaste(dataToInsert: PasteData): Promise<void> { private async processInlinePaste(dataToInsert: PasteData): Promise<void> {
const {BlockManager, Caret, Sanitizer, Tools} = this.Editor; const { BlockManager, Caret, Sanitizer, Tools } = this.Editor;
const {content, tool} = dataToInsert; const { content } = dataToInsert;
const currentBlockIsInitial = BlockManager.currentBlock && Tools.isInitial(BlockManager.currentBlock.tool); const currentBlockIsInitial = BlockManager.currentBlock && Tools.isInitial(BlockManager.currentBlock.tool);
@ -624,15 +648,14 @@ export default class Paste extends Module {
const blockData = await this.processPattern(content.textContent); const blockData = await this.processPattern(content.textContent);
if (blockData) { if (blockData) {
let insertedBlock; const needToReplaceCurrentBlock = BlockManager.currentBlock &&
Tools.isInitial(BlockManager.currentBlock.tool) &&
BlockManager.currentBlock.isEmpty;
const needToReplaceCurrentBlock = BlockManager.currentBlock const insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock);
&& Tools.isInitial(BlockManager.currentBlock.tool)
&& BlockManager.currentBlock.isEmpty;
insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock);
Caret.setToBlock(insertedBlock, Caret.positions.END); Caret.setToBlock(insertedBlock, Caret.positions.END);
return; return;
} }
} }
@ -644,7 +667,7 @@ export default class Paste extends Module {
document.execCommand( document.execCommand(
'insertHTML', 'insertHTML',
false, false,
Sanitizer.clean(content.innerHTML, currentToolSanitizeConfig), Sanitizer.clean(content.innerHTML, currentToolSanitizeConfig)
); );
} else { } else {
this.insertBlock(dataToInsert); this.insertBlock(dataToInsert);
@ -654,11 +677,12 @@ export default class Paste extends Module {
/** /**
* Get patterns` matches * Get patterns` matches
* *
* @param {string} text * @param {string} text - text to process
* @returns Promise<{data: BlockToolData, tool: string}> *
* @returns {Promise<{event: PasteEvent, tool: string}>}
*/ */
private async processPattern(text: string): Promise<{event: PasteEvent, tool: string}> { private async processPattern(text: string): Promise<{event: PasteEvent; tool: string}> {
const pattern = this.toolsPatterns.find((substitute) => { const pattern = this.toolsPatterns.find((substitute) => {
const execResult = substitute.pattern.exec(text); const execResult = substitute.pattern.exec(text);
if (!execResult) { if (!execResult) {
@ -686,18 +710,20 @@ export default class Paste extends Module {
/** /**
* Insert pasted Block content to Editor * Insert pasted Block content to Editor
* *
* @param {PasteData} data * @param {PasteData} data - data to insert
* @param {Boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block * @param {boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block
*
* @returns {void} * @returns {void}
*/ */
private insertBlock(data: PasteData, canReplaceCurrentBlock: boolean = false): void { private insertBlock(data: PasteData, canReplaceCurrentBlock = false): void {
const {BlockManager, Caret} = this.Editor; const { BlockManager, Caret } = this.Editor;
const {currentBlock} = BlockManager; const { currentBlock } = BlockManager;
let block: Block; let block: Block;
if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) { if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) {
block = BlockManager.paste(data.tool, data.event, true); block = BlockManager.paste(data.tool, data.event, true);
Caret.setToBlock(block, Caret.positions.END); Caret.setToBlock(block, Caret.positions.END);
return; return;
} }
@ -709,16 +735,14 @@ export default class Paste extends Module {
/** /**
* Insert data passed as application/x-editor-js JSON * Insert data passed as application/x-editor-js JSON
* *
* @param {object} blocks Blocks' data to insert * @param {Array} blocks Blocks' data to insert
* *
* @return {void} * @returns {void}
*/ */
private insertEditorJSData(blocks: Array<{tool: string, data: BlockToolData}>): void { private insertEditorJSData(blocks: Array<Pick<SavedData, 'data' | 'tool'>>): void {
const { BlockManager, Tools } = this.Editor; const { BlockManager, Tools } = this.Editor;
blocks.forEach(({ tool, data }, i) => { blocks.forEach(({ tool, data }, i) => {
const settings = this.Editor.Tools.getToolSettings(tool);
let needToReplaceCurrentBlock = false; let needToReplaceCurrentBlock = false;
if (i === 0) { if (i === 0) {
@ -727,25 +751,70 @@ export default class Paste extends Module {
needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty; needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;
} }
if (needToReplaceCurrentBlock) { BlockManager.insert({
BlockManager.replace(tool, data, settings); tool,
} else { data,
BlockManager.insert(tool, data, settings); replace: needToReplaceCurrentBlock,
} });
}); });
} }
/**
* Fetch nodes from Element node
*
* @param {Node} node - current node
* @param {Node[]} nodes - processed nodes
* @param {Node} destNode - destination node
*
* @returns {Node[]}
*/
private processElementNode(node: Node, nodes: Node[], destNode: Node): Node[] | void {
const tags = Object.keys(this.toolsTags);
const element = node as HTMLElement;
const { tool = '' } = this.toolsTags[element.tagName] || {};
const toolTags = this.tagsByTool[tool] || [];
const isSubstitutable = tags.includes(element.tagName);
const isBlockElement = $.blockElements.includes(element.tagName.toLowerCase());
const containsAnotherToolTags = Array
.from(element.children)
.some(
({ tagName }) => tags.includes(tagName) && !toolTags.includes(tagName)
);
const containsBlockElements = Array.from(element.children).some(
({ tagName }) => $.blockElements.includes(tagName.toLowerCase())
);
/** Append inline elements to previous fragment */
if (!isBlockElement && !isSubstitutable && !containsAnotherToolTags) {
destNode.appendChild(element);
return [...nodes, destNode];
}
if (
(isSubstitutable && !containsAnotherToolTags) ||
(isBlockElement && !containsBlockElements && !containsAnotherToolTags)
) {
return [...nodes, destNode, element];
}
}
/** /**
* Recursively divide HTML string to two types of nodes: * Recursively divide HTML string to two types of nodes:
* 1. Block element * 1. Block element
* 2. Document Fragments contained text and markup tags like a, b, i etc. * 2. Document Fragments contained text and markup tags like a, b, i etc.
* *
* @param {Node} wrapper * @param {Node} wrapper - wrapper of paster HTML content
*
* @returns {Node[]} * @returns {Node[]}
*/ */
private getNodes(wrapper: Node): Node[] { private getNodes(wrapper: Node): Node[] {
const children = Array.from(wrapper.childNodes), const children = Array.from(wrapper.childNodes);
tags = Object.keys(this.toolsTags); let elementNodeProcessingResult: Node[] | void;
const reducer = (nodes: Node[], node: Node): Node[] => { const reducer = (nodes: Node[], node: Node): Node[] => {
if ($.isEmpty(node) && !$.isSingleTag(node as HTMLElement)) { if ($.isEmpty(node) && !$.isSingleTag(node as HTMLElement)) {
@ -767,34 +836,10 @@ export default class Paste extends Module {
* 2. Check if it contains another block or substitutable elements * 2. Check if it contains another block or substitutable elements
*/ */
case Node.ELEMENT_NODE: case Node.ELEMENT_NODE:
const element = node as HTMLElement; elementNodeProcessingResult = this.processElementNode(node, nodes, destNode);
const {tool = ''} = this.toolsTags[element.tagName] || {}; if (elementNodeProcessingResult) {
const toolTags = this.tagsByTool[tool] || []; return elementNodeProcessingResult;
const isSubstitutable = tags.includes(element.tagName);
const isBlockElement = $.blockElements.includes(element.tagName.toLowerCase());
const containsAnotherToolTags = Array
.from(element.children)
.some(
({tagName}) => tags.includes(tagName) && !toolTags.includes(tagName),
);
const containsBlockElements = Array.from(element.children).some(
({tagName}) => $.blockElements.includes(tagName.toLowerCase()),
);
/** Append inline elements to previous fragment */
if (!isBlockElement && !isSubstitutable && !containsAnotherToolTags) {
destNode.appendChild(element);
return [...nodes, destNode];
}
if (
(isSubstitutable && !containsAnotherToolTags) ||
(isBlockElement && !containsBlockElements && !containsAnotherToolTags )
) {
return [...nodes, destNode, element];
} }
break; break;
@ -803,6 +848,7 @@ export default class Paste extends Module {
*/ */
case Node.TEXT_NODE: case Node.TEXT_NODE:
destNode.appendChild(node); destNode.appendChild(node);
return [...nodes, destNode]; return [...nodes, destNode];
default: default:
@ -818,8 +864,8 @@ export default class Paste extends Module {
/** /**
* Compose paste event with passed type and detail * Compose paste event with passed type and detail
* *
* @param {string} type * @param {string} type - event type
* @param {PasteEventDetail} detail * @param {PasteEventDetail} detail - event detail
*/ */
private composePasteEvent(type: string, detail: PasteEventDetail): PasteEvent { private composePasteEvent(type: string, detail: PasteEventDetail): PasteEvent {
return new CustomEvent(type, { return new CustomEvent(type, {

View file

@ -11,12 +11,16 @@ import $ from '../dom';
import SelectionUtils from '../selection'; import SelectionUtils from '../selection';
import Block from '../block'; import Block from '../block';
/**
*
*/
export default class RectangleSelection extends Module { export default class RectangleSelection extends Module {
/** /**
* CSS classes for the Block * CSS classes for the Block
* @return {{wrapper: string, content: string}} *
* @returns {{wrapper: string, content: string}}
*/ */
static get CSS() { public static get CSS(): {[name: string]: string} {
return { return {
overlay: 'codex-editor-overlay', overlay: 'codex-editor-overlay',
overlayContainer: 'codex-editor-overlay__container', overlayContainer: 'codex-editor-overlay__container',
@ -28,9 +32,10 @@ export default class RectangleSelection extends Module {
/** /**
* Using the selection rectangle * Using the selection rectangle
*
* @type {boolean} * @type {boolean}
*/ */
private isRectSelectionActivated: boolean = false; private isRectSelectionActivated = false;
/** /**
* Speed of Scrolling * Speed of Scrolling
@ -56,12 +61,12 @@ export default class RectangleSelection extends Module {
/** /**
* Mouse is clamped * Mouse is clamped
*/ */
private mousedown: boolean = false; private mousedown = false;
/** /**
* Is scrolling now * Is scrolling now
*/ */
private isScrolling: boolean = false; private isScrolling = false;
/** /**
* Mouse is in scroll zone * Mouse is in scroll zone
@ -71,10 +76,10 @@ export default class RectangleSelection extends Module {
/** /**
* Coords of rect * Coords of rect
*/ */
private startX: number = 0; private startX = 0;
private startY: number = 0; private startY = 0;
private mouseX: number = 0; private mouseX = 0;
private mouseY: number = 0; private mouseY = 0;
/** /**
* Selected blocks * Selected blocks
@ -96,8 +101,8 @@ export default class RectangleSelection extends Module {
* Creating rect and hang handlers * Creating rect and hang handlers
*/ */
public prepare(): void { public prepare(): void {
const {Listeners} = this.Editor; const { Listeners } = this.Editor;
const {container} = this.genHTML(); const { container } = this.genHTML();
Listeners.on(container, 'mousedown', (event: MouseEvent) => { Listeners.on(container, 'mousedown', (event: MouseEvent) => {
if (event.button !== this.MAIN_MOUSE_BUTTON) { if (event.button !== this.MAIN_MOUSE_BUTTON) {
@ -127,10 +132,11 @@ export default class RectangleSelection extends Module {
/** /**
* Init rect params * Init rect params
*
* @param {number} pageX - X coord of mouse * @param {number} pageX - X coord of mouse
* @param {number} pageY - Y coord of mouse * @param {number} pageY - Y coord of mouse
*/ */
public startSelection(pageX, pageY) { public startSelection(pageX, pageY): void {
const elemWhereSelectionStart = document.elementFromPoint(pageX - window.pageXOffset, pageY - window.pageYOffset); const elemWhereSelectionStart = document.elementFromPoint(pageX - window.pageXOffset, pageY - window.pageYOffset);
/** /**
@ -152,7 +158,7 @@ export default class RectangleSelection extends Module {
]; ];
const startsInsideEditor = elemWhereSelectionStart.closest('.' + this.Editor.UI.CSS.editorWrapper); const startsInsideEditor = elemWhereSelectionStart.closest('.' + this.Editor.UI.CSS.editorWrapper);
const startsInSelectorToAvoid = selectorsToAvoid.some(((selector) => !!elemWhereSelectionStart.closest(selector))); const startsInSelectorToAvoid = selectorsToAvoid.some((selector) => !!elemWhereSelectionStart.closest(selector));
/** /**
* If selection starts outside of the editor or inside the blocks or on Editor UI elements, do not handle it * If selection starts outside of the editor or inside the blocks or on Editor UI elements, do not handle it
@ -169,7 +175,7 @@ export default class RectangleSelection extends Module {
/** /**
* Clear all params to end selection * Clear all params to end selection
*/ */
public endSelection() { public endSelection(): void {
this.mousedown = false; this.mousedown = false;
this.startX = 0; this.startX = 0;
this.startY = 0; this.startY = 0;
@ -179,22 +185,23 @@ export default class RectangleSelection extends Module {
/** /**
* is RectSelection Activated * is RectSelection Activated
*/ */
public isRectActivated() { public isRectActivated(): boolean {
return this.isRectSelectionActivated; return this.isRectSelectionActivated;
} }
/** /**
* Mark that selection is end * Mark that selection is end
*/ */
public clearSelection() { public clearSelection(): void {
this.isRectSelectionActivated = false; this.isRectSelectionActivated = false;
} }
/** /**
* Scroll If mouse in scroll zone * Scroll If mouse in scroll zone
*
* @param {number} clientY - Y coord of mouse * @param {number} clientY - Y coord of mouse
*/ */
private scrollByZones(clientY) { private scrollByZones(clientY): void {
this.inScrollZone = null; this.inScrollZone = null;
if (clientY <= this.HEIGHT_OF_SCROLL_ZONE) { if (clientY <= this.HEIGHT_OF_SCROLL_ZONE) {
this.inScrollZone = this.TOP_SCROLL_ZONE; this.inScrollZone = this.TOP_SCROLL_ZONE;
@ -205,6 +212,7 @@ export default class RectangleSelection extends Module {
if (!this.inScrollZone) { if (!this.inScrollZone) {
this.isScrolling = false; this.isScrolling = false;
return; return;
} }
@ -214,8 +222,13 @@ export default class RectangleSelection extends Module {
} }
} }
private genHTML() { /**
const {UI} = this.Editor; * Generates required HTML elements
*
* @returns {object<string, Element>}
*/
private genHTML(): {container: Element; overlay: Element} {
const { UI } = this.Editor;
const container = UI.nodes.holder.querySelector('.' + UI.CSS.editorWrapper); const container = UI.nodes.holder.querySelector('.' + UI.CSS.editorWrapper);
const overlay = $.make('div', RectangleSelection.CSS.overlay, {}); const overlay = $.make('div', RectangleSelection.CSS.overlay, {});
@ -227,6 +240,7 @@ export default class RectangleSelection extends Module {
container.appendChild(overlay); container.appendChild(overlay);
this.overlayRectangle = overlayRectangle as HTMLDivElement; this.overlayRectangle = overlayRectangle as HTMLDivElement;
return { return {
container, container,
overlay, overlay,
@ -235,13 +249,15 @@ export default class RectangleSelection extends Module {
/** /**
* Activates scrolling if blockSelection is active and mouse is in scroll zone * Activates scrolling if blockSelection is active and mouse is in scroll zone
*
* @param {number} speed - speed of scrolling * @param {number} speed - speed of scrolling
*/ */
private scrollVertical(speed) { private scrollVertical(speed): void {
if (!(this.inScrollZone && this.mousedown)) { if (!(this.inScrollZone && this.mousedown)) {
return; return;
} }
const lastOffset = window.pageYOffset; const lastOffset = window.pageYOffset;
window.scrollBy(0, speed); window.scrollBy(0, speed);
this.mouseY += window.pageYOffset - lastOffset; this.mouseY += window.pageYOffset - lastOffset;
setTimeout(() => { setTimeout(() => {
@ -251,9 +267,10 @@ export default class RectangleSelection extends Module {
/** /**
* Handles the change in the rectangle and its effect * Handles the change in the rectangle and its effect
* @param {MouseEvent} event *
* @param {MouseEvent} event - mouse event
*/ */
private changingRectangle(event) { private changingRectangle(event): void {
if (!this.mousedown) { if (!this.mousedown) {
return; return;
} }
@ -263,11 +280,12 @@ export default class RectangleSelection extends Module {
this.mouseY = event.pageY; this.mouseY = event.pageY;
} }
const {rightPos, leftPos, index} = this.genInfoForMouseSelection(); const { rightPos, leftPos, index } = this.genInfoForMouseSelection();
// There is not new block in selection // There is not new block in selection
const rectIsOnRighSideOfredactor = this.startX > rightPos && this.mouseX > rightPos; const rectIsOnRighSideOfredactor = this.startX > rightPos && this.mouseX > rightPos;
const rectISOnLeftSideOfRedactor = this.startX < leftPos && this.mouseX < leftPos; const rectISOnLeftSideOfRedactor = this.startX < leftPos && this.mouseX < leftPos;
this.rectCrossesBlocks = !(rectIsOnRighSideOfredactor || rectISOnLeftSideOfRedactor); this.rectCrossesBlocks = !(rectIsOnRighSideOfredactor || rectISOnLeftSideOfRedactor);
if (!this.isRectSelectionActivated) { if (!this.isRectSelectionActivated) {
@ -294,7 +312,7 @@ export default class RectangleSelection extends Module {
/** /**
* Shrink rect to singular point * Shrink rect to singular point
*/ */
private shrinkRectangleToPoint() { private shrinkRectangleToPoint(): void {
this.overlayRectangle.style.left = `${this.startX - window.pageXOffset}px`; this.overlayRectangle.style.left = `${this.startX - window.pageXOffset}px`;
this.overlayRectangle.style.top = `${this.startY - window.pageYOffset}px`; this.overlayRectangle.style.top = `${this.startY - window.pageYOffset}px`;
this.overlayRectangle.style.bottom = `calc(100% - ${this.startY - window.pageYOffset}px`; this.overlayRectangle.style.bottom = `calc(100% - ${this.startY - window.pageYOffset}px`;
@ -304,17 +322,17 @@ export default class RectangleSelection extends Module {
/** /**
* Select or unselect all of blocks in array if rect is out or in selectable area * Select or unselect all of blocks in array if rect is out or in selectable area
*/ */
private inverseSelection() { private inverseSelection(): void {
const firstBlockInStack = this.Editor.BlockManager.getBlockByIndex(this.stackOfSelected[0]); const firstBlockInStack = this.Editor.BlockManager.getBlockByIndex(this.stackOfSelected[0]);
const isSelecteMode = firstBlockInStack.selected; const isSelectedMode = firstBlockInStack.selected;
if (this.rectCrossesBlocks && !isSelecteMode) { if (this.rectCrossesBlocks && !isSelectedMode) {
for (const it of this.stackOfSelected) { for (const it of this.stackOfSelected) {
this.Editor.BlockSelection.selectBlockByIndex(it); this.Editor.BlockSelection.selectBlockByIndex(it);
} }
} }
if (!this.rectCrossesBlocks && isSelecteMode) { if (!this.rectCrossesBlocks && isSelectedMode) {
for (const it of this.stackOfSelected) { for (const it of this.stackOfSelected) {
this.Editor.BlockSelection.unSelectBlockByIndex(it); this.Editor.BlockSelection.unSelectBlockByIndex(it);
} }
@ -324,7 +342,7 @@ export default class RectangleSelection extends Module {
/** /**
* Updates size of rectangle * Updates size of rectangle
*/ */
private updateRectangleSize() { private updateRectangleSize(): void {
// Depending on the position of the mouse relative to the starting point, // Depending on the position of the mouse relative to the starting point,
// change this.e distance from the desired edge of the screen*/ // change this.e distance from the desired edge of the screen*/
if (this.mouseY >= this.startY) { if (this.mouseY >= this.startY) {
@ -346,15 +364,17 @@ export default class RectangleSelection extends Module {
/** /**
* Collects information needed to determine the behavior of the rectangle * Collects information needed to determine the behavior of the rectangle
* @return {number} index - index next Block, leftPos - start of left border of Block, rightPos - right border *
* @returns {object} index - index next Block, leftPos - start of left border of Block, rightPos - right border
*/ */
private genInfoForMouseSelection() { private genInfoForMouseSelection(): {index: number; leftPos: number; rightPos: number} {
const widthOfRedactor = document.body.offsetWidth; const widthOfRedactor = document.body.offsetWidth;
const centerOfRedactor = widthOfRedactor / 2; const centerOfRedactor = widthOfRedactor / 2;
const Y = this.mouseY - window.pageYOffset; const Y = this.mouseY - window.pageYOffset;
const elementUnderMouse = document.elementFromPoint(centerOfRedactor, Y); const elementUnderMouse = document.elementFromPoint(centerOfRedactor, Y);
const blockInCurrentPos = this.Editor.BlockManager.getBlockByChildNode(elementUnderMouse); const blockInCurrentPos = this.Editor.BlockManager.getBlockByChildNode(elementUnderMouse);
let index; let index;
if (blockInCurrentPos !== undefined) { if (blockInCurrentPos !== undefined) {
index = this.Editor.BlockManager.blocks.findIndex((block) => block.holder === blockInCurrentPos.holder); index = this.Editor.BlockManager.blocks.findIndex((block) => block.holder === blockInCurrentPos.holder);
} }
@ -372,9 +392,10 @@ export default class RectangleSelection extends Module {
/** /**
* Select block with index index * Select block with index index
*
* @param index - index of block in redactor * @param index - index of block in redactor
*/ */
private addBlockInSelection(index) { private addBlockInSelection(index): void {
if (this.rectCrossesBlocks) { if (this.rectCrossesBlocks) {
this.Editor.BlockSelection.selectBlockByIndex(index); this.Editor.BlockSelection.selectBlockByIndex(index);
} }
@ -383,9 +404,10 @@ export default class RectangleSelection extends Module {
/** /**
* Adds a block to the selection and determines which blocks should be selected * Adds a block to the selection and determines which blocks should be selected
*
* @param {object} index - index of new block in the reactor * @param {object} index - index of new block in the reactor
*/ */
private trySelectNextBlock(index) { private trySelectNextBlock(index): void {
const sameBlock = this.stackOfSelected[this.stackOfSelected.length - 1] === index; const sameBlock = this.stackOfSelected[this.stackOfSelected.length - 1] === index;
const sizeStack = this.stackOfSelected.length; const sizeStack = this.stackOfSelected.length;
const down = 1, up = -1, undef = 0; const down = 1, up = -1, undef = 0;
@ -395,10 +417,16 @@ export default class RectangleSelection extends Module {
} }
const blockNumbersIncrease = this.stackOfSelected[sizeStack - 1] - this.stackOfSelected[sizeStack - 2] > 0; const blockNumbersIncrease = this.stackOfSelected[sizeStack - 1] - this.stackOfSelected[sizeStack - 2] > 0;
const direction = sizeStack <= 1 ? undef : blockNumbersIncrease ? down : up;
const selectionInDownDurection = index > this.stackOfSelected[sizeStack - 1] && direction === down; let direction = undef;
if (sizeStack > 1) {
direction = blockNumbersIncrease ? down : up;
}
const selectionInDownDirection = index > this.stackOfSelected[sizeStack - 1] && direction === down;
const selectionInUpDirection = index < this.stackOfSelected[sizeStack - 1] && direction === up; const selectionInUpDirection = index < this.stackOfSelected[sizeStack - 1] && direction === up;
const generalSelection = selectionInDownDurection || selectionInUpDirection || direction === undef; const generalSelection = selectionInDownDirection || selectionInUpDirection || direction === undef;
const reduction = !generalSelection; const reduction = !generalSelection;
// When the selection is too fast, some blocks do not have time to be noticed. Fix it. // When the selection is too fast, some blocks do not have time to be noticed. Fix it.
@ -409,6 +437,7 @@ export default class RectangleSelection extends Module {
for (ind; ind <= index; ind++) { for (ind; ind <= index; ind++) {
this.addBlockInSelection(ind); this.addBlockInSelection(ind);
} }
return; return;
} }
@ -417,6 +446,7 @@ export default class RectangleSelection extends Module {
for (let ind = this.stackOfSelected[sizeStack - 1] - 1; ind >= index; ind--) { for (let ind = this.stackOfSelected[sizeStack - 1] - 1; ind >= index; ind--) {
this.addBlockInSelection(ind); this.addBlockInSelection(ind);
} }
return; return;
} }
@ -429,9 +459,9 @@ export default class RectangleSelection extends Module {
// cmp for different directions // cmp for different directions
if (index > this.stackOfSelected[sizeStack - 1]) { if (index > this.stackOfSelected[sizeStack - 1]) {
cmp = () => index > this.stackOfSelected[i]; cmp = (): boolean => index > this.stackOfSelected[i];
} else { } else {
cmp = () => index < this.stackOfSelected[i]; cmp = (): boolean => index < this.stackOfSelected[i];
} }
// Remove blocks missed due to speed. // Remove blocks missed due to speed.
@ -443,6 +473,5 @@ export default class RectangleSelection extends Module {
this.stackOfSelected.pop(); this.stackOfSelected.pop();
i--; i--;
} }
return;
} }
} }

View file

@ -1,8 +1,8 @@
import Module from '../__module'; import Module from '../__module';
/* eslint-disable import/no-duplicates */
import * as _ from '../utils'; import * as _ from '../utils';
import {ChainData} from '../utils'; import { ChainData } from '../utils';
import {BlockToolData} from '../../../types'; import { BlockToolConstructable, OutputBlockData } from '../../../types';
import {BlockToolConstructable} from '../../../types/tools';
/** /**
* Editor.js Renderer Module * Editor.js Renderer Module
@ -14,9 +14,9 @@ import {BlockToolConstructable} from '../../../types/tools';
*/ */
export default class Renderer extends Module { export default class Renderer extends Module {
/** /**
* @typedef {Object} RendererBlocks * @typedef {object} RendererBlocks
* @property {String} type - tool name * @property {string} type - tool name
* @property {Object} data - tool data * @property {object} data - tool data
*/ */
/** /**
@ -41,10 +41,11 @@ export default class Renderer extends Module {
/** /**
* Make plugin blocks from array of plugin`s data * Make plugin blocks from array of plugin`s data
* @param {RendererBlocks[]} blocks *
* @param {OutputBlockData[]} blocks - blocks to render
*/ */
public async render(blocks: BlockToolData[]): Promise<void> { public async render(blocks: OutputBlockData[]): Promise<void> {
const chainData = blocks.map((block) => ({function: () => this.insertBlock(block)})); 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[]);
@ -58,25 +59,25 @@ export default class Renderer extends Module {
* Add plugin instance to BlockManager * Add plugin instance to BlockManager
* Insert block to working zone * Insert block to working zone
* *
* @param {Object} item * @param {object} item - Block data to insert
* @returns {Promise<void>} * @returns {Promise<void>}
* @private
*/ */
public async insertBlock(item): Promise<void> { public async insertBlock(item: OutputBlockData): Promise<void> {
const { Tools, BlockManager } = this.Editor; const { Tools, BlockManager } = this.Editor;
const tool = item.type; const tool = item.type;
const data = item.data; const data = item.data;
const settings = item.settings;
if (tool in Tools.available) { if (tool in Tools.available) {
try { try {
BlockManager.insert(tool, data, settings); BlockManager.insert({
tool,
data,
});
} catch (error) { } catch (error) {
_.log(`Block «${tool}» skipped because of plugins error`, 'warn', data); _.log(`Block «${tool}» skipped because of plugins error`, 'warn', data);
throw Error(error); throw Error(error);
} }
} else { } else {
/** If Tool is unavailable, create stub Block for it */ /** If Tool is unavailable, create stub Block for it */
const stubData = { const stubData = {
savedData: { savedData: {
@ -93,7 +94,10 @@ export default class Renderer extends Module {
stubData.title = toolToolboxSettings.title || userToolboxSettings.title || stubData.title; stubData.title = toolToolboxSettings.title || userToolboxSettings.title || stubData.title;
} }
const stub = BlockManager.insert(Tools.stubTool, stubData, settings); const stub = BlockManager.insert({
tool: Tools.stubTool,
data: stubData,
});
stub.stretched = true; stub.stretched = true;

View file

@ -20,8 +20,8 @@ import Module from '../__module';
import * as _ from '../utils'; import * as _ from '../utils';
/** /**
* @typedef {Object} SanitizerConfig * @typedef {object} SanitizerConfig
* @property {Object} tags - define tags restrictions * @property {object} tags - define tags restrictions
* *
* @example * @example
* *
@ -36,8 +36,12 @@ import * as _ from '../utils';
*/ */
import HTMLJanitor from 'html-janitor'; import HTMLJanitor from 'html-janitor';
import {BlockToolData, InlineToolConstructable, SanitizerConfig} from '../../../types'; import { BlockToolData, InlineToolConstructable, SanitizerConfig } from '../../../types';
import { SavedData } from '../../types-internal/block-data';
/**
*
*/
export default class Sanitizer extends Module { export default class Sanitizer extends Module {
/** /**
* Memoize tools config * Memoize tools config
@ -54,12 +58,11 @@ export default class Sanitizer extends Module {
* *
* Enumerate blocks and clean data * Enumerate blocks and clean data
* *
* @param {{tool, data: BlockToolData}[]} blocksData[] * @param {Array<{tool, data: BlockToolData}>} blocksData - blocks' data to sanitize
*/ */
public sanitizeBlocks( public sanitizeBlocks(
blocksData: Array<{tool: string, data: BlockToolData}>, blocksData: Array<Pick<SavedData, 'data' | 'tool'>>
): Array<{tool: string, data: BlockToolData}> { ): Array<Pick<SavedData, 'data' | 'tool'>> {
return blocksData.map((block) => { return blocksData.map((block) => {
const toolConfig = this.composeToolConfig(block.tool); const toolConfig = this.composeToolConfig(block.tool);
@ -67,7 +70,7 @@ export default class Sanitizer extends Module {
return block; return block;
} }
block.data = this.deepSanitize(block.data, toolConfig); block.data = this.deepSanitize(block.data, toolConfig) as BlockToolData;
return block; return block;
}); });
@ -79,7 +82,7 @@ export default class Sanitizer extends Module {
* @param {BlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string * @param {BlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string
* @param {SanitizerConfig} rules - object with sanitizer rules * @param {SanitizerConfig} rules - object with sanitizer rules
*/ */
public deepSanitize(dataToSanitize: any, rules: SanitizerConfig): any { public deepSanitize(dataToSanitize: object | string, rules: SanitizerConfig): object | string {
/** /**
* BlockData It may contain 3 types: * BlockData It may contain 3 types:
* - Array * - Array
@ -105,6 +108,7 @@ export default class Sanitizer extends Module {
if (typeof dataToSanitize === 'string') { if (typeof dataToSanitize === 'string') {
return this.cleanOneItem(dataToSanitize, rules); return this.cleanOneItem(dataToSanitize, rules);
} }
return dataToSanitize; return dataToSanitize;
} }
} }
@ -116,10 +120,9 @@ export default class Sanitizer extends Module {
* @param {string} taintString - taint string * @param {string} taintString - taint string
* @param {SanitizerConfig} customConfig - allowed tags * @param {SanitizerConfig} customConfig - allowed tags
* *
* @return {string} clean HTML * @returns {string} clean HTML
*/ */
public clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string { public clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {
const sanitizerConfig = { const sanitizerConfig = {
tags: customConfig, tags: customConfig,
}; };
@ -128,15 +131,16 @@ export default class Sanitizer extends Module {
* API client can use custom config to manage sanitize process * API client can use custom config to manage sanitize process
*/ */
const sanitizerInstance = this.createHTMLJanitorInstance(sanitizerConfig); const sanitizerInstance = this.createHTMLJanitorInstance(sanitizerConfig);
return sanitizerInstance.clean(taintString); return sanitizerInstance.clean(taintString);
} }
/** /**
* Merge with inline tool config * Merge with inline tool config
* *
* @param {string} toolName * @param {string} toolName - tool name
* @param {SanitizerConfig} toolRules *
* @return {SanitizerConfig} * @returns {SanitizerConfig}
*/ */
public composeToolConfig(toolName: string): SanitizerConfig { public composeToolConfig(toolName: string): SanitizerConfig {
/** /**
@ -160,9 +164,11 @@ export default class Sanitizer extends Module {
const toolRules = toolClass.sanitize; const toolRules = toolClass.sanitize;
const toolConfig = {} as SanitizerConfig; const toolConfig = {} as SanitizerConfig;
for (const fieldName in toolRules) { for (const fieldName in toolRules) {
if (toolRules.hasOwnProperty(fieldName)) { if (Object.prototype.hasOwnProperty.call(toolRules, fieldName)) {
const rule = toolRules[fieldName]; const rule = toolRules[fieldName];
if (typeof rule === 'object') { if (typeof rule === 'object') {
toolConfig[fieldName] = Object.assign({}, baseConfig, rule); toolConfig[fieldName] = Object.assign({}, baseConfig, rule);
} else { } else {
@ -179,9 +185,11 @@ export default class Sanitizer extends Module {
* Returns Sanitizer config * Returns Sanitizer config
* When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools, * When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools,
* otherwise get only enabled * otherwise get only enabled
*
* @param {string} name - Inline Tool name
*/ */
public getInlineToolsConfig(name: string): SanitizerConfig { public getInlineToolsConfig(name: string): SanitizerConfig {
const {Tools} = this.Editor; const { Tools } = this.Editor;
const toolsConfig = Tools.getToolSettings(name); const toolsConfig = Tools.getToolSettings(name);
const enableInlineTools = toolsConfig.inlineToolbar || []; const enableInlineTools = toolsConfig.inlineToolbar || [];
@ -196,10 +204,10 @@ export default class Sanitizer extends Module {
/** /**
* getting only enabled * getting only enabled
*/ */
(enableInlineTools as string[]).map( (inlineToolName) => { (enableInlineTools as string[]).map((inlineToolName) => {
config = Object.assign( config = Object.assign(
config, config,
Tools.inline[inlineToolName][Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG], Tools.inline[inlineToolName][Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]
) as SanitizerConfig; ) as SanitizerConfig;
}); });
} }
@ -217,7 +225,7 @@ export default class Sanitizer extends Module {
* Return general config for all inline tools * Return general config for all inline tools
*/ */
public getAllInlineToolsConfig(): SanitizerConfig { public getAllInlineToolsConfig(): SanitizerConfig {
const {Tools} = this.Editor; const { Tools } = this.Editor;
if (this.inlineToolsConfigCache) { if (this.inlineToolsConfigCache) {
return this.inlineToolsConfigCache; return this.inlineToolsConfigCache;
@ -226,7 +234,7 @@ export default class Sanitizer extends Module {
const config: SanitizerConfig = {} as SanitizerConfig; const config: SanitizerConfig = {} as SanitizerConfig;
Object.entries(Tools.inline) Object.entries(Tools.inline)
.forEach( ([name, inlineTool]: [string, InlineToolConstructable]) => { .forEach(([, inlineTool]: [string, InlineToolConstructable]) => {
Object.assign(config, inlineTool[Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]); Object.assign(config, inlineTool[Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]);
}); });
@ -237,24 +245,26 @@ export default class Sanitizer extends Module {
/** /**
* Clean array * Clean array
* @param {array} array - [1, 2, {}, []] *
* @param {object} ruleForItem * @param {Array} array - [1, 2, {}, []]
* @param {SanitizerConfig} ruleForItem - sanitizer config for array
*/ */
private cleanArray(array: any[], ruleForItem: SanitizerConfig): any[] { private cleanArray(array: Array<object | string>, ruleForItem: SanitizerConfig): Array<object | string> {
return array.map( (arrayItem) => this.deepSanitize(arrayItem, ruleForItem)); return array.map((arrayItem) => this.deepSanitize(arrayItem, ruleForItem));
} }
/** /**
* Clean object * Clean object
*
* @param {object} object - {level: 0, text: 'adada', items: [1,2,3]}} * @param {object} object - {level: 0, text: 'adada', items: [1,2,3]}}
* @param {object} rules - { b: true } or true|false * @param {object} rules - { b: true } or true|false
* @return {object} * @returns {object}
*/ */
private cleanObject(object: any, rules: SanitizerConfig|{[field: string]: SanitizerConfig}): any { private cleanObject(object: object, rules: SanitizerConfig|{[field: string]: SanitizerConfig}): object {
const cleanData = {}; const cleanData = {};
for (const fieldName in object) { for (const fieldName in object) {
if (!object.hasOwnProperty(fieldName)) { if (!Object.prototype.hasOwnProperty.call(object, fieldName)) {
continue; continue;
} }
@ -269,13 +279,17 @@ export default class Sanitizer extends Module {
cleanData[fieldName] = this.deepSanitize(currentIterationItem, ruleForItem as SanitizerConfig); cleanData[fieldName] = this.deepSanitize(currentIterationItem, ruleForItem as SanitizerConfig);
} }
return cleanData; return cleanData;
} }
/** /**
* @param {string} taintString * Clean primitive value
* @param {SanitizerConfig|boolean} rule *
* @return {string} * @param {string} taintString - string to clean
* @param {SanitizerConfig|boolean} rule - sanitizer rule
*
* @returns {string}
*/ */
private cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string { private cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string {
if (typeof rule === 'object') { if (typeof rule === 'object') {
@ -291,7 +305,8 @@ export default class Sanitizer extends Module {
* Check if passed item is a HTML Janitor rule: * Check if passed item is a HTML Janitor rule:
* { a : true }, {}, false, true, function(){} correct rules * { a : true }, {}, false, true, function(){} correct rules
* undefined, null, 0, 1, 2 not a rules * undefined, null, 0, 1, 2 not a rules
* @param config *
* @param {SanitizerConfig} config - config to check
*/ */
private isRule(config: SanitizerConfig): boolean { private isRule(config: SanitizerConfig): boolean {
return typeof config === 'object' || typeof config === 'boolean' || typeof config === 'function'; return typeof config === 'object' || typeof config === 'boolean' || typeof config === 'function';
@ -302,8 +317,9 @@ export default class Sanitizer extends Module {
* Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere * Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses Default configuration * At least, if there is no config overrides, that API uses Default configuration
* *
* @uses https://www.npmjs.com/package/html-janitor * @see {@link https://www.npmjs.com/package/html-janitor}
* @license https://github.com/guardian/html-janitor/blob/master/LICENSE * @license Apache-2.0
* @see {@link https://github.com/guardian/html-janitor/blob/master/LICENSE}
* *
* @param {SanitizerConfig} config - sanitizer extension * @param {SanitizerConfig} config - sanitizer extension
*/ */
@ -311,6 +327,7 @@ export default class Sanitizer extends Module {
if (config) { if (config) {
return new HTMLJanitor(config); return new HTMLJanitor(config);
} }
return null; return null;
} }
} }

View file

@ -6,8 +6,8 @@
* @version 2.0.0 * @version 2.0.0
*/ */
import Module from '../__module'; import Module from '../__module';
import {OutputData} from '../../../types'; import { OutputData } from '../../../types';
import {ValidatedData} from '../../types-internal/block-data'; import { ValidatedData } from '../../types-internal/block-data';
import Block from '../block'; import Block from '../block';
import * as _ from '../utils'; import * as _ from '../utils';
@ -18,17 +18,18 @@ declare const VERSION: string;
* *
* @typedef {Saver} Saver * @typedef {Saver} Saver
* @property {Element} html - Editor HTML content * @property {Element} html - Editor HTML content
* @property {String} json - Editor JSON output * @property {string} json - Editor JSON output
*/ */
export default class Saver extends Module { export default class Saver extends Module {
/** /**
* Composes new chain of Promises to fire them alternatelly * Composes new chain of Promises to fire them alternatelly
* @return {OutputData} *
* @returns {OutputData}
*/ */
public async save(): Promise<OutputData> { public async save(): Promise<OutputData> {
const {BlockManager, Sanitizer, ModificationsObserver} = this.Editor; const { BlockManager, Sanitizer, ModificationsObserver } = this.Editor;
const blocks = BlockManager.blocks, const blocks = BlockManager.blocks,
chainData = []; chainData = [];
/** /**
* Disable modifications observe while saving * Disable modifications observe while saving
@ -36,7 +37,7 @@ export default class Saver extends Module {
ModificationsObserver.disable(); ModificationsObserver.disable();
blocks.forEach((block: Block) => { blocks.forEach((block: Block) => {
chainData.push(this.getSavedData(block)); chainData.push(this.getSavedData(block));
}); });
const extractedData = await Promise.all(chainData); const extractedData = await Promise.all(chainData);
@ -49,20 +50,25 @@ export default class Saver extends Module {
/** /**
* Saves and validates * Saves and validates
*
* @param {Block} block - Editor's Tool * @param {Block} block - Editor's Tool
* @return {ValidatedData} - Tool's validated data * @returns {ValidatedData} - Tool's validated data
*/ */
private async getSavedData(block: Block): Promise<ValidatedData> { private async getSavedData(block: Block): Promise<ValidatedData> {
const blockData = await block.save(); const blockData = await block.save();
const isValid = blockData && await block.validate(blockData.data); const isValid = blockData && await block.validate(blockData.data);
return {...blockData, isValid}; return {
...blockData,
isValid,
};
} }
/** /**
* Creates output object with saved data, time and version of editor * Creates output object with saved data, time and version of editor
* @param {ValidatedData} allExtractedData *
* @return {OutputData} * @param {ValidatedData} allExtractedData - data extracted from Blocks
* @returns {OutputData}
*/ */
private makeOutput(allExtractedData): OutputData { private makeOutput(allExtractedData): OutputData {
let totalTime = 0; let totalTime = 0;
@ -70,7 +76,7 @@ export default class Saver extends Module {
_.log('[Editor.js saving]:', 'groupCollapsed'); _.log('[Editor.js saving]:', 'groupCollapsed');
allExtractedData.forEach(({tool, data, time, isValid}) => { allExtractedData.forEach(({ tool, data, time, isValid }) => {
totalTime += time; totalTime += time;
/** /**
@ -85,12 +91,14 @@ export default class Saver extends Module {
} else { } else {
_.log(`Block «${tool}» skipped because saved data is invalid`); _.log(`Block «${tool}» skipped because saved data is invalid`);
_.log(undefined, 'groupEnd'); _.log(undefined, 'groupEnd');
return; return;
} }
/** If it was stub Block, get original data */ /** If it was stub Block, get original data */
if (tool === this.Editor.Tools.stubTool) { if (tool === this.Editor.Tools.stubTool) {
blocks.push(data); blocks.push(data);
return; return;
} }

View file

@ -1,10 +1,17 @@
import Shortcut from '@codexteam/shortcuts'; import Shortcut from '@codexteam/shortcuts';
/**
* Contains keyboard and mouse events binded on each Block by Block Manager
*/
import Module from '../__module';
/** /**
* ShortcutData interface * ShortcutData interface
* Each shortcut must have name and handler * Each shortcut must have name and handler
* `name` is a shortcut, like 'CMD+K', 'CMD+B' etc * `name` is a shortcut, like 'CMD+K', 'CMD+B' etc
* `handler` is a callback * `handler` is a callback
*
* @interface ShortcutData
*/ */
export interface ShortcutData { export interface ShortcutData {
@ -20,11 +27,6 @@ export interface ShortcutData {
handler(event): void; handler(event): void;
} }
/**
* Contains keyboard and mouse events binded on each Block by Block Manager
*/
import Module from '../__module';
/** /**
* @class Shortcut * @class Shortcut
* @classdesc Allows to register new shortcut * @classdesc Allows to register new shortcut
@ -34,13 +36,15 @@ import Module from '../__module';
export default class Shortcuts extends Module { export default class Shortcuts extends Module {
/** /**
* All registered shortcuts * All registered shortcuts
*
* @type {Shortcut[]} * @type {Shortcut[]}
*/ */
private registeredShortcuts: Shortcut[] = []; private registeredShortcuts: Shortcut[] = [];
/** /**
* Register shortcut * Register shortcut
* @param {ShortcutData} shortcut *
* @param {ShortcutData} shortcut - shortcut options
*/ */
public add(shortcut: ShortcutData): void { public add(shortcut: ShortcutData): void {
const newShortcut = new Shortcut({ const newShortcut = new Shortcut({
@ -54,7 +58,8 @@ export default class Shortcuts extends Module {
/** /**
* Remove shortcut * Remove shortcut
* @param {ShortcutData} shortcut *
* @param {string} shortcut - shortcut name
*/ */
public remove(shortcut: string): void { public remove(shortcut: string): void {
const index = this.registeredShortcuts.findIndex((shc) => shc.name === shortcut); const index = this.registeredShortcuts.findIndex((shc) => shc.name === shortcut);

View file

@ -1,7 +1,8 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import Flipper, {FlipperOptions} from '../../flipper'; import Flipper, { FlipperOptions } from '../../flipper';
import * as _ from '../../utils'; import * as _ from '../../utils';
import SelectionUtils from '../../selection';
/** /**
* Block Settings * Block Settings
@ -15,12 +16,12 @@ import * as _ from '../../utils';
* |________________________| * |________________________|
*/ */
export default class BlockSettings extends Module { export default class BlockSettings extends Module {
/** /**
* Module Events * Module Events
* @return {{opened: string, closed: string}} *
* @returns {{opened: string, closed: string}}
*/ */
public get events(): {opened: string, closed: string} { public get events(): {opened: string; closed: string} {
return { return {
opened: 'block-settings-opened', opened: 'block-settings-opened',
closed: 'block-settings-closed', closed: 'block-settings-closed',
@ -29,9 +30,10 @@ export default class BlockSettings extends Module {
/** /**
* Block Settings CSS * Block Settings CSS
* @return {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}} *
* @returns {{wrapper, wrapperOpened, toolSettings, defaultSettings, button}}
*/ */
public get CSS() { public get CSS(): {[name: string]: string} {
return { return {
// Settings Panel // Settings Panel
wrapper: 'ce-settings', wrapper: 'ce-settings',
@ -41,13 +43,14 @@ export default class BlockSettings extends Module {
button: 'ce-settings__button', button: 'ce-settings__button',
focusedButton : 'ce-settings__button--focused', focusedButton: 'ce-settings__button--focused',
focusedButtonAnimated: 'ce-settings__button--focused-animated', focusedButtonAnimated: 'ce-settings__button--focused-animated',
}; };
} }
/** /**
* Is Block Settings opened or not * Is Block Settings opened or not
*
* @returns {boolean} * @returns {boolean}
*/ */
public get opened(): boolean { public get opened(): boolean {
@ -70,16 +73,20 @@ export default class BlockSettings extends Module {
/** /**
* Instance of class that responses for leafing buttons by arrows/tab * Instance of class that responses for leafing buttons by arrows/tab
*
* @type {Flipper|null} * @type {Flipper|null}
*/ */
private flipper: Flipper = null; private flipper: Flipper = null;
/**
* Page selection utils
*/
private selection: SelectionUtils = new SelectionUtils();
/** /**
* Panel with block settings with 2 sections: * Panel with block settings with 2 sections:
* - Tool's Settings * - Tool's Settings
* - Default Settings [Move, Remove, etc] * - Default Settings [Move, Remove, etc]
*
* @return {Element}
*/ */
public make(): void { public make(): void {
this.nodes.wrapper = $.make('div', this.CSS.wrapper); this.nodes.wrapper = $.make('div', this.CSS.wrapper);
@ -102,6 +109,12 @@ export default class BlockSettings extends Module {
public open(): void { public open(): void {
this.nodes.wrapper.classList.add(this.CSS.wrapperOpened); this.nodes.wrapper.classList.add(this.CSS.wrapperOpened);
/**
* If block settings contains any inputs, focus will be set there,
* so we need to save current selection to restore it after block settings is closed
*/
this.selection.save();
/** /**
* Highlight content of a Block we are working with * Highlight content of a Block we are working with
*/ */
@ -129,6 +142,19 @@ export default class BlockSettings extends Module {
public close(): void { public close(): void {
this.nodes.wrapper.classList.remove(this.CSS.wrapperOpened); this.nodes.wrapper.classList.remove(this.CSS.wrapperOpened);
/**
* If selection is at editor on Block Settings closing,
* it means that caret placed at some editable element inside the Block Settings.
* Previously we have saved the selection, then open the Block Settings and set caret to the input
*
* So, we need to restore selection back to Block after closing the Block Settings
*/
if (!SelectionUtils.isAtEditor) {
this.selection.restore();
}
this.selection.clearSaved();
/** Clear settings */ /** Clear settings */
this.nodes.toolSettings.innerHTML = ''; this.nodes.toolSettings.innerHTML = '';
this.nodes.defaultSettings.innerHTML = ''; this.nodes.defaultSettings.innerHTML = '';
@ -145,9 +171,12 @@ export default class BlockSettings extends Module {
/** /**
* Returns Tools Settings and Default Settings * Returns Tools Settings and Default Settings
* @return {HTMLElement[]} *
* @returns {HTMLElement[]}
*/ */
public get blockTunesButtons(): HTMLElement[] { public get blockTunesButtons(): HTMLElement[] {
const { StylesAPI } = this.Editor;
/** /**
* Return from cache * Return from cache
* if exists * if exists
@ -156,7 +185,10 @@ export default class BlockSettings extends Module {
return this.buttons; return this.buttons;
} }
const toolSettings = this.nodes.toolSettings.querySelectorAll(`.${this.Editor.StylesAPI.classes.settingsButton}`); const toolSettings = this.nodes.toolSettings.querySelectorAll(
// Select buttons and inputs
`.${StylesAPI.classes.settingsButton}, ${$.allInputsSelector}`
);
const defaultSettings = this.nodes.defaultSettings.querySelectorAll(`.${this.CSS.button}`); const defaultSettings = this.nodes.defaultSettings.querySelectorAll(`.${this.CSS.button}`);
toolSettings.forEach((item) => { toolSettings.forEach((item) => {
@ -193,14 +225,26 @@ export default class BlockSettings extends Module {
private enableFlipper(): void { private enableFlipper(): void {
this.flipper = new Flipper({ this.flipper = new Flipper({
focusedItemClass: this.CSS.focusedButton, focusedItemClass: this.CSS.focusedButton,
activateCallback: () => { /**
* @param {HTMLElement} focusedItem - activated Tune
*/
activateCallback: (focusedItem) => {
/**
* If focused item is editable element, close block settings
*/
if (focusedItem && $.canSetCaret(focusedItem)) {
this.close();
return;
}
/** /**
* Restoring focus on current Block after settings clicked. * Restoring focus on current Block after settings clicked.
* For example, when H3 changed to H2 DOM Elements replaced, so we need to focus a new one * For example, when H3 changed to H2 DOM Elements replaced, so we need to focus a new one
*/ */
_.delay( () => { _.delay(() => {
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock); this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
}, 10)(); }, 50)();
}, },
} as FlipperOptions); } as FlipperOptions);
} }

View file

@ -1,10 +1,11 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import {BlockToolConstructable} from '../../../../types'; import { BlockToolConstructable } from '../../../../types';
import * as _ from '../../utils'; import * as _ from '../../utils';
import {SavedData} from '../../../types-internal/block-data'; import { SavedData } from '../../../types-internal/block-data';
import Block from '../../block';
import Flipper from '../../flipper'; import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/** /**
* Block Converter * Block Converter
@ -23,8 +24,8 @@ export default class ConversionToolbar extends Module {
conversionToolHidden: 'ce-conversion-tool--hidden', conversionToolHidden: 'ce-conversion-tool--hidden',
conversionToolIcon: 'ce-conversion-tool__icon', conversionToolIcon: 'ce-conversion-tool__icon',
conversionToolFocused : 'ce-conversion-tool--focused', conversionToolFocused: 'ce-conversion-tool--focused',
conversionToolActive : 'ce-conversion-tool--active', conversionToolActive: 'ce-conversion-tool--active',
}; };
} }
@ -38,9 +39,10 @@ export default class ConversionToolbar extends Module {
/** /**
* Conversion Toolbar open/close state * Conversion Toolbar open/close state
*
* @type {boolean} * @type {boolean}
*/ */
public opened: boolean = false; public opened = false;
/** /**
* Available tools * Available tools
@ -49,6 +51,7 @@ export default class ConversionToolbar extends Module {
/** /**
* Instance of class that responses for leafing buttons by arrows/tab * Instance of class that responses for leafing buttons by arrows/tab
*
* @type {Flipper|null} * @type {Flipper|null}
*/ */
private flipper: Flipper = null; private flipper: Flipper = null;
@ -66,7 +69,7 @@ export default class ConversionToolbar extends Module {
this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools); this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);
const label = $.make('div', ConversionToolbar.CSS.conversionToolbarLabel, { const label = $.make('div', ConversionToolbar.CSS.conversionToolbarLabel, {
textContent: 'Convert to', textContent: I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'),
}); });
/** /**
@ -87,7 +90,8 @@ export default class ConversionToolbar extends Module {
/** /**
* Toggle conversion dropdown visibility * Toggle conversion dropdown visibility
* @param {function} [togglingCallback] callback that will accept opening state *
* @param {Function} [togglingCallback] callback that will accept opening state
*/ */
public toggle(togglingCallback?: (openedState: boolean) => void): void { public toggle(togglingCallback?: (openedState: boolean) => void): void {
if (!this.opened) { if (!this.opened) {
@ -152,11 +156,12 @@ export default class ConversionToolbar extends Module {
* Replaces one Block with another * Replaces one Block with another
* For that Tools must provide import/export methods * For that Tools must provide import/export methods
* *
* @param {string} replacingToolName * @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 * At first, we get current Block data
*
* @type {BlockToolConstructable} * @type {BlockToolConstructable}
*/ */
const currentBlockClass = this.Editor.BlockManager.currentBlock.class; const currentBlockClass = this.Editor.BlockManager.currentBlock.class;
@ -175,6 +180,7 @@ export default class ConversionToolbar extends Module {
/** /**
* Getting a class of replacing Tool * Getting a class of replacing Tool
*
* @type {BlockToolConstructable} * @type {BlockToolConstructable}
*/ */
const replacingTool = this.Editor.Tools.toolsClasses[replacingToolName] as BlockToolConstructable; const replacingTool = this.Editor.Tools.toolsClasses[replacingToolName] as BlockToolConstructable;
@ -186,7 +192,7 @@ export default class ConversionToolbar extends Module {
* *
* In both cases returning value must be a string * In both cases returning value must be a string
*/ */
let exportData: string = ''; let exportData = '';
const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export; const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export;
if (typeof exportProp === 'function') { if (typeof exportProp === 'function') {
@ -196,6 +202,7 @@ export default class ConversionToolbar extends Module {
} else { } else {
_.log('Conversion «export» property must be a string or function. ' + _.log('Conversion «export» property must be a string or function. ' +
'String means key of saved data object to export. Function should export processed string to export.'); 'String means key of saved data object to export. Function should export processed string to export.');
return; return;
} }
@ -204,7 +211,7 @@ export default class ConversionToolbar extends Module {
*/ */
const cleaned: string = this.Editor.Sanitizer.clean( const cleaned: string = this.Editor.Sanitizer.clean(
exportData, exportData,
replacingTool.sanitize, replacingTool.sanitize
); );
/** /**
@ -222,10 +229,14 @@ export default class ConversionToolbar extends Module {
} else { } else {
_.log('Conversion «import» property must be a string or function. ' + _.log('Conversion «import» property must be a string or function. ' +
'String means key of tool data to import. Function accepts a imported string and return composed tool data.'); 'String means key of tool data to import. Function accepts a imported string and return composed tool data.');
return; return;
} }
this.Editor.BlockManager.replace(replacingToolName, newBlockData); this.Editor.BlockManager.replace({
tool: replacingToolName,
data: newBlockData,
});
this.Editor.BlockSelection.clearSelection(); this.Editor.BlockSelection.clearSelection();
this.close(); this.close();
@ -244,7 +255,7 @@ export default class ConversionToolbar extends Module {
const tools = this.Editor.Tools.blockTools; const tools = this.Editor.Tools.blockTools;
for (const toolName in tools) { for (const toolName in tools) {
if (!tools.hasOwnProperty(toolName)) { if (!Object.prototype.hasOwnProperty.call(tools, toolName)) {
continue; continue;
} }
@ -273,6 +284,10 @@ export default class ConversionToolbar extends Module {
/** /**
* Add tool to the Conversion Toolbar * Add tool to the Conversion Toolbar
*
* @param {string} toolName - name of Tool to add
* @param {string} toolIcon - Tool icon
* @param {string} title - button title
*/ */
private addTool(toolName: string, toolIcon: string, title: string): void { private addTool(toolName: string, toolIcon: string, title: string): void {
const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]); const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);
@ -282,7 +297,7 @@ export default class ConversionToolbar extends Module {
icon.innerHTML = toolIcon; icon.innerHTML = toolIcon;
$.append(tool, icon); $.append(tool, icon);
$.append(tool, $.text(title || _.capitalize(toolName))); $.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, title || _.capitalize(toolName))));
$.append(this.nodes.tools, tool); $.append(this.nodes.tools, tool);
this.tools[toolName] = tool; this.tools[toolName] = tool;

View file

@ -1,6 +1,8 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import * as _ from '../../utils'; import * as _ from '../../utils';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/** /**
* *
@ -42,7 +44,7 @@ import * as _ from '../../utils';
* @classdesc Toolbar module * @classdesc Toolbar module
* *
* @typedef {Toolbar} Toolbar * @typedef {Toolbar} Toolbar
* @property {Object} nodes * @property {object} nodes - Toolbar nodes
* @property {Element} nodes.wrapper - Toolbar main element * @property {Element} nodes.wrapper - Toolbar main element
* @property {Element} nodes.content - Zone with Plus button and toolbox. * @property {Element} nodes.content - Zone with Plus button and toolbox.
* @property {Element} nodes.actions - Zone with Block Settings and Remove Button * @property {Element} nodes.actions - Zone with Block Settings and Remove Button
@ -59,23 +61,24 @@ export default class Toolbar extends Module {
* HTML Elements used for Toolbar UI * HTML Elements used for Toolbar UI
*/ */
public nodes: {[key: string]: HTMLElement} = { public nodes: {[key: string]: HTMLElement} = {
wrapper : null, wrapper: null,
content : null, content: null,
actions : null, actions: null,
// Content Zone // Content Zone
plusButton : null, plusButton: null,
// Actions Zone // Actions Zone
blockActionsButtons: null, blockActionsButtons: null,
settingsToggler : null, settingsToggler: null,
}; };
/** /**
* CSS styles * CSS styles
* @return {Object} *
* @returns {object}
*/ */
public get CSS() { public get CSS(): {[name: string]: string} {
return { return {
toolbar: 'ce-toolbar', toolbar: 'ce-toolbar',
content: 'ce-toolbar__content', content: 'ce-toolbar__content',
@ -104,7 +107,7 @@ export default class Toolbar extends Module {
/** /**
* Make Content Zone and Actions Zone * Make Content Zone and Actions Zone
*/ */
['content', 'actions'].forEach( (el) => { ['content', 'actions'].forEach((el) => {
this.nodes[el] = $.make('div', this.CSS[el]); this.nodes[el] = $.make('div', this.CSS[el]);
}); });
@ -130,7 +133,7 @@ export default class Toolbar extends Module {
*/ */
const tooltipContent = $.make('div'); const tooltipContent = $.make('div');
tooltipContent.appendChild(document.createTextNode('Add')); tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add')));
tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, { tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {
textContent: '⇥ Tab', textContent: '⇥ Tab',
})); }));
@ -149,16 +152,20 @@ export default class Toolbar extends Module {
* - Settings Panel * - Settings Panel
*/ */
this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons); this.nodes.blockActionsButtons = $.make('div', this.CSS.blockActionsButtons);
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler); this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
const settingsIcon = $.svg('dots', 8, 8); const settingsIcon = $.svg('dots', 8, 8);
$.append(this.nodes.settingsToggler, settingsIcon); $.append(this.nodes.settingsToggler, settingsIcon);
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler); $.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons); $.append(this.nodes.actions, this.nodes.blockActionsButtons);
this.Editor.Tooltip.onHover(this.nodes.settingsToggler, 'Click to tune', { this.Editor.Tooltip.onHover(
placement: 'top', this.nodes.settingsToggler,
}); I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
{
placement: 'top',
}
);
/** /**
* Make and append Settings Panel * Make and append Settings Panel
@ -179,9 +186,10 @@ export default class Toolbar extends Module {
/** /**
* Move Toolbar to the Current Block * Move Toolbar to the Current Block
* @param {Boolean} forceClose - force close Toolbar Settings and Toolbar *
* @param {boolean} forceClose - force close Toolbar Settings and Toolbar
*/ */
public move(forceClose: boolean = true): void { public move(forceClose = true): void {
if (forceClose) { if (forceClose) {
/** Close Toolbox when we move toolbar */ /** Close Toolbox when we move toolbar */
this.Editor.Toolbox.close(); this.Editor.Toolbox.close();
@ -222,6 +230,7 @@ export default class Toolbar extends Module {
/** /**
* Open Toolbar with Plus Button and Actions * Open Toolbar with Plus Button and Actions
*
* @param {boolean} withBlockActions - by default, Toolbar opens with Block Actions. * @param {boolean} withBlockActions - by default, Toolbar opens with Block Actions.
* This flag allows to open Toolbar without Actions. * This flag allows to open Toolbar without Actions.
* @param {boolean} needToCloseToolbox - by default, Toolbar will be moved with opening * @param {boolean} needToCloseToolbox - by default, Toolbar will be moved with opening
@ -229,7 +238,7 @@ export default class Toolbar extends Module {
* with closing Toolbox and Block Settings * with closing Toolbox and Block Settings
* This flag allows to open Toolbar with Toolbox * This flag allows to open Toolbar with Toolbox
*/ */
public open(withBlockActions: boolean = true, needToCloseToolbox: boolean = true): void { public open(withBlockActions = true, needToCloseToolbox = true): void {
_.delay(() => { _.delay(() => {
this.move(needToCloseToolbox); this.move(needToCloseToolbox);
this.nodes.wrapper.classList.add(this.CSS.toolbarOpened); this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);
@ -244,7 +253,8 @@ export default class Toolbar extends Module {
/** /**
* returns toolbar opened state * returns toolbar opened state
* @return {Boolean} *
* @returns {boolean}
*/ */
public get opened(): boolean { public get opened(): boolean {
return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened); return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);
@ -264,12 +274,13 @@ export default class Toolbar extends Module {
/** /**
* Plus Button public methods * Plus Button public methods
* @return {{hide: function(): void, show: function(): void}} *
* @returns {{hide: function(): void, show: function(): void}}
*/ */
public get plusButton(): {hide: () => void, show: () => void} { public get plusButton(): {hide: () => void; show: () => void} {
return { return {
hide: () => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden), hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
show: () => { show: (): void => {
if (this.Editor.Toolbox.isEmpty) { if (this.Editor.Toolbox.isEmpty) {
return; return;
} }
@ -280,14 +291,15 @@ export default class Toolbar extends Module {
/** /**
* Block actions appearance manipulations * Block actions appearance manipulations
* @return {{hide: function(): void, show: function(): void}} *
* @returns {{hide: function(): void, show: function(): void}}
*/ */
private get blockActions(): {hide: () => void, show: () => void} { private get blockActions(): {hide: () => void; show: () => void} {
return { return {
hide: () => { hide: (): void => {
this.nodes.actions.classList.remove(this.CSS.actionsOpened); this.nodes.actions.classList.remove(this.CSS.actionsOpened);
}, },
show : () => { show: (): void => {
this.nodes.actions.classList.add(this.CSS.actionsOpened); this.nodes.actions.classList.add(this.CSS.actionsOpened);
}, },
}; };
@ -295,7 +307,6 @@ export default class Toolbar extends Module {
/** /**
* Handler for Plus Button * Handler for Plus Button
* @param {MouseEvent} event
*/ */
private plusButtonClicked(): void { private plusButtonClicked(): void {
this.Editor.Toolbox.toggle(); this.Editor.Toolbox.toggle();

View file

@ -1,10 +1,11 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import SelectionUtils from '../../selection'; import SelectionUtils from '../../selection';
import * as _ from '../../utils'; import * as _ from '../../utils';
import {InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings} from '../../../../types'; import { InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings } from '../../../../types';
import Flipper from '../../flipper'; import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/** /**
* Inline toolbar with actions that modifies selected text fragment * Inline toolbar with actions that modifies selected text fragment
@ -14,7 +15,6 @@ import Flipper from '../../flipper';
* |________________________| * |________________________|
*/ */
export default class InlineToolbar extends Module { export default class InlineToolbar extends Module {
/** /**
* CSS styles * CSS styles
*/ */
@ -37,19 +37,20 @@ export default class InlineToolbar extends Module {
/** /**
* State of inline toolbar * State of inline toolbar
*
* @type {boolean} * @type {boolean}
*/ */
public opened: boolean = false; public opened = false;
/** /**
* Inline Toolbar elements * Inline Toolbar elements
*/ */
private nodes: { private nodes: {
wrapper: HTMLElement, wrapper: HTMLElement;
buttons: HTMLElement, buttons: HTMLElement;
conversionToggler: HTMLElement, conversionToggler: HTMLElement;
conversionTogglerContent: HTMLElement, conversionTogglerContent: HTMLElement;
actions: HTMLElement, actions: HTMLElement;
} = { } = {
wrapper: null, wrapper: null,
buttons: null, buttons: null,
@ -74,15 +75,17 @@ export default class InlineToolbar extends Module {
/** /**
* Buttons List * Buttons List
*
* @type {NodeList} * @type {NodeList}
*/ */
private buttonsList: NodeList = null; private buttonsList: NodeList = null;
/** /**
* Cache for Inline Toolbar width * Cache for Inline Toolbar width
*
* @type {number} * @type {number}
*/ */
private width: number = 0; private width = 0;
/** /**
* Instance of class that responses for leafing buttons by arrows/tab * Instance of class that responses for leafing buttons by arrows/tab
@ -92,15 +95,15 @@ export default class InlineToolbar extends Module {
/** /**
* Inline Toolbar Tools * Inline Toolbar Tools
* *
* @returns Map<string, InlineTool> * @returns {Map<string, InlineTool>}
*/ */
get tools(): Map<string, InlineTool> { public get tools(): Map<string, InlineTool> {
if (!this.toolsInstances || this.toolsInstances.size === 0) { if (!this.toolsInstances || this.toolsInstances.size === 0) {
const allTools = this.inlineTools; const allTools = this.inlineTools;
this.toolsInstances = new Map(); this.toolsInstances = new Map();
for (const tool in allTools) { for (const tool in allTools) {
if (allTools.hasOwnProperty(tool)) { if (Object.prototype.hasOwnProperty.call(allTools, tool)) {
this.toolsInstances.set(tool, allTools[tool]); this.toolsInstances.set(tool, allTools[tool]);
} }
} }
@ -112,7 +115,7 @@ export default class InlineToolbar extends Module {
/** /**
* Making DOM * Making DOM
*/ */
public make() { public make(): void {
this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar); this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);
this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper); this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);
this.nodes.actions = $.make('div', this.CSS.actionsWrapper); this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
@ -169,14 +172,16 @@ export default class InlineToolbar extends Module {
/** /**
* Shows Inline Toolbar if something is selected * Shows Inline Toolbar if something is selected
*
* @param {boolean} [needToClose] - pass true to close toolbar if it is not allowed. * @param {boolean} [needToClose] - pass true to close toolbar if it is not allowed.
* Avoid to use it just for closing IT, better call .close() clearly. * Avoid to use it just for closing IT, better call .close() clearly.
*/ */
public tryToShow(needToClose: boolean = false): void { public tryToShow(needToClose = false): void {
if (!this.allowedToShow()) { if (!this.allowedToShow()) {
if (needToClose) { if (needToClose) {
this.close(); this.close();
} }
return; return;
} }
@ -196,11 +201,11 @@ export default class InlineToolbar extends Module {
const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect(); const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
const newCoords = { const newCoords = {
x: selectionRect.x - wrapperOffset.left, x: selectionRect.x - wrapperOffset.left,
y: selectionRect.y y: selectionRect.y +
+ selectionRect.height selectionRect.height -
// + window.scrollY // + window.scrollY
- wrapperOffset.top wrapperOffset.top +
+ this.toolbarVerticalMargin, this.toolbarVerticalMargin,
}; };
/** /**
@ -222,12 +227,12 @@ export default class InlineToolbar extends Module {
*/ */
this.nodes.wrapper.classList.toggle( this.nodes.wrapper.classList.toggle(
this.CSS.inlineToolbarLeftOriented, this.CSS.inlineToolbarLeftOriented,
realLeftCoord < this.Editor.UI.contentRect.left, realLeftCoord < this.Editor.UI.contentRect.left
); );
this.nodes.wrapper.classList.toggle( this.nodes.wrapper.classList.toggle(
this.CSS.inlineToolbarRightOriented, this.CSS.inlineToolbarRightOriented,
realRightCoord > this.Editor.UI.contentRect.right, realRightCoord > this.Editor.UI.contentRect.right
); );
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px'; this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
@ -331,7 +336,7 @@ export default class InlineToolbar extends Module {
return false; return false;
} }
const target = !$.isElement(currentSelection.anchorNode ) const target = !$.isElement(currentSelection.anchorNode)
? currentSelection.anchorNode.parentElement ? currentSelection.anchorNode.parentElement
: currentSelection.anchorNode; : currentSelection.anchorNode;
@ -363,13 +368,14 @@ export default class InlineToolbar extends Module {
*/ */
private filterTools(): void { private filterTools(): void {
const currentSelection = SelectionUtils.get(), const currentSelection = SelectionUtils.get(),
currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement); currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);
const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name), const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name),
inlineToolbarSettings = toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS]; inlineToolbarSettings = toolSettings && toolSettings[this.Editor.Tools.USER_SETTINGS.ENABLED_INLINE_TOOLS];
/** /**
* All Inline Toolbar buttons * All Inline Toolbar buttons
*
* @type {HTMLElement[]} * @type {HTMLElement[]}
*/ */
const buttons = Array.from(this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`)) as HTMLElement[]; const buttons = Array.from(this.nodes.buttons.querySelectorAll(`.${this.CSS.inlineToolButton}`)) as HTMLElement[];
@ -448,7 +454,7 @@ export default class InlineToolbar extends Module {
}); });
}); });
this.Editor.Tooltip.onHover(this.nodes.conversionToggler, 'Convert to', { this.Editor.Tooltip.onHover(this.nodes.conversionToggler, I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'), {
placement: 'top', placement: 'top',
hidingDelay: 100, hidingDelay: 100,
}); });
@ -458,7 +464,7 @@ export default class InlineToolbar extends Module {
* Changes Conversion Dropdown content for current block's Tool * Changes Conversion Dropdown content for current block's Tool
*/ */
private setConversionTogglerContent(): void { private setConversionTogglerContent(): void {
const {BlockManager, Tools} = this.Editor; const { BlockManager, Tools } = this.Editor;
const toolName = BlockManager.currentBlock.name; const toolName = BlockManager.currentBlock.name;
/** /**
@ -478,11 +484,11 @@ export default class InlineToolbar extends Module {
const userToolboxSettings = toolSettings.toolbox || {}; const userToolboxSettings = toolSettings.toolbox || {};
this.nodes.conversionTogglerContent.innerHTML = this.nodes.conversionTogglerContent.innerHTML =
userToolboxSettings.icon userToolboxSettings.icon ||
|| toolboxSettings.icon toolboxSettings.icon ||
|| userToolboxSettings.title userToolboxSettings.title ||
|| toolboxSettings.title toolboxSettings.title ||
|| _.capitalize(toolName); _.capitalize(toolName);
} }
/** /**
@ -510,6 +516,9 @@ export default class InlineToolbar extends Module {
/** /**
* Add tool button and activate clicks * Add tool button and activate clicks
*
* @param {string} toolName - name of Tool to add
* @param {InlineTool} tool - Tool class instance
*/ */
private addTool(toolName: string, tool: InlineTool): void { private addTool(toolName: string, tool: InlineTool): void {
const { const {
@ -522,6 +531,7 @@ export default class InlineToolbar extends Module {
if (!button) { if (!button) {
_.log('Render method must return an instance of Node', 'warn', toolName); _.log('Render method must return an instance of Node', 'warn', toolName);
return; return;
} }
@ -530,6 +540,7 @@ export default class InlineToolbar extends Module {
if (typeof tool.renderActions === 'function') { if (typeof tool.renderActions === 'function') {
const actions = tool.renderActions(); const actions = tool.renderActions();
this.nodes.actions.appendChild(actions); this.nodes.actions.appendChild(actions);
} }
@ -551,23 +562,26 @@ export default class InlineToolbar extends Module {
*/ */
const internalTools: string[] = Object const internalTools: string[] = Object
.entries(Tools.internalTools) .entries(Tools.internalTools)
.filter(([name, toolClass]: [string, ToolConstructable | ToolSettings]) => { .filter(([, toolClass]: [string, ToolConstructable | ToolSettings]) => {
if (_.isFunction(toolClass)) { if (_.isFunction(toolClass)) {
return toolClass[Tools.INTERNAL_SETTINGS.IS_INLINE]; return toolClass[Tools.INTERNAL_SETTINGS.IS_INLINE];
} }
return (toolClass as ToolSettings).class[Tools.INTERNAL_SETTINGS.IS_INLINE]; return (toolClass as ToolSettings).class[Tools.INTERNAL_SETTINGS.IS_INLINE];
}) })
.map(([name]: [string, InlineToolConstructable | ToolSettings]) => name); .map(([ name ]: [string, InlineToolConstructable | ToolSettings]) => name);
/** /**
* 1) For internal tools, check public getter 'shortcut' * 1) For internal tools, check public getter 'shortcut'
* 2) For external tools, check tool's settings * 2) For external tools, check tool's settings
* 3) If shortcut is not set in settings, check Tool's public property
*/ */
if (internalTools.includes(toolName)) { if (internalTools.includes(toolName)) {
shortcut = this.inlineTools[toolName][Tools.INTERNAL_SETTINGS.SHORTCUT]; shortcut = this.inlineTools[toolName][Tools.INTERNAL_SETTINGS.SHORTCUT];
} else if (toolSettings && toolSettings[Tools.USER_SETTINGS.SHORTCUT]) { } else if (toolSettings && toolSettings[Tools.USER_SETTINGS.SHORTCUT]) {
shortcut = toolSettings[Tools.USER_SETTINGS.SHORTCUT]; shortcut = toolSettings[Tools.USER_SETTINGS.SHORTCUT];
} else if (tool.shortcut) {
shortcut = tool.shortcut;
} }
if (shortcut) { if (shortcut) {
@ -578,7 +592,10 @@ export default class InlineToolbar extends Module {
* Enable tooltip module on button * Enable tooltip module on button
*/ */
const tooltipContent = $.make('div'); const tooltipContent = $.make('div');
const toolTitle = Tools.toolsClasses[toolName][Tools.INTERNAL_SETTINGS.TITLE] || _.capitalize(toolName); const toolTitle = I18n.t(
I18nInternalNS.toolNames,
Tools.toolsClasses[toolName][Tools.INTERNAL_SETTINGS.TITLE] || _.capitalize(toolName)
);
tooltipContent.appendChild($.text(toolTitle)); tooltipContent.appendChild($.text(toolTitle));
@ -592,11 +609,11 @@ export default class InlineToolbar extends Module {
placement: 'top', placement: 'top',
hidingDelay: 100, hidingDelay: 100,
}); });
} }
/** /**
* Enable Tool shortcut with Editor Shortcuts Module * Enable Tool shortcut with Editor Shortcuts Module
*
* @param {InlineTool} tool - Tool instance * @param {InlineTool} tool - Tool instance
* @param {string} shortcut - shortcut according to the ShortcutData Module format * @param {string} shortcut - shortcut according to the ShortcutData Module format
*/ */
@ -604,7 +621,7 @@ export default class InlineToolbar extends Module {
this.Editor.Shortcuts.add({ this.Editor.Shortcuts.add({
name: shortcut, name: shortcut,
handler: (event) => { handler: (event) => {
const {currentBlock} = this.Editor.BlockManager; const { currentBlock } = this.Editor.BlockManager;
/** /**
* Editor is not focused * Editor is not focused
@ -618,7 +635,7 @@ export default class InlineToolbar extends Module {
* it can be used by tools like «Mention» that works without selection: * it can be used by tools like «Mention» that works without selection:
* Example: by SHIFT+@ show dropdown and insert selected username * Example: by SHIFT+@ show dropdown and insert selected username
*/ */
// if (SelectionUtils.isCollapsed) return; // if (SelectionUtils.isCollapsed) return;
const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name); const toolSettings = this.Editor.Tools.getToolSettings(currentBlock.name);
@ -634,6 +651,7 @@ export default class InlineToolbar extends Module {
/** /**
* Inline Tool button clicks * Inline Tool button clicks
*
* @param {InlineTool} tool - Tool's instance * @param {InlineTool} tool - Tool's instance
*/ */
private toolClicked(tool: InlineTool): void { private toolClicked(tool: InlineTool): void {
@ -660,10 +678,10 @@ export default class InlineToolbar extends Module {
const result = {}; const result = {};
for (const tool in this.Editor.Tools.inline) { for (const tool in this.Editor.Tools.inline) {
if (this.Editor.Tools.inline.hasOwnProperty(tool)) { if (Object.prototype.hasOwnProperty.call(this.Editor.Tools.inline, tool)) {
const toolSettings = this.Editor.Tools.getToolSettings(tool); const toolSettings = this.Editor.Tools.getToolSettings(tool);
result[tool] = this.Editor.Tools.constructInline(this.Editor.Tools.inline[tool], toolSettings); result[tool] = this.Editor.Tools.constructInline(this.Editor.Tools.inline[tool], tool, toolSettings);
} }
} }

View file

@ -1,32 +1,33 @@
import Module from '../../__module'; import Module from '../../__module';
import $ from '../../dom'; import $ from '../../dom';
import * as _ from '../../utils'; import * as _ from '../../utils';
import {BlockToolConstructable} from '../../../../types'; import { BlockToolConstructable } from '../../../../types';
import Flipper from '../../flipper'; import Flipper from '../../flipper';
import {BlockToolAPI} from '../../block'; import { BlockToolAPI } from '../../block';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/** /**
* @class Toolbox * @class Toolbox
* @classdesc Holder for Tools * @classdesc Holder for Tools
* *
* @typedef {Toolbox} Toolbox * @typedef {Toolbox} Toolbox
* @property {Boolean} opened - opening state * @property {boolean} opened - opening state
* @property {Object} nodes - Toolbox nodes * @property {object} nodes - Toolbox nodes
* @property {Object} CSS - CSS class names * @property {object} CSS - CSS class names
* *
*/ */
export default class Toolbox extends Module { export default class Toolbox extends Module {
/** /**
* CSS styles * CSS styles
* @return {{toolbox: string, toolboxButton string, toolboxButtonActive: string, *
* toolboxOpened: string, tooltip: string, tooltipShown: string, tooltipShortcut: string}} * @returns {object.<string, string>}
*/ */
get CSS() { public get CSS(): {[name: string]: string} {
return { return {
toolbox: 'ce-toolbox', toolbox: 'ce-toolbox',
toolboxButton: 'ce-toolbox__button', toolboxButton: 'ce-toolbox__button',
toolboxButtonActive : 'ce-toolbox__button--active', toolboxButtonActive: 'ce-toolbox__button--active',
toolboxOpened: 'ce-toolbox--opened', toolboxOpened: 'ce-toolbox--opened',
openedToolbarHolderModifier: 'codex-editor--toolbox-opened', openedToolbarHolderModifier: 'codex-editor--toolbox-opened',
@ -37,7 +38,8 @@ export default class Toolbox extends Module {
/** /**
* Returns True if Toolbox is Empty and nothing to show * Returns True if Toolbox is Empty and nothing to show
* @return {boolean} *
* @returns {boolean}
*/ */
public get isEmpty(): boolean { public get isEmpty(): boolean {
return this.displayedToolsCount === 0; return this.displayedToolsCount === 0;
@ -45,16 +47,17 @@ export default class Toolbox extends Module {
/** /**
* Opening state * Opening state
*
* @type {boolean} * @type {boolean}
*/ */
public opened: boolean = false; public opened = false;
/** /**
* HTMLElements used for Toolbox UI * HTMLElements used for Toolbox UI
*/ */
public nodes: { public nodes: {
toolbox: HTMLElement, toolbox: HTMLElement;
buttons: HTMLElement[], buttons: HTMLElement[];
} = { } = {
toolbox: null, toolbox: null,
buttons: [], buttons: [],
@ -62,12 +65,14 @@ export default class Toolbox extends Module {
/** /**
* How many tools displayed in Toolbox * How many tools displayed in Toolbox
*
* @type {number} * @type {number}
*/ */
private displayedToolsCount: number = 0; private displayedToolsCount = 0;
/** /**
* Instance of class that responses for leafing buttons by arrows/tab * Instance of class that responses for leafing buttons by arrows/tab
*
* @type {Flipper|null} * @type {Flipper|null}
*/ */
private flipper: Flipper = null; private flipper: Flipper = null;
@ -86,8 +91,8 @@ export default class Toolbox extends Module {
/** /**
* Toolbox Tool's button click handler * Toolbox Tool's button click handler
* *
* @param {MouseEvent|KeyboardEvent} event * @param {MouseEvent|KeyboardEvent} event - event that activates toolbox button
* @param {string} toolName * @param {string} toolName - button to activate
*/ */
public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void { public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void {
const tool = this.Editor.Tools.toolsClasses[toolName] as BlockToolConstructable; const tool = this.Editor.Tools.toolsClasses[toolName] as BlockToolConstructable;
@ -139,8 +144,8 @@ export default class Toolbox extends Module {
const tools = this.Editor.Tools.available; const tools = this.Editor.Tools.available;
for (const toolName in tools) { for (const toolName in tools) {
if (tools.hasOwnProperty(toolName)) { if (Object.prototype.hasOwnProperty.call(tools, toolName)) {
this.addTool(toolName, tools[toolName] as BlockToolConstructable); this.addTool(toolName, tools[toolName] as BlockToolConstructable);
} }
} }
} }
@ -166,6 +171,7 @@ export default class Toolbox extends Module {
if (toolToolboxSettings && !toolToolboxSettings.icon) { if (toolToolboxSettings && !toolToolboxSettings.icon) {
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', toolName); _.log('Toolbar icon is missed. Tool %o skipped', 'warn', toolName);
return; return;
} }
@ -222,14 +228,14 @@ export default class Toolbox extends Module {
/** /**
* Draw tooltip for toolbox tools * Draw tooltip for toolbox tools
* *
* @param {String} toolName - toolbox tool name * @param {string} toolName - toolbox tool name
* @return { HTMLElement } * @returns {HTMLElement}
*/ */
private drawTooltip(toolName: string): HTMLElement { private drawTooltip(toolName: string): HTMLElement {
const toolSettings = this.Editor.Tools.getToolSettings(toolName); const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {}; const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {}; const userToolboxSettings = toolSettings.toolbox || {};
const name = userToolboxSettings.title || toolboxSettings.title || toolName; const name = I18n.t(I18nInternalNS.toolNames, userToolboxSettings.title || toolboxSettings.title || toolName);
let shortcut = toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT]; let shortcut = toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT];
@ -251,11 +257,12 @@ export default class Toolbox extends Module {
/** /**
* Enable shortcut Block Tool implemented shortcut * Enable shortcut Block Tool implemented shortcut
*
* @param {BlockToolConstructable} tool - Tool class * @param {BlockToolConstructable} tool - Tool class
* @param {String} toolName - Tool name * @param {string} toolName - Tool name
* @param {String} shortcut - shortcut according to the ShortcutData Module format * @param {string} shortcut - shortcut according to the ShortcutData Module format
*/ */
private enableShortcut(tool: BlockToolConstructable, toolName: string, shortcut: string) { private enableShortcut(tool: BlockToolConstructable, toolName: string, shortcut: string): void {
this.Editor.Shortcuts.add({ this.Editor.Shortcuts.add({
name: shortcut, name: shortcut,
handler: (event: KeyboardEvent) => { handler: (event: KeyboardEvent) => {
@ -270,6 +277,7 @@ export default class Toolbox extends Module {
*/ */
private enableFlipper(): void { private enableFlipper(): void {
const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[]; const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[];
this.flipper = new Flipper({ this.flipper = new Flipper({
items: tools, items: tools,
focusedItemClass: this.CSS.toolboxButtonActive, focusedItemClass: this.CSS.toolboxButtonActive,
@ -281,22 +289,16 @@ export default class Toolbox extends Module {
* Can be called when button clicked on Toolbox or by ShortcutData * Can be called when button clicked on Toolbox or by ShortcutData
* *
* @param {BlockToolConstructable} tool - Tool Class * @param {BlockToolConstructable} tool - Tool Class
* @param {String} toolName - Tool name * @param {string} toolName - Tool name
*/ */
private insertNewBlock(tool: BlockToolConstructable, toolName: string) { private insertNewBlock(tool: BlockToolConstructable, toolName: string): void {
const {BlockManager, Caret} = this.Editor; const { BlockManager, Caret } = this.Editor;
/** const { currentBlock } = BlockManager;
* @type {Block}
*/
const {currentBlock} = BlockManager;
let newBlock; const newBlock = BlockManager.insert({
tool: toolName,
if (currentBlock.isEmpty) { replace: currentBlock.isEmpty,
newBlock = BlockManager.replace(toolName); });
} else {
newBlock = BlockManager.insert(toolName);
}
/** /**
* Apply callback before inserting html * Apply callback before inserting html

View file

@ -3,11 +3,12 @@ import Module from '../__module';
import * as _ from '../utils'; import * as _ from '../utils';
import { import {
BlockToolConstructable, BlockToolConstructable,
EditorConfig,
InlineTool, InlineTool,
InlineToolConstructable, Tool, InlineToolConstructable, Tool,
ToolConfig, ToolConfig,
ToolConstructable, ToolConstructable,
ToolSettings, ToolSettings
} from '../../../types'; } from '../../../types';
import BoldInlineTool from '../inline-tools/inline-tool-bold'; import BoldInlineTool from '../inline-tools/inline-tool-bold';
import ItalicInlineTool from '../inline-tools/inline-tool-italic'; import ItalicInlineTool from '../inline-tools/inline-tool-italic';
@ -31,17 +32,18 @@ import Stub from '../tools/stub';
* @property {EditorConfig} config - Editor config * @property {EditorConfig} config - Editor config
*/ */
export default class Tools extends Module { export default class Tools extends Module {
/** /**
* Name of Stub Tool * Name of Stub Tool
* Stub Tool is used to substitute unavailable block Tools and store their data * Stub Tool is used to substitute unavailable block Tools and store their data
*
* @type {string} * @type {string}
*/ */
public stubTool = 'stub'; public stubTool = 'stub';
/** /**
* Returns available Tools * Returns available Tools
* @return {Tool[]} *
* @returns {Tool[]}
*/ */
public get available(): {[name: string]: ToolConstructable} { public get available(): {[name: string]: ToolConstructable} {
return this.toolsAvailable; return this.toolsAvailable;
@ -49,7 +51,8 @@ export default class Tools extends Module {
/** /**
* Returns unavailable Tools * Returns unavailable Tools
* @return {Tool[]} *
* @returns {Tool[]}
*/ */
public get unavailable(): {[name: string]: ToolConstructable} { public get unavailable(): {[name: string]: ToolConstructable} {
return this.toolsUnavailable; return this.toolsUnavailable;
@ -57,14 +60,15 @@ export default class Tools extends Module {
/** /**
* Return Tools for the Inline Toolbar * Return Tools for the Inline Toolbar
* @return {Object} - object of Inline Tool's classes *
* @returns {object} - object of Inline Tool's classes
*/ */
public get inline(): {[name: string]: ToolConstructable} { public get inline(): {[name: string]: ToolConstructable} {
if (this._inlineTools) { if (this._inlineTools) {
return this._inlineTools; return this._inlineTools;
} }
const tools = Object.entries(this.available).filter( ([name, tool]) => { const tools = Object.entries(this.available).filter(([name, tool]) => {
if (!tool[this.INTERNAL_SETTINGS.IS_INLINE]) { if (!tool[this.INTERNAL_SETTINGS.IS_INLINE]) {
return false; return false;
} }
@ -73,14 +77,15 @@ export default class Tools extends Module {
* Some Tools validation * Some Tools validation
*/ */
const inlineToolRequiredMethods = ['render', 'surround', 'checkState']; const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];
const notImplementedMethods = inlineToolRequiredMethods.filter( (method) => !this.constructInline(tool)[method]); const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !this.constructInline(tool, name)[method]);
if (notImplementedMethods.length) { if (notImplementedMethods.length) {
_.log( _.log(
`Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`, `Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,
'warn', 'warn',
notImplementedMethods, notImplementedMethods
); );
return false; return false;
} }
@ -92,7 +97,9 @@ export default class Tools extends Module {
*/ */
const result = {}; const result = {};
tools.forEach(([name, tool]) => result[name] = tool); tools.forEach(([name, tool]) => {
result[name] = tool;
});
/** /**
* Cache prepared Tools * Cache prepared Tools
@ -106,8 +113,7 @@ export default class Tools extends Module {
* Return editor block tools * Return editor block tools
*/ */
public get blockTools(): {[name: string]: BlockToolConstructable} { public get blockTools(): {[name: string]: BlockToolConstructable} {
// eslint-disable-next-line no-unused-vars const tools = Object.entries(this.available).filter(([, tool]) => {
const tools = Object.entries(this.available).filter( ([name, tool]) => {
return !tool[this.INTERNAL_SETTINGS.IS_INLINE]; return !tool[this.INTERNAL_SETTINGS.IS_INLINE];
}); });
@ -116,7 +122,9 @@ export default class Tools extends Module {
*/ */
const result = {}; const result = {};
tools.forEach(([name, tool]) => result[name] = tool); tools.forEach(([name, tool]) => {
result[name] = tool;
});
return result; return result;
} }
@ -124,9 +132,9 @@ export default class Tools extends Module {
/** /**
* Constant for available Tools internal settings provided by Tool developer * Constant for available Tools internal settings provided by Tool developer
* *
* @return {object} * @returns {object}
*/ */
public get INTERNAL_SETTINGS() { public get INTERNAL_SETTINGS(): {[name: string]: string} {
return { return {
IS_ENABLED_LINE_BREAKS: 'enableLineBreaks', IS_ENABLED_LINE_BREAKS: 'enableLineBreaks',
IS_INLINE: 'isInline', IS_INLINE: 'isInline',
@ -143,7 +151,7 @@ export default class Tools extends Module {
* *
* return {object} * return {object}
*/ */
public get USER_SETTINGS() { public get USER_SETTINGS(): {[name: string]: string} {
return { return {
SHORTCUT: 'shortcut', SHORTCUT: 'shortcut',
TOOLBOX: 'toolbox', TOOLBOX: 'toolbox',
@ -155,7 +163,8 @@ export default class Tools extends Module {
/** /**
* Map {name: Class, ...} where: * Map {name: Class, ...} where:
* name block type name in JSON. Got from EditorConfig.tools keys * name block type name in JSON. Got from EditorConfig.tools keys
* @type {Object} *
* @type {object}
*/ */
public readonly toolsClasses: {[name: string]: ToolConstructable} = {}; public readonly toolsClasses: {[name: string]: ToolConstructable} = {};
@ -171,24 +180,26 @@ export default class Tools extends Module {
/** /**
* Tools settings in a map {name: settings, ...} * Tools settings in a map {name: settings, ...}
* @type {Object} *
* @type {object}
*/ */
private readonly toolsSettings: {[name: string]: ToolSettings} = {}; private readonly toolsSettings: {[name: string]: ToolSettings} = {};
/** /**
* Cache for the prepared inline tools * Cache for the prepared inline tools
*
* @type {null|object} * @type {null|object}
* @private * @private
*/ */
private _inlineTools: {[name: string]: ToolConstructable} = {}; private _inlineTools: {[name: string]: ToolConstructable} = {};
/** /**
* @constructor * @class
* *
* @param {EditorConfig} config * @param {EditorConfig} config - Editor's configuration
*/ */
constructor({config}) { constructor({ config }) {
super({config}); super({ config });
this.toolsClasses = {}; this.toolsClasses = {};
@ -197,14 +208,16 @@ export default class Tools extends Module {
/** /**
* Available tools list * Available tools list
* {name: Class, ...} * {name: Class, ...}
* @type {Object} *
* @type {object}
*/ */
this.toolsAvailable = {}; this.toolsAvailable = {};
/** /**
* Tools that rejected a prepare method * Tools that rejected a prepare method
* {name: Class, ... } * {name: Class, ... }
* @type {Object} *
* @type {object}
*/ */
this.toolsUnavailable = {}; this.toolsUnavailable = {};
@ -213,9 +226,10 @@ export default class Tools extends Module {
/** /**
* Creates instances via passed or default configuration * Creates instances via passed or default configuration
* @return {Promise} *
* @returns {Promise<void>}
*/ */
public prepare() { public prepare(): Promise<void> {
this.validateTools(); this.validateTools();
/** /**
@ -223,7 +237,7 @@ export default class Tools extends Module {
*/ */
this.config.tools = _.deepMerge({}, this.internalTools, this.config.tools); this.config.tools = _.deepMerge({}, this.internalTools, this.config.tools);
if (!this.config.hasOwnProperty('tools') || Object.keys(this.config.tools).length === 0) { if (!Object.prototype.hasOwnProperty.call(this.config, 'tools') || Object.keys(this.config.tools).length === 0) {
throw Error('Can\'t start without tools'); throw Error('Can\'t start without tools');
} }
@ -238,12 +252,14 @@ export default class Tools extends Module {
if (typeof this.config.tools[toolName] === 'object') { if (typeof this.config.tools[toolName] === 'object') {
/** /**
* Save Tool's class from 'class' field * Save Tool's class from 'class' field
*
* @type {Tool} * @type {Tool}
*/ */
this.toolsClasses[toolName] = (this.config.tools[toolName] as ToolSettings).class; this.toolsClasses[toolName] = (this.config.tools[toolName] as ToolSettings).class;
/** /**
* Save Tool's settings * Save Tool's settings
*
* @type {ToolSettings} * @type {ToolSettings}
*/ */
this.toolsSettings[toolName] = this.config.tools[toolName] as ToolSettings; this.toolsSettings[toolName] = this.config.tools[toolName] as ToolSettings;
@ -255,15 +271,17 @@ export default class Tools extends Module {
} else { } else {
/** /**
* Save Tool's class * Save Tool's class
*
* @type {Tool} * @type {Tool}
*/ */
this.toolsClasses[toolName] = this.config.tools[toolName] as ToolConstructable; this.toolsClasses[toolName] = this.config.tools[toolName] as ToolConstructable;
/** /**
* Set empty settings for Block by default * Set empty settings for Block by default
*
* @type {{}} * @type {{}}
*/ */
this.toolsSettings[toolName] = {class: this.config.tools[toolName] as ToolConstructable}; this.toolsSettings[toolName] = { class: this.config.tools[toolName] as ToolConstructable };
} }
} }
@ -280,129 +298,132 @@ export default class Tools extends Module {
} }
/** /**
* to see how it works {@link Util#sequence} * to see how it works {@link '../utils.ts#sequence'}
*/ */
return _.sequence(sequenceData, (data: any) => { return _.sequence(sequenceData, (data: {toolName: string}) => {
this.success(data); this.success(data);
}, (data) => { }, (data: {toolName: string}) => {
this.fallback(data); this.fallback(data);
}); });
} }
/** /**
* @param {ChainData.data} data - append tool to available list * Success callback
*
* @param {object} data - append tool to available list
*/ */
public success(data) { public success(data: {toolName: string}): void {
this.toolsAvailable[data.toolName] = this.toolsClasses[data.toolName]; this.toolsAvailable[data.toolName] = this.toolsClasses[data.toolName];
} }
/** /**
* @param {ChainData.data} data - append tool to unavailable list * Fail callback
*/
public fallback(data) {
this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName];
}
/**
* Return Tool`s instance
* *
* @param {String} tool tool name * @param {object} data - append tool to unavailable list
* @param {BlockToolData} data initial data
* @return {BlockTool}
*/ */
public construct(tool, data) { public fallback(data: {toolName: string}): void {
const plugin = this.toolsClasses[tool]; this.toolsUnavailable[data.toolName] = this.toolsClasses[data.toolName];
/**
* Configuration to be passed to the Tool's constructor
*/
const config = this.toolsSettings[tool][this.USER_SETTINGS.CONFIG] || {};
// Pass placeholder to initial Block config
if (tool === this.config.initialBlock && !config.placeholder) {
config.placeholder = this.config.placeholder;
}
/**
* @type {{api: API, config: ({}), data: BlockToolData}}
*/
const constructorOptions = {
api: this.Editor.API.methods,
config,
data,
};
return new plugin(constructorOptions);
} }
/** /**
* Return Inline Tool's instance * Return Inline Tool's instance
* *
* @param {InlineTool} tool * @param {InlineTool} tool - Inline Tool instance
* @param {ToolSettings} toolSettings * @param {string} name - tool name
* @return {InlineTool} instance * @param {ToolSettings} toolSettings - tool settings
*
* @returns {InlineTool} instance
*/ */
public constructInline(tool: InlineToolConstructable, toolSettings: ToolSettings = {} as ToolSettings): InlineTool { public constructInline(tool: InlineToolConstructable, name: string, toolSettings: ToolSettings = {} as ToolSettings): InlineTool {
/**
* @type {{api: API}}
*/
const constructorOptions = { const constructorOptions = {
api: this.Editor.API.methods, api: this.Editor.API.getMethodsForTool(name),
config: (toolSettings[this.USER_SETTINGS.CONFIG] || {}) as ToolSettings, config: (toolSettings[this.USER_SETTINGS.CONFIG] || {}) as ToolSettings,
}; };
// eslint-disable-next-line new-cap
return new tool(constructorOptions) as InlineTool; return new tool(constructorOptions) as InlineTool;
} }
/** /**
* Check if passed Tool is an instance of Initial Block Tool * Check if passed Tool is an instance of Initial Block Tool
*
* @param {Tool} tool - Tool to check * @param {Tool} tool - Tool to check
* @return {Boolean} *
* @returns {boolean}
*/ */
public isInitial(tool) { public isInitial(tool): boolean {
return tool instanceof this.available[this.config.initialBlock]; return tool instanceof this.available[this.config.initialBlock];
} }
/** /**
* Return Tool's config by name * Return Tool's config by name
* @param {string} toolName *
* @return {ToolSettings} * @param {string} toolName - name of tool
*
* @returns {ToolSettings}
*/ */
public getToolSettings(toolName): ToolSettings { public getToolSettings(toolName): ToolSettings {
return this.toolsSettings[toolName]; 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) {
config.placeholder = this.config.placeholder;
settings[this.USER_SETTINGS.CONFIG] = config;
}
return settings;
}
/**
* Returns internal tools
* Includes Bold, Italic, Link and Paragraph
*/
public get internalTools(): {[toolName: string]: ToolConstructable|ToolSettings} {
return {
bold: { class: BoldInlineTool },
italic: { class: ItalicInlineTool },
link: { class: LinkInlineTool },
paragraph: {
class: Paragraph,
inlineToolbar: true,
},
stub: { class: Stub },
};
} }
/** /**
* Binds prepare function of plugins with user or default config * Binds prepare function of plugins with user or default config
* @return {Array} list of functions that needs to be fired sequentially *
* @returns {Array} list of functions that needs to be fired sequentially
*/ */
private getListOfPrepareFunctions(): Array<{ private getListOfPrepareFunctions(): Array<{
function: (data: {toolName: string, config: ToolConfig}) => void, function: (data: {toolName: string; config: ToolConfig}) => void;
data: {toolName: string, config: ToolConfig}, data: {toolName: string; config: ToolConfig};
}> { }> {
const toolPreparationList: Array<{ const toolPreparationList: Array<{
function: (data: {toolName: string, config: ToolConfig}) => void, function: (data: {toolName: string; config: ToolConfig}) => void;
data: {toolName: string, config: ToolConfig}} data: {toolName: string; config: ToolConfig};}
> = []; > = [];
for (const toolName in this.toolsClasses) { for (const toolName in this.toolsClasses) {
if (this.toolsClasses.hasOwnProperty(toolName)) { if (Object.prototype.hasOwnProperty.call(this.toolsClasses, toolName)) {
const toolClass = this.toolsClasses[toolName]; const toolClass = this.toolsClasses[toolName];
const toolConfig = this.toolsSettings[toolName][this.USER_SETTINGS.CONFIG];
if (typeof toolClass.prepare === 'function') { /**
toolPreparationList.push({ * If Tool hasn't a prepare method,
function: toolClass.prepare, * still push it to tool preparation list to save tools order in Toolbox.
data: { * As Tool's prepare method might be async, _.sequence util helps to save the order.
toolName, */
config: this.toolsSettings[toolName][this.USER_SETTINGS.CONFIG], toolPreparationList.push({
}, // eslint-disable-next-line @typescript-eslint/no-empty-function
}); function: typeof toolClass.prepare === 'function' ? toolClass.prepare : (): void => {},
} else { data: {
/** toolName,
* If Tool hasn't a prepare method, mark it as available config: toolConfig,
*/ },
this.toolsAvailable[toolName] = toolClass; });
}
} }
} }
@ -412,12 +433,12 @@ export default class Tools extends Module {
/** /**
* Validate Tools configuration objects and throw Error for user if it is invalid * Validate Tools configuration objects and throw Error for user if it is invalid
*/ */
private validateTools() { private validateTools(): void {
/** /**
* Check Tools for a class containing * Check Tools for a class containing
*/ */
for (const toolName in this.config.tools) { for (const toolName in this.config.tools) {
if (this.config.tools.hasOwnProperty(toolName)) { if (Object.prototype.hasOwnProperty.call(this.config.tools, toolName)) {
if (toolName in this.internalTools) { if (toolName in this.internalTools) {
return; return;
} }
@ -426,27 +447,29 @@ export default class Tools extends Module {
if (!_.isFunction(tool) && !_.isFunction((tool as ToolSettings).class)) { if (!_.isFunction(tool) && !_.isFunction((tool as ToolSettings).class)) {
throw Error( throw Error(
`Tool «${toolName}» must be a constructor function or an object with function in the «class» property`, `Tool «${toolName}» must be a constructor function or an object with function in the «class» property`
); );
} }
} }
} }
} }
}
/**
* What kind of plugins developers can create
*/
export enum ToolType {
/**
* Block tool
*/
Block,
/**
* Inline tool
*/
Inline,
/** /**
* Returns internal tools * Block tune
* Includes Bold, Italic, Link and Paragraph
*/ */
get internalTools() { Tune,
return {
bold: {class: BoldInlineTool},
italic: {class: ItalicInlineTool},
link: {class: LinkInlineTool},
paragraph: {
class: Paragraph,
inlineToolbar: true,
},
stub: {class: Stub},
};
}
} }

View file

@ -1,10 +1,11 @@
/* eslint-disable jsdoc/no-undefined-types */
import Module from '../__module'; import Module from '../__module';
/** /**
* Use external module CodeX Tooltip * Use external module CodeX Tooltip
*/ */
import CodeXTooltips, { TooltipContent, TooltipOptions } from 'codex-tooltip'; import CodeXTooltips, { TooltipContent, TooltipOptions } from 'codex-tooltip';
import {ModuleConfig} from '../../types-internal/module-config'; import { ModuleConfig } from '../../types-internal/module-config';
/** /**
* Tooltip * Tooltip
@ -12,19 +13,19 @@ import {ModuleConfig} from '../../types-internal/module-config';
* Decorates any tooltip module like adapter * Decorates any tooltip module like adapter
*/ */
export default class Tooltip extends Module { export default class Tooltip extends Module {
/** /**
* Tooltips lib: CodeX Tooltips * Tooltips lib: CodeX Tooltips
*
* @see https://github.com/codex-team/codex.tooltips * @see https://github.com/codex-team/codex.tooltips
*/ */
private lib: CodeXTooltips = new CodeXTooltips(); private lib: CodeXTooltips = new CodeXTooltips();
/** /**
* @constructor * @class
* @param {EditorConfig} * @param {EditorConfig} - Editor's config
*/ */
constructor({config}: ModuleConfig) { constructor({ config }: ModuleConfig) {
super({config}); super({ config });
} }
/** /**

View file

@ -1,3 +1,4 @@
/* eslint-disable jsdoc/no-undefined-types */
/** /**
* Prebuilded sprite of SVG icons * Prebuilded sprite of SVG icons
*/ */
@ -28,35 +29,36 @@ import Flipper from '../flipper';
* *
* @typedef {UI} UI * @typedef {UI} UI
* @property {EditorConfig} config - editor configuration {@link EditorJS#configuration} * @property {EditorConfig} config - editor configuration {@link EditorJS#configuration}
* @property {Object} Editor - available editor modules {@link EditorJS#moduleInstances} * @property {object} Editor - available editor modules {@link EditorJS#moduleInstances}
* @property {Object} nodes - * @property {object} nodes -
* @property {Element} nodes.holder - element where we need to append redactor * @property {Element} nodes.holder - element where we need to append redactor
* @property {Element} nodes.wrapper - <codex-editor> * @property {Element} nodes.wrapper - <codex-editor>
* @property {Element} nodes.redactor - <ce-redactor> * @property {Element} nodes.redactor - <ce-redactor>
*/ */
export default class UI extends Module { export default class UI extends Module {
/** /**
* Editor.js UI CSS class names * Editor.js UI CSS class names
* @return {{editorWrapper: string, editorZone: string}} *
* @returns {{editorWrapper: string, editorZone: string}}
*/ */
public get CSS(): { public get CSS(): {
editorWrapper: string, editorWrapperNarrow: string, editorZone: string, editorZoneHidden: string, editorWrapper: string; editorWrapperNarrow: string; editorZone: string; editorZoneHidden: string;
editorLoader: string, editorEmpty: string, editorLoader: string; editorEmpty: string;
} { } {
return { return {
editorWrapper : 'codex-editor', editorWrapper: 'codex-editor',
editorWrapperNarrow : 'codex-editor--narrow', editorWrapperNarrow: 'codex-editor--narrow',
editorZone : 'codex-editor__redactor', editorZone: 'codex-editor__redactor',
editorZoneHidden : 'codex-editor__redactor--hidden', editorZoneHidden: 'codex-editor__redactor--hidden',
editorLoader : 'codex-editor__loader', editorLoader: 'codex-editor__loader',
editorEmpty : 'codex-editor--empty', editorEmpty: 'codex-editor--empty',
}; };
} }
/** /**
* Return Width of center column of Editor * Return Width of center column of Editor
* @return {DOMRect} *
* @returns {DOMRect}
*/ */
public get contentRect(): DOMRect { public get contentRect(): DOMRect {
if (this.contentRectCache) { if (this.contentRectCache) {
@ -83,9 +85,10 @@ export default class UI extends Module {
/** /**
* Flag that became true on mobile viewport * Flag that became true on mobile viewport
*
* @type {boolean} * @type {boolean}
*/ */
public isMobile: boolean = false; public isMobile = false;
/** /**
* HTML Elements used for UI * HTML Elements used for UI
@ -99,12 +102,14 @@ export default class UI extends Module {
/** /**
* Cache for center column rectangle info * Cache for center column rectangle info
* Invalidates on window resize * Invalidates on window resize
*
* @type {DOMRect} * @type {DOMRect}
*/ */
private contentRectCache: DOMRect = undefined; private contentRectCache: DOMRect = undefined;
/** /**
* Handle window resize only when it finished * Handle window resize only when it finished
*
* @type {() => void} * @type {() => void}
*/ */
private resizeDebouncer: () => void = _.debounce(() => { private resizeDebouncer: () => void = _.debounce(() => {
@ -177,7 +182,7 @@ export default class UI extends Module {
* Check if Editor is empty and set CSS class to wrapper * Check if Editor is empty and set CSS class to wrapper
*/ */
public checkEmptiness(): void { public checkEmptiness(): void {
const {BlockManager} = this.Editor; const { BlockManager } = this.Editor;
this.nodes.wrapper.classList.toggle(this.CSS.editorEmpty, BlockManager.isEditorEmpty); this.nodes.wrapper.classList.toggle(this.CSS.editorEmpty, BlockManager.isEditorEmpty);
} }
@ -185,7 +190,8 @@ export default class UI extends Module {
/** /**
* Check if one of Toolbar is opened * Check if one of Toolbar is opened
* Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar * Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar
* @return {boolean} *
* @returns {boolean}
*/ */
public get someToolbarOpened(): boolean { public get someToolbarOpened(): boolean {
const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor; const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
@ -199,9 +205,10 @@ export default class UI extends Module {
public get someFlipperButtonFocused(): boolean { public get someFlipperButtonFocused(): boolean {
return Object.entries(this.Editor).filter(([moduleName, moduleClass]) => { return Object.entries(this.Editor).filter(([moduleName, moduleClass]) => {
return moduleClass.flipper instanceof Flipper; return moduleClass.flipper instanceof Flipper;
}).some(([moduleName, moduleClass]) => { })
return moduleClass.flipper.currentItem; .some(([moduleName, moduleClass]) => {
}); return moduleClass.flipper.currentItem;
});
} }
/** /**
@ -226,17 +233,19 @@ export default class UI extends Module {
/** /**
* Check for mobile mode and cache a result * Check for mobile mode and cache a result
*/ */
private checkIsMobile() { private checkIsMobile(): void {
this.isMobile = window.innerWidth < 650; this.isMobile = window.innerWidth < 650;
} }
/** /**
* Makes Editor.js interface * Makes Editor.js interface
* @return {Promise<void>} *
* @returns {Promise<void>}
*/ */
private async make(): Promise<void> { private async make(): Promise<void> {
/** /**
* Element where we need to append Editor.js * Element where we need to append Editor.js
*
* @type {Element} * @type {Element}
*/ */
this.nodes.holder = $.getHolder(this.config.holder); this.nodes.holder = $.getHolder(this.config.holder);
@ -244,7 +253,7 @@ export default class UI extends Module {
/** /**
* Create and save main UI elements * Create and save main UI elements
*/ */
this.nodes.wrapper = $.make('div', this.CSS.editorWrapper); this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);
this.nodes.redactor = $.make('div', this.CSS.editorZone); this.nodes.redactor = $.make('div', this.CSS.editorZone);
/** /**
@ -261,7 +270,6 @@ export default class UI extends Module {
this.nodes.wrapper.appendChild(this.nodes.redactor); this.nodes.wrapper.appendChild(this.nodes.redactor);
this.nodes.holder.appendChild(this.nodes.wrapper); this.nodes.holder.appendChild(this.nodes.wrapper);
} }
/** /**
@ -271,12 +279,22 @@ export default class UI extends Module {
/** /**
* Load CSS * Load CSS
*/ */
// eslint-disable-next-line @typescript-eslint/no-var-requires
const styles = require('../../styles/main.css'); const styles = require('../../styles/main.css');
const styleTagId = 'editor-js-styles';
/**
* Do not append styles again if they are already on the page
*/
if ($.get(styleTagId)) {
return;
}
/** /**
* Make tag * Make tag
*/ */
const tag = $.make('style', null, { const tag = $.make('style', null, {
id: styleTagId,
textContent: styles.toString(), textContent: styles.toString(),
}); });
@ -294,17 +312,17 @@ export default class UI extends Module {
this.nodes.redactor, this.nodes.redactor,
'click', 'click',
(event) => this.redactorClicked(event as MouseEvent), (event) => this.redactorClicked(event as MouseEvent),
false, false
); );
this.Editor.Listeners.on(this.nodes.redactor, this.Editor.Listeners.on(this.nodes.redactor,
'mousedown', 'mousedown',
(event) => this.documentTouched(event as MouseEvent), (event) => this.documentTouched(event as MouseEvent),
true, true
); );
this.Editor.Listeners.on(this.nodes.redactor, this.Editor.Listeners.on(this.nodes.redactor,
'touchstart', 'touchstart',
(event) => this.documentTouched(event as MouseEvent), (event) => this.documentTouched(event as MouseEvent),
true, true
); );
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true); this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true);
@ -341,16 +359,23 @@ export default class UI extends Module {
/** /**
* All keydowns on document * All keydowns on document
* @param {Event} event *
* @param {KeyboardEvent} event - keyboard event
*/ */
private documentKeydown(event: KeyboardEvent): void { private documentKeydown(event: KeyboardEvent): void {
switch (event.keyCode) { switch (event.keyCode) {
case _.keyCodes.ENTER: case _.keyCodes.ENTER:
this.enterPressed(event); this.enterPressed(event);
break; break;
case _.keyCodes.BACKSPACE: case _.keyCodes.BACKSPACE:
this.backspacePressed(event); this.backspacePressed(event);
break; break;
case _.keyCodes.ESC:
this.escapePressed(event);
break;
default: default:
this.defaultBehaviour(event); this.defaultBehaviour(event);
break; break;
@ -359,11 +384,12 @@ export default class UI extends Module {
/** /**
* Ignore all other document's keydown events * Ignore all other document's keydown events
* @param {KeyboardEvent} event *
* @param {KeyboardEvent} event - keyboard event
*/ */
private defaultBehaviour(event: KeyboardEvent): void { private defaultBehaviour(event: KeyboardEvent): void {
const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`); const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`);
const {currentBlock} = this.Editor.BlockManager; const { currentBlock } = this.Editor.BlockManager;
const isMetaKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; const isMetaKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
/** /**
@ -385,13 +411,18 @@ export default class UI extends Module {
} }
/** /**
* @param {KeyboardEvent} event * @param {KeyboardEvent} event - keyboard event
*/ */
private backspacePressed(event: KeyboardEvent): void { private backspacePressed(event: KeyboardEvent): void {
const {BlockManager, BlockSelection, Caret} = this.Editor; const { BlockManager, BlockSelection, Caret } = this.Editor;
if (BlockSelection.anyBlockSelected) { /**
* If any block selected and selection doesn't exists on the page (that means no other editable element is focused),
* remove selected blocks
*/
if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks(); const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START); Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */ /** Clear selection */
@ -408,16 +439,47 @@ export default class UI extends Module {
} }
} }
/**
* Escape pressed
* If some of Toolbar components are opened, then close it otherwise close Toolbar
*
* @param {Event} event - escape keydown event
*/
private escapePressed(event): void {
/**
* Clear blocks selection by ESC
*/
this.Editor.BlockSelection.clearSelection(event);
if (this.Editor.Toolbox.opened) {
this.Editor.Toolbox.close();
} else if (this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.close();
} else if (this.Editor.ConversionToolbar.opened) {
this.Editor.ConversionToolbar.close();
} else if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.close();
} else {
this.Editor.Toolbar.close();
}
}
/** /**
* Enter pressed on document * Enter pressed on document
* @param event *
* @param {KeyboardEvent} event - keyboard event
*/ */
private enterPressed(event: KeyboardEvent): void { private enterPressed(event: KeyboardEvent): void {
const { BlockManager, BlockSelection, Caret } = this.Editor; const { BlockManager, BlockSelection, Caret } = this.Editor;
const hasPointerToBlock = BlockManager.currentBlockIndex >= 0; const hasPointerToBlock = BlockManager.currentBlockIndex >= 0;
if (BlockSelection.anyBlockSelected) { /**
* If any block selected and selection doesn't exists on the page (that means no other editable element is focused),
* remove selected blocks
*/
if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks(); const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START); Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */ /** Clear selection */
@ -431,6 +493,7 @@ export default class UI extends Module {
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
event.stopPropagation(); event.stopPropagation();
return; return;
} }
@ -466,7 +529,8 @@ export default class UI extends Module {
/** /**
* All clicks on document * All clicks on document
* @param {MouseEvent} event - Click *
* @param {MouseEvent} event - Click event
*/ */
private documentClicked(event: MouseEvent): void { private documentClicked(event: MouseEvent): void {
/** /**
@ -519,6 +583,8 @@ export default class UI extends Module {
* Also: * Also:
* - Move and show the Toolbar * - Move and show the Toolbar
* - Set a Caret * - Set a Caret
*
* @param {MouseEvent | TouchEvent} event - touch or mouse event
*/ */
private documentTouched(event: MouseEvent | TouchEvent): void { private documentTouched(event: MouseEvent | TouchEvent): void {
let clickedNode = event.target as HTMLElement; let clickedNode = event.target as HTMLElement;
@ -569,7 +635,7 @@ export default class UI extends Module {
/** /**
* All clicks on the redactor zone * All clicks on the redactor zone
* *
* @param {MouseEvent} event * @param {MouseEvent} event - click event
* *
* @description * @description
* - By clicks on the Editor's bottom zone: * - By clicks on the Editor's bottom zone:
@ -596,6 +662,7 @@ export default class UI extends Module {
const validUrl = _.getValidUrl(href); const validUrl = _.getValidUrl(href);
_.openTab(validUrl); _.openTab(validUrl);
return; return;
} }
@ -625,7 +692,8 @@ export default class UI extends Module {
/** /**
* Handle selection changes on mobile devices * Handle selection changes on mobile devices
* Uses for showing the Inline Toolbar * Uses for showing the Inline Toolbar
* @param {Event} event *
* @param {Event} event - selection event
*/ */
private selectionChanged(event: Event): void { private selectionChanged(event: Event): void {
const focusedElement = Selection.anchorElement as Element; const focusedElement = Selection.anchorElement as Element;
@ -635,7 +703,6 @@ export default class UI extends Module {
* We need to skip such firings * We need to skip such firings
*/ */
if (!focusedElement || !focusedElement.closest(`.${Block.CSS.content}`)) { if (!focusedElement || !focusedElement.closest(`.${Block.CSS.content}`)) {
/** /**
* If new selection is not on Inline Toolbar, we need to close it * If new selection is not on Inline Toolbar, we need to close it
*/ */

View file

@ -18,7 +18,9 @@ interface Element {
* would be selected by the specified selector string; * would be selected by the specified selector string;
* otherwise, returns false. * otherwise, returns false.
* *
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill} * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill}
*
* @param {string} s - selector
*/ */
if (!Element.prototype.matches) { if (!Element.prototype.matches) {
Element.prototype.matches = Element.prototype.matchesSelector || Element.prototype.matches = Element.prototype.matchesSelector ||
@ -26,7 +28,7 @@ if (!Element.prototype.matches) {
Element.prototype.msMatchesSelector || Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector || Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector || Element.prototype.webkitMatchesSelector ||
function(s) { function (s): boolean {
const matches = (this.document || this.ownerDocument).querySelectorAll(s); const matches = (this.document || this.ownerDocument).querySelectorAll(s);
let i = matches.length; let i = matches.length;
@ -43,10 +45,13 @@ if (!Element.prototype.matches) {
* matches the selectors given in parameter. * matches the selectors given in parameter.
* If there isn't such an ancestor, it returns null. * If there isn't such an ancestor, it returns null.
* *
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill} * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill}
*
* @param {string} s - selector
*/ */
if (!Element.prototype.closest) { if (!Element.prototype.closest) {
Element.prototype.closest = function(s) { Element.prototype.closest = function (s): Element | null {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let el = this; let el = this;
if (!document.documentElement.contains(el)) { if (!document.documentElement.contains(el)) {
@ -70,20 +75,22 @@ if (!Element.prototype.closest) {
* or DOMString objects before the first child of the ParentNode. * or DOMString objects before the first child of the ParentNode.
* DOMString objects are inserted as equivalent Text nodes. * DOMString objects are inserted as equivalent Text nodes.
* *
* {@link https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/prepend#Polyfill} * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/prepend#Polyfill}
*
* @param {Node | Node[] | string | string[]} nodes - nodes to prepend
*/ */
if (!Element.prototype.prepend) { if (!Element.prototype.prepend) {
Element.prototype.prepend = function prepend(nodes: Node|Node[]|any) { Element.prototype.prepend = function prepend(nodes: Array<Node | string> | Node | string): void {
const docFrag = document.createDocumentFragment(); const docFrag = document.createDocumentFragment();
if (!Array.isArray(nodes)) { if (!Array.isArray(nodes)) {
nodes = [ nodes ]; nodes = [ nodes ];
} }
nodes.forEach((node: Node|any) => { nodes.forEach((node: Node | string) => {
const isNode = node instanceof Node; const isNode = node instanceof Node;
docFrag.appendChild(isNode ? node : document.createTextNode(String(node))); docFrag.appendChild(isNode ? node as Node : document.createTextNode(node as string));
}); });
this.insertBefore(docFrag, this.firstChild); this.insertBefore(docFrag, this.firstChild);

View file

@ -30,15 +30,16 @@ interface Document {
/** /**
* Working with selection * Working with selection
*
* @typedef {SelectionUtils} SelectionUtils * @typedef {SelectionUtils} SelectionUtils
*/ */
export default class SelectionUtils { export default class SelectionUtils {
/** /**
* Editor styles * Editor styles
* @return {{editorWrapper: string, editorZone: string}} *
* @returns {{editorWrapper: string, editorZone: string}}
*/ */
static get CSS(): { editorWrapper: string, editorZone: string } { public static get CSS(): { editorWrapper: string; editorZone: string } {
return { return {
editorWrapper: 'codex-editor', editorWrapper: 'codex-editor',
editorZone: 'codex-editor__redactor', editorZone: 'codex-editor__redactor',
@ -48,9 +49,10 @@ export default class SelectionUtils {
/** /**
* Returns selected anchor * Returns selected anchor
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode} * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}
* @return {Node|null} *
* @returns {Node|null}
*/ */
static get anchorNode(): Node | null { public static get anchorNode(): Node | null {
const selection = window.getSelection(); const selection = window.getSelection();
return selection ? selection.anchorNode : null; return selection ? selection.anchorNode : null;
@ -58,9 +60,10 @@ export default class SelectionUtils {
/** /**
* Returns selected anchor element * Returns selected anchor element
* @return {Element|null} *
* @returns {Element|null}
*/ */
static get anchorElement(): Element | null { public static get anchorElement(): Element | null {
const selection = window.getSelection(); const selection = window.getSelection();
if (!selection) { if (!selection) {
@ -83,9 +86,10 @@ export default class SelectionUtils {
/** /**
* Returns selection offset according to the anchor node * Returns selection offset according to the anchor node
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset} * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
* @return {Number|null} *
* @returns {number|null}
*/ */
static get anchorOffset(): number | null { public static get anchorOffset(): number | null {
const selection = window.getSelection(); const selection = window.getSelection();
return selection ? selection.anchorOffset : null; return selection ? selection.anchorOffset : null;
@ -93,9 +97,10 @@ export default class SelectionUtils {
/** /**
* Is current selection range collapsed * Is current selection range collapsed
* @return {boolean|null} *
* @returns {boolean|null}
*/ */
static get isCollapsed(): boolean | null { public static get isCollapsed(): boolean | null {
const selection = window.getSelection(); const selection = window.getSelection();
return selection ? selection.isCollapsed : null; return selection ? selection.isCollapsed : null;
@ -103,9 +108,10 @@ export default class SelectionUtils {
/** /**
* Check current selection if it is at Editor's zone * Check current selection if it is at Editor's zone
* @return {boolean} *
* @returns {boolean}
*/ */
static get isAtEditor(): boolean { public static get isAtEditor(): boolean {
const selection = SelectionUtils.get(); const selection = SelectionUtils.get();
/** /**
@ -118,6 +124,7 @@ export default class SelectionUtils {
} }
let editorZone = null; let editorZone = null;
if (selectedNode) { if (selectedNode) {
editorZone = selectedNode.closest(`.${SelectionUtils.CSS.editorZone}`); editorZone = selectedNode.closest(`.${SelectionUtils.CSS.editorZone}`);
} }
@ -129,10 +136,20 @@ export default class SelectionUtils {
} }
/** /**
* Return first range * Methods return boolean that true if selection exists on the page
* @return {Range|null}
*/ */
static get range(): Range { public static get isSelectionExists(): boolean {
const selection = SelectionUtils.get();
return !!selection.anchorNode;
}
/**
* Return first range
*
* @returns {Range|null}
*/
public static get range(): Range | null {
const selection = window.getSelection(); const selection = window.getSelection();
return selection && selection.rangeCount ? selection.getRangeAt(0) : null; return selection && selection.rangeCount ? selection.getRangeAt(0) : null;
@ -140,11 +157,12 @@ export default class SelectionUtils {
/** /**
* Calculates position and size of selected text * Calculates position and size of selected text
* @return {{x, y, width, height, top?, left?, bottom?, right?}} *
* @returns {DOMRect | ClientRect}
*/ */
static get rect(): DOMRect | ClientRect { public static get rect(): DOMRect | ClientRect {
let sel: Selection | MSSelection = (document as Document).selection, let sel: Selection | MSSelection = (document as Document).selection,
range: TextRange | Range; range: TextRange | Range;
let rect = { let rect = {
x: 0, x: 0,
@ -166,6 +184,7 @@ export default class SelectionUtils {
if (!window.getSelection) { if (!window.getSelection) {
_.log('Method window.getSelection is not supported', 'warn'); _.log('Method window.getSelection is not supported', 'warn');
return rect; return rect;
} }
@ -173,6 +192,7 @@ export default class SelectionUtils {
if (sel.rangeCount === null || isNaN(sel.rangeCount)) { if (sel.rangeCount === null || isNaN(sel.rangeCount)) {
_.log('Method SelectionUtils.rangeCount is not supported', 'warn'); _.log('Method SelectionUtils.rangeCount is not supported', 'warn');
return rect; return rect;
} }
@ -210,26 +230,24 @@ export default class SelectionUtils {
/** /**
* Returns selected text as String * Returns selected text as String
*
* @returns {string} * @returns {string}
*/ */
static get text(): string { public static get text(): string {
return window.getSelection ? window.getSelection().toString() : ''; return window.getSelection ? window.getSelection().toString() : '';
} }
/** /**
* Returns window SelectionUtils * Selection instances
* {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection} *
* @return {Selection} * @todo Check if this is still relevant
*/ */
public static get(): Selection {
return window.getSelection();
}
public instance: Selection = null; public instance: Selection = null;
public selection: Selection = null; public selection: Selection = null;
/** /**
* This property can store SelectionUtils's range for restoring later * This property can store SelectionUtils's range for restoring later
*
* @type {Range|null} * @type {Range|null}
*/ */
public savedSelectionRange: Range = null; public savedSelectionRange: Range = null;
@ -237,7 +255,7 @@ export default class SelectionUtils {
/** /**
* Fake background is active * Fake background is active
* *
* @return {boolean} * @returns {boolean}
*/ */
public isFakeBackgroundEnabled = false; public isFakeBackgroundEnabled = false;
@ -247,10 +265,53 @@ export default class SelectionUtils {
private readonly commandBackground: string = 'backColor'; private readonly commandBackground: string = 'backColor';
private readonly commandRemoveFormat: string = 'removeFormat'; private readonly commandRemoveFormat: string = 'removeFormat';
/**
* Returns window SelectionUtils
* {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}
*
* @returns {Selection}
*/
public static get(): Selection {
return window.getSelection();
}
/**
* Set focus to contenteditable or native input element
*
* @param element - element where to set focus
* @param offset - offset of cursor
*
* @returns {DOMRect} of range
*/
public static setCursor(element: HTMLElement, offset = 0): DOMRect {
const range = document.createRange();
const selection = window.getSelection();
/** if found deepest node is native input */
if ($.isNativeInput(element)) {
if (!$.canSetCaret(element)) {
return;
}
element.focus();
element.selectionStart = element.selectionEnd = offset;
return element.getBoundingClientRect();
}
range.setStart(element, offset);
range.setEnd(element, offset);
selection.removeAllRanges();
selection.addRange(range);
return range.getBoundingClientRect();
}
/** /**
* Removes fake background * Removes fake background
*/ */
public removeFakeBackground() { public removeFakeBackground(): void {
if (!this.isFakeBackgroundEnabled) { if (!this.isFakeBackgroundEnabled) {
return; return;
} }
@ -262,7 +323,7 @@ export default class SelectionUtils {
/** /**
* Sets fake background * Sets fake background
*/ */
public setFakeBackground() { public setFakeBackground(): void {
document.execCommand(this.commandBackground, false, '#a8d6ff'); document.execCommand(this.commandBackground, false, '#a8d6ff');
this.isFakeBackgroundEnabled = true; this.isFakeBackgroundEnabled = true;
@ -312,10 +373,11 @@ export default class SelectionUtils {
/** /**
* Looks ahead to find passed tag from current selection * Looks ahead to find passed tag from current selection
* *
* @param {String} tagName - tag to found * @param {string} tagName - tag to found
* @param {String} [className] - tag's class name * @param {string} [className] - tag's class name
* @param {Number} [searchDepth] - count of tags that can be included. For better performance. * @param {number} [searchDepth] - count of tags that can be included. For better performance.
* @return {HTMLElement|null} *
* @returns {HTMLElement|null}
*/ */
public findParentTag(tagName: string, className?: string, searchDepth = 10): HTMLElement | null { public findParentTag(tagName: string, className?: string, searchDepth = 10): HTMLElement | null {
const selection = window.getSelection(); const selection = window.getSelection();
@ -388,7 +450,7 @@ export default class SelectionUtils {
/** /**
* Expands selection range to the passed parent node * Expands selection range to the passed parent node
* *
* @param {HTMLElement} element * @param {HTMLElement} element - element which contents should be selcted
*/ */
public expandToTag(element: HTMLElement): void { public expandToTag(element: HTMLElement): void {
const selection = window.getSelection(); const selection = window.getSelection();

View file

@ -1,10 +1,20 @@
import $ from '../../dom'; import $ from '../../dom';
import {BlockTool, BlockToolData} from '../../../../types'; import { API, BlockTool, BlockToolData, BlockToolConstructorOptions } from '../../../../types';
export interface StubData extends BlockToolData{
title: string;
savedData: BlockToolData;
}
/**
* This tool will be shown in place of a block without corresponding plugin
* It will store its data inside and pass it back with article saving
*/
export default class Stub implements BlockTool { export default class Stub implements BlockTool {
/** /**
* Stub styles * Stub styles
* @type {{wrapper: string; info: string; title: string; subtitle: string}} *
* @type {{wrapper: string, info: string, title: string, subtitle: string}}
*/ */
private CSS = { private CSS = {
wrapper: 'ce-stub', wrapper: 'ce-stub',
@ -18,6 +28,11 @@ export default class Stub implements BlockTool {
*/ */
private readonly wrapper: HTMLElement; private readonly wrapper: HTMLElement;
/**
* Editor.js API
*/
private readonly api: API;
/** /**
* Stub title tool name * Stub title tool name
*/ */
@ -33,9 +48,15 @@ export default class Stub implements BlockTool {
*/ */
private readonly savedData: BlockToolData; private readonly savedData: BlockToolData;
constructor({data, config, api}) { /**
this.title = data.title || 'Error'; * @param options - constructor options
this.subtitle = 'The block can not be displayed correctly.'; * @param options.data - stub tool data
* @param options.api - Editor.js API
*/
constructor({ data, api }: BlockToolConstructorOptions<StubData>) {
this.api = api;
this.title = data.title || this.api.i18n.t('Error');
this.subtitle = this.api.i18n.t('The block can not be displayed correctly.');
this.savedData = data.savedData; this.savedData = data.savedData;
this.wrapper = this.make(); this.wrapper = this.make();
@ -43,7 +64,8 @@ export default class Stub implements BlockTool {
/** /**
* Returns stub holder * Returns stub holder
* @return {HTMLElement} *
* @returns {HTMLElement}
*/ */
public render(): HTMLElement { public render(): HTMLElement {
return this.wrapper; return this.wrapper;
@ -51,7 +73,8 @@ export default class Stub implements BlockTool {
/** /**
* Return original Tool data * Return original Tool data
* @return {BlockToolData} *
* @returns {BlockToolData}
*/ */
public save(): BlockToolData { public save(): BlockToolData {
return this.savedData; return this.savedData;
@ -59,7 +82,8 @@ export default class Stub implements BlockTool {
/** /**
* Create Tool html markup * Create Tool html markup
* @return {HTMLElement} *
* @returns {HTMLElement}
*/ */
private make(): HTMLElement { private make(): HTMLElement {
const wrapper = $.make('div', this.CSS.wrapper); const wrapper = $.make('div', this.CSS.wrapper);

View file

@ -20,12 +20,15 @@ export enum LogLevels {
declare const VERSION: string; declare const VERSION: string;
/** /**
* @typedef {Object} ChainData * @typedef {object} ChainData
* @property {Object} data - data that will be passed to the success or fallback * @property {object} data - data that will be passed to the success or fallback
* @property {Function} function - function's that must be called asynchronically * @property {Function} function - function's that must be called asynchronously
*
* @interface ChainData
*/ */
export interface ChainData { export interface ChainData {
data?: any; data?: object;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function: (...args: any[]) => any; function: (...args: any[]) => any;
} }
@ -35,7 +38,8 @@ export interface ChainData {
/** /**
* Returns basic keycodes as constants * Returns basic keycodes as constants
* @return {{}} *
* @returns {{}}
*/ */
export const keyCodes = { export const keyCodes = {
BACKSPACE: 8, BACKSPACE: 8,
@ -73,17 +77,16 @@ export const mouseButtons = {
* @param {string} type - logging type 'log'|'warn'|'error'|'info' * @param {string} type - logging type 'log'|'warn'|'error'|'info'
* @param {*} [args] - argument to log with a message * @param {*} [args] - argument to log with a message
* @param {string} style - additional styling to message * @param {string} style - additional styling to message
* @param labeled
*/ */
function _log( function _log(
labeled: boolean, labeled: boolean,
msg: string, msg: string,
type: string = 'log', type = 'log',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any, args?: any,
style: string = 'color: inherit', style = 'color: inherit'
): void { ): void {
if (!('console' in window) || !window.console[type]) {
if ( !('console' in window) || !window.console[ type ] ) {
return; return;
} }
@ -156,7 +159,7 @@ _log.logLevel = LogLevels.VERBOSE;
* *
* @param {LogLevels} logLevel - log level to set * @param {LogLevels} logLevel - log level to set
*/ */
export function setLogLevel(logLevel: LogLevels) { export function setLogLevel(logLevel: LogLevels): void {
_log.logLevel = logLevel; _log.logLevel = logLevel;
} }
@ -172,46 +175,50 @@ export const logLabeled = _log.bind(window, true);
/** /**
* Returns true if passed key code is printable (a-Z, 0-9, etc) character. * Returns true if passed key code is printable (a-Z, 0-9, etc) character.
* @param {number} keyCode *
* @return {boolean} * @param {number} keyCode - key code
*
* @returns {boolean}
*/ */
export function isPrintableKey( keyCode: number ): boolean { export function isPrintableKey(keyCode: number): boolean {
return (keyCode > 47 && keyCode < 58) || // number keys return (keyCode > 47 && keyCode < 58) || // number keys
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s) keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)
(keyCode > 64 && keyCode < 91) || // letter keys (keyCode > 64 && keyCode < 91) || // letter keys
(keyCode > 95 && keyCode < 112) || // Numpad keys (keyCode > 95 && keyCode < 112) || // Numpad keys
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order) (keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
(keyCode > 218 && keyCode < 223); // [\]' (in order) (keyCode > 218 && keyCode < 223); // [\]' (in order)
} }
/** /**
* Fires a promise sequence asyncronically * Fires a promise sequence asynchronously
* *
* @param {ChainData[]} chains - list or ChainData's * @param {ChainData[]} chains - list or ChainData's
* @param {Function} success - success callback * @param {Function} success - success callback
* @param {Function} fallback - callback that fires in case of errors * @param {Function} fallback - callback that fires in case of errors
* *
* @return {Promise} * @returns {Promise}
*/ */
export async function sequence( export async function sequence(
chains: ChainData[], chains: ChainData[],
success: (data: any) => void = () => {}, // eslint-disable-next-line @typescript-eslint/no-empty-function
fallback: (data: any) => void = () => {}, success: (data: object) => void = (): void => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
fallback: (data: object) => void = (): void => {}
): Promise<void> { ): Promise<void> {
/** /**
* Decorator * Decorator
* *
* @param {ChainData} chainData * @param {ChainData} chainData - Chain data
* *
* @param {Function} successCallback * @param {Function} successCallback - success callback
* @param {Function} fallbackCallback * @param {Function} fallbackCallback - fail callback
* *
* @return {Promise} * @returns {Promise}
*/ */
async function waitNextBlock( async function waitNextBlock(
chainData: ChainData, chainData: ChainData,
successCallback: (data: any) => void, successCallback: (data: object) => void,
fallbackCallback: (data: any) => void, fallbackCallback: (data: object) => void
): Promise<void> { ): Promise<void> {
try { try {
await chainData.function(chainData.data); await chainData.function(chainData.data);
@ -228,8 +235,9 @@ export async function sequence(
* reduce current element will not be able to continue while can't get * reduce current element will not be able to continue while can't get
* a resolved Promise * a resolved Promise
*/ */
return await chains.reduce(async (previousValue, currentValue) => { return chains.reduce(async (previousValue, currentValue) => {
await previousValue; await previousValue;
return waitNextBlock(currentValue, success, fallback); return waitNextBlock(currentValue, success, fallback);
}, Promise.resolve()); }, Promise.resolve());
} }
@ -237,28 +245,35 @@ export async function sequence(
/** /**
* Make array from array-like collection * Make array from array-like collection
* *
* @param {ArrayLike} collection * @param {ArrayLike} collection - collection to convert to array
* *
* @return {Array} * @returns {Array}
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function array(collection: ArrayLike<any>): any[] { export function array(collection: ArrayLike<any>): any[] {
return Array.prototype.slice.call(collection); return Array.prototype.slice.call(collection);
} }
/** /**
* Check if passed variable is a function * Check if passed variable is a function
* @param {*} fn *
* @return {boolean} * @param {*} fn - function to check
*
* @returns {boolean}
*/ */
export function isFunction(fn: any): boolean { // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isFunction(fn: any): fn is Function {
return typeof fn === 'function'; return typeof fn === 'function';
} }
/** /**
* Check if passed function is a class * Check if passed function is a class
* @param {function} fn *
* @return {boolean} * @param {Function} fn - function to check
*
* @returns {boolean}
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isClass(fn: any): boolean { export function isClass(fn: any): boolean {
return typeof fn === 'function' && /^\s*class\s+/.test(fn.toString()); return typeof fn === 'function' && /^\s*class\s+/.test(fn.toString());
} }
@ -266,8 +281,9 @@ export function isClass(fn: any): boolean {
/** /**
* Checks if object is empty * Checks if object is empty
* *
* @param {Object} object * @param {object} object - object to check
* @return {boolean} *
* @returns {boolean}
*/ */
export function isEmpty(object: object): boolean { export function isEmpty(object: object): boolean {
if (!object) { if (!object) {
@ -279,23 +295,28 @@ export function isEmpty(object: object): boolean {
/** /**
* Check if passed object is a Promise * Check if passed object is a Promise
*
* @param {*} object - object to check * @param {*} object - object to check
* @return {Boolean} * @returns {boolean}
*/ */
export function isPromise(object: any): boolean { // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isPromise(object: any): object is Promise<any> {
return Promise.resolve(object) === object; return Promise.resolve(object) === object;
} }
/** /**
* Delays method execution * Delays method execution
* *
* @param {Function} method * @param {Function} method - method to execute
* @param {Number} timeout * @param {number} timeout - timeout in ms
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function delay(method: (...args: any[]) => any, timeout: number) { export function delay(method: (...args: any[]) => any, timeout: number) {
return function() { return function (): void {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this, const context = this,
args = arguments; // eslint-disable-next-line prefer-rest-params
args = arguments;
window.setTimeout(() => method.apply(context, args), timeout); window.setTimeout(() => method.apply(context, args), timeout);
}; };
@ -304,8 +325,9 @@ export function delay(method: (...args: any[]) => any, timeout: number) {
/** /**
* Get file extension * Get file extension
* *
* @param {File} file * @param {File} file - file
* @return string *
* @returns {string}
*/ */
export function getFileExtension(file: File): string { export function getFileExtension(file: File): string {
return file.name.split('.').pop(); return file.name.split('.').pop();
@ -314,8 +336,9 @@ export function getFileExtension(file: File): string {
/** /**
* Check if string is MIME type * Check if string is MIME type
* *
* @param {string} type * @param {string} type - string to check
* @return boolean *
* @returns {boolean}
*/ */
export function isValidMimeType(type: string): boolean { export function isValidMimeType(type: string): boolean {
return /^[-\w]+\/([-+\w]+|\*)$/.test(type); return /^[-\w]+\/([-+\w]+|\*)$/.test(type);
@ -328,17 +351,20 @@ export function isValidMimeType(type: string): boolean {
* Note that this method returns Function and declared variable need to be called * Note that this method returns Function and declared variable need to be called
* *
* @param {Function} func - function that we're throttling * @param {Function} func - function that we're throttling
* @param {Number} wait - time in milliseconds * @param {number} wait - time in milliseconds
* @param {Boolean} immediate - call now * @param {boolean} immediate - call now
* @return {Function} * @returns {Function}
*/ */
export function debounce(func: () => void, wait?: number , immediate?: boolean): () => void { export function debounce(func: () => void, wait?: number, immediate?: boolean): () => void {
let timeout; let timeout;
return () => { return (): void => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this, const context = this,
args = arguments; // eslint-disable-next-line prefer-rest-params
args = arguments;
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const later = () => { const later = () => {
timeout = null; timeout = null;
if (!immediate) { if (!immediate) {
@ -358,9 +384,10 @@ export function debounce(func: () => void, wait?: number , immediate?: boolean):
/** /**
* Copies passed text to the clipboard * Copies passed text to the clipboard
* @param text *
* @param text - text to copy
*/ */
export function copyTextToClipboard(text) { export function copyTextToClipboard(text): void {
const el = Dom.make('div', 'codex-editor-clipboard', { const el = Dom.make('div', 'codex-editor-clipboard', {
innerHTML: text, innerHTML: text,
}); });
@ -369,6 +396,7 @@ export function copyTextToClipboard(text) {
const selection = window.getSelection(); const selection = window.getSelection();
const range = document.createRange(); const range = document.createRange();
range.selectNode(el); range.selectNode(el);
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
@ -380,8 +408,6 @@ export function copyTextToClipboard(text) {
/** /**
* Returns object with os name as key and boolean as value. Shows current user OS * Returns object with os name as key and boolean as value. Shows current user OS
*
* @return {[key: string]: boolean}
*/ */
export function getUserOS(): {[key: string]: boolean} { export function getUserOS(): {[key: string]: boolean} {
const OS = { const OS = {
@ -395,6 +421,7 @@ export function getUserOS(): {[key: string]: boolean} {
if (userOS) { if (userOS) {
OS[userOS] = true; OS[userOS] = true;
return OS; return OS;
} }
@ -403,23 +430,40 @@ export function getUserOS(): {[key: string]: boolean} {
/** /**
* Capitalizes first letter of the string * Capitalizes first letter of the string
* @param {string} text *
* @return {string} * @param {string} text - text to capitalize
*
* @returns {string}
*/ */
export function capitalize(text: string): string { export function capitalize(text: string): string {
return text[0].toUpperCase() + text.slice(1); return text[0].toUpperCase() + text.slice(1);
} }
/** /**
* Merge to objects recursively * Return string representation of the object type
* @param {object} target *
* @param {object[]} sources * @param {*} object - object to get type
* @return {object} *
* @returns {string}
*/ */
export function deepMerge(target, ...sources) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
const isObject = (item) => item && typeOf(item) === 'object'; export function typeOf(object: any): string {
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
if (!sources.length) { return target; } /**
* Merge to objects recursively
*
* @param {object} target - merge target
* @param {object[]} sources - merge sources
* @returns {object}
*/
export function deepMerge<T extends object>(target, ...sources): T {
const isObject = (item): item is object => item && typeOf(item) === 'object';
if (!sources.length) {
return target;
}
const source = sources.shift(); const source = sources.shift();
if (isObject(target) && isObject(source)) { if (isObject(target) && isObject(source)) {
@ -444,23 +488,16 @@ export function deepMerge(target, ...sources) {
* *
* Note! This is a simple solution, it can give false-positive results. * Note! This is a simple solution, it can give false-positive results.
* To detect touch devices more carefully, use 'touchstart' event listener * To detect touch devices more carefully, use 'touchstart' event listener
*
* @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/ * @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
* *
* @return {boolean} * @returns {boolean}
*/ */
export const isTouchSupported: boolean = 'ontouchstart' in document.documentElement; export const isTouchSupported: boolean = 'ontouchstart' in document.documentElement;
/**
* Return string representation of the object type
*
* @param {any} object
*/
export function typeOf(object: any): string {
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
/** /**
* Make shortcut command more human-readable * Make shortcut command more human-readable
*
* @param {string} shortcut string like 'CMD+B' * @param {string} shortcut string like 'CMD+B'
*/ */
export function beautifyShortcut(shortcut: string): string { export function beautifyShortcut(shortcut: string): string {
@ -493,7 +530,7 @@ export function beautifyShortcut(shortcut: string): string {
* If url has `one slash`, then it concatenates with window location origin * If url has `one slash`, then it concatenates with window location origin
* or when url has `two lack` it appends only protocol * or when url has `two lack` it appends only protocol
* *
* @param {String} url * @param {string} url - url to prettify
*/ */
export function getValidUrl(url: string): string { export function getValidUrl(url: string): string {
try { try {
@ -514,7 +551,7 @@ export function getValidUrl(url: string): string {
/** /**
* Opens new Tab with passed URL * Opens new Tab with passed URL
* *
* @param {String} url - URL address to redirect * @param {string} url - URL address to redirect
*/ */
export function openTab(url: string): void { export function openTab(url: string): void {
window.open(url, '_blank'); window.open(url, '_blank');

View file

@ -117,6 +117,8 @@
*/ */
--toolbar-button: { --toolbar-button: {
display: inline-flex; display: inline-flex;
align-items: center;
justify-content: center;
width: 34px; width: 34px;
height: 34px; height: 34px;
line-height: 34px; line-height: 34px;
@ -135,11 +137,6 @@
background-color: var(--bg-light); background-color: var(--bg-light);
} }
& > svg,
.icon {
margin: auto;
}
&--active { &--active {
color: var(--color-active-icon); color: var(--color-active-icon);
} }

View file

@ -35,6 +35,7 @@ import InlineToolbarAPI from '../components/modules/api/inlineToolbar';
import CrossBlockSelection from '../components/modules/crossBlockSelection'; import CrossBlockSelection from '../components/modules/crossBlockSelection';
import ConversionToolbar from '../components/modules/toolbar/conversion'; import ConversionToolbar from '../components/modules/toolbar/conversion';
import TooltipAPI from '../components/modules/api/tooltip'; import TooltipAPI from '../components/modules/api/tooltip';
import I18nAPI from '../components/modules/api/i18n';
export interface EditorModules { export interface EditorModules {
UI: UI; UI: UI;
@ -74,4 +75,5 @@ export interface EditorModules {
CrossBlockSelection: CrossBlockSelection; CrossBlockSelection: CrossBlockSelection;
NotifierAPI: NotifierAPI; NotifierAPI: NotifierAPI;
TooltipAPI: TooltipAPI; TooltipAPI: TooltipAPI;
I18nAPI: I18nAPI;
} }

View file

@ -0,0 +1,68 @@
/**
* Decorator above the type object
*/
type Indexed<T> = { [key: string]: T };
/**
* Type for I18n dictionary values that can be strings or dictionary sub-sections
*
* Can be used as:
* LeavesDictKeys<typeof myDictionary>
*
* where myDictionary is a JSON with messages
*/
export type LeavesDictKeys<D> = D extends string
/**
* If generic type is string, just return it
*/
? D
/**
* If generic type is object that has only one level and contains only strings, return it's keys union
*
* { key: "string", anotherKey: "string" } => "key" | "anotherKey"
*
*/
: D extends Indexed<string>
? keyof D
/**
* If generic type is object, but not the one described above,
* use LeavesDictKey on it's values recursively and union the results
*
* { "rootKey": { "subKey": "string" }, "anotherRootKey": { "anotherSubKey": "string" } } => "subKey" | "anotherSubKey"
*
*/
: D extends Indexed<any>
? { [K in keyof D]: LeavesDictKeys<D[K]> }[keyof D]
/**
* In other cases, return never type
*/
: never;
/**
* Provide type-safe access to the available namespaces of the dictionary
*
* Can be uses as:
* DictNamespaces<typeof myDictionary>
*
* where myDictionary is a JSON with messages
*/
export type DictNamespaces<D extends object> = {
/**
* Iterate through generic type keys
*
* If value under current key is object that has only one level and contains only strings, return string type
*/
[K in keyof D]: D[K] extends Indexed<string>
? string
/**
* If value under current key is object with depth more than one, apply DictNamespaces recursively
*/
: D[K] extends Indexed<any>
? DictNamespaces<D[K]>
/**
* In other cases, return never type
*/
: never;
}

View file

@ -4,6 +4,12 @@
"target": "es2017", "target": "es2017",
"declaration": false, "declaration": false,
"moduleResolution": "node", // This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime "moduleResolution": "node", // This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime
"lib": ["dom", "es2017", "es2018"] "lib": ["dom", "es2017", "es2018"],
// allows to import .json files for i18n
"resolveJsonModule": true,
// allows to omit export default in .json files
"allowSyntheticDefaultImports": true
} }
} }

65
types/api/block.d.ts vendored Normal file
View file

@ -0,0 +1,65 @@
import {BlockToolData, ToolConfig} from '../tools';
import {SavedData} from '../../src/types-internal/block-data';
/**
* @interface BlockAPI Describes Block API methods and properties
*/
export interface BlockAPI {
/**
* Tool name
*/
readonly name: string;
/**
* Tool config passed on Editor's initialization
*/
readonly config: ToolConfig;
/**
* Wrapper of Tool's HTML element
*/
readonly holder: HTMLElement;
/**
* True if Block content is empty
*/
readonly isEmpty: boolean;
/**
* True if Block is selected with Cross-Block selection
*/
readonly selected: boolean;
/**
* Setter sets Block's stretch state
*
* Getter returns true if Block is stretched
*/
stretched: boolean;
/**
* Call Tool method with errors handler under-the-hood
*
* @param {string} methodName - method to call
* @param {object} param - object with parameters
*
* @return {void}
*/
call(methodName: string, param?: object): void;
/**
* Save Block content
*
* @return {Promise<void|SavedData>}
*/
save(): Promise<void|SavedData>;
/**
* Validate Block data
*
* @param {BlockToolData} data
*
* @return {Promise<boolean>}
*/
validate(data: BlockToolData): Promise<boolean>;
}

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

@ -1,5 +1,6 @@
import {OutputData} from '../data-formats/output-data'; import {OutputData} from '../data-formats/output-data';
import {BlockToolData, ToolConfig} from "../tools"; import {BlockToolData, ToolConfig} from '../tools';
import {BlockAPI} from './block';
/** /**
* Describes methods to manipulate with Editor`s blocks * Describes methods to manipulate with Editor`s blocks
@ -26,8 +27,9 @@ export interface Blocks {
/** /**
* Removes current Block * Removes current Block
* @param {number} index - index of a block to delete
*/ */
delete(): void; delete(index?: number): void;
/** /**
* Swaps two Blocks * Swaps two Blocks
@ -49,7 +51,7 @@ export interface Blocks {
* @param {number} index * @param {number} index
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
getBlockByIndex(index: number): HTMLElement; getBlockByIndex(index: number): BlockAPI;
/** /**
* Returns current Block index * Returns current Block index
@ -61,6 +63,8 @@ export interface Blocks {
* Mark Block as stretched * Mark Block as stretched
* @param {number} index - Block to mark * @param {number} index - Block to mark
* @param {boolean} status - stretch status * @param {boolean} status - stretch status
*
* @deprecated Use BlockAPI interface to stretch Blocks
*/ */
stretchBlock(index: number, status?: boolean): void; stretchBlock(index: number, status?: boolean): void;

11
types/api/i18n.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
/**
* Describes Editor`s I18n API
*/
export interface I18n {
/**
* Perform translation with automatically added namespace like `tools.${toolName}` or `blockTunes.${tuneName}`
*
* @param dictKey - what to translate
*/
t(dictKey: string): string;
}

View file

@ -10,3 +10,5 @@ export * from './toolbar';
export * from './notifier'; export * from './notifier';
export * from './tooltip'; export * from './tooltip';
export * from './inline-toolbar'; export * from './inline-toolbar';
export * from './block';
export * from './i18n';

View file

@ -1,11 +1,12 @@
import {ToolConstructable, ToolSettings} from '../tools'; import {ToolConstructable, ToolSettings} from '../tools';
import {LogLevels, OutputData, API} from '../index'; import {API, LogLevels, OutputData} from '../index';
import {SanitizerConfig} from './sanitizer-config'; import {SanitizerConfig} from './sanitizer-config';
import {I18nConfig} from './i18n-config';
export interface EditorConfig { export interface EditorConfig {
/** /**
* Element where Editor will be append * Element where Editor will be append
* @deprecated property will be removed in next major release, use holder instead * @deprecated property will be removed in the next major release, use holder instead
*/ */
holderId?: string | HTMLElement; holderId?: string | HTMLElement;
@ -62,6 +63,11 @@ export interface EditorConfig {
*/ */
logLevel?: LogLevels; logLevel?: LogLevels;
/**
* Internalization config
*/
i18n?: I18nConfig;
/** /**
* Fires when Editor is ready to work * Fires when Editor is ready to work
*/ */

11
types/configs/i18n-config.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
/**
* Available options of i18n config property
*/
import { I18nDictionary } from './i18n-dictionary';
export interface I18nConfig {
/**
* Dictionary used for translation
*/
messages: I18nDictionary;
}

93
types/configs/i18n-dictionary.d.ts vendored Normal file
View file

@ -0,0 +1,93 @@
/**
* Structure of the i18n dictionary
*/
export interface I18nDictionary {
/**
* Section for translation Tool Names: both block and inline tools
* Example:
* "toolNames": {
* "Text": "Параграф",
* "Heading": "Заголовок",
* "List": "Список",
* ...
* },
*/
toolNames?: Dictionary;
/**
* Section for passing translations to the external tools classes
* The first-level keys of this object should be equal of keys ot the 'tools' property of EditorConfig
* Includes internal tools: "paragraph", "stub"
*
* Example:
* "tools": {
* "warning": {
* "Title": "Название",
* "Message": "Сообщение",
* },
* "link": {
* "Add a link": "Вставьте ссылку"
* },
* },
*/
tools?: Dictionary;
/**
* Section allows to translate Block Tunes
* The first-level keys of this object should be equal of 'name' ot the 'tools.<toolName>.tunes' property of EditorConfig
* Including some internal block-tunes: "delete", "moveUp", "moveDown
*
* Example:
* "blockTunes": {
* "delete": {
* "Delete": "Удалить"
* },
* "moveUp": {
* "Move up": "Переместить вверх"
* },
* "moveDown": {
* "Move down": "Переместить вниз"
* }
* },
*/
blockTunes?: Dictionary;
/**
* Translation of internal UI components of the editor.js core
*/
ui?: Dictionary;
}
/**
* Represent item of the I18nDictionary config
*/
export interface Dictionary {
/**
* The keys of the object can represent two entities:
* 1. Dictionary key usually is an original string from default locale, like "Convert to"
* 2. Sub-namespace section, like "toolbar.converter.<...>"
*
* Example of 1:
* toolbox: {
* "Add": "Добавить",
* }
*
* Example of 2:
* ui: {
* toolbar: {
* toolbox: { <-- Example of 1
* "Add": "Добавить"
* }
* }
* }
*/
[key: string]: DictValue;
}
/**
* The value of the dictionary can be:
* - other dictionary
* - result translate string
*/
export type DictValue = {[key: string]: Dictionary | string} | string;

View file

@ -3,3 +3,5 @@ export * from './sanitizer-config';
export * from './paste-config'; export * from './paste-config';
export * from './conversion-config'; export * from './conversion-config';
export * from './log-levels'; export * from './log-levels';
export * from './i18n-config';
export * from './i18n-dictionary';

View file

@ -1,5 +1,3 @@
import {BlockToolData} from '../index';
/** /**
* Tool onPaste configuration object * Tool onPaste configuration object
*/ */

View file

@ -1,5 +1,19 @@
import {BlockToolData} from '../tools'; import {BlockToolData} from '../tools';
/**
* Output of one Tool
*/
export interface OutputBlockData {
/**
* Too type
*/
type: string;
/**
* Saved Block data
*/
data: BlockToolData;
}
export interface OutputData { export interface OutputData {
/** /**
* Editor's version * Editor's version
@ -14,8 +28,5 @@ export interface OutputData {
/** /**
* Saved Blocks * Saved Blocks
*/ */
blocks: Array<{ blocks: OutputBlockData[];
type: string;
data: BlockToolData
}>;
} }

27
types/index.d.ts vendored
View file

@ -4,7 +4,13 @@
* ------------------------------------ * ------------------------------------
*/ */
import {EditorConfig} from './configs'; import {
EditorConfig,
I18nDictionary,
Dictionary,
DictValue,
I18nConfig,
} from './configs';
import { import {
Blocks, Blocks,
Caret, Caret,
@ -18,6 +24,7 @@ import {
Styles, Styles,
Toolbar, Toolbar,
Tooltip, Tooltip,
I18n,
} from './api'; } from './api';
import {OutputData} from './data-formats/output-data'; import {OutputData} from './data-formats/output-data';
@ -29,7 +36,9 @@ export {
BaseToolConstructable, BaseToolConstructable,
InlineTool, InlineTool,
InlineToolConstructable, InlineToolConstructable,
InlineToolConstructorOptions,
BlockToolConstructable, BlockToolConstructable,
BlockToolConstructorOptions,
BlockTool, BlockTool,
BlockToolData, BlockToolData,
Tool, Tool,
@ -47,8 +56,19 @@ export {
FilePasteEventDetail, FilePasteEventDetail,
} from './tools'; } from './tools';
export {BlockTune, BlockTuneConstructable} from './block-tunes'; export {BlockTune, BlockTuneConstructable} from './block-tunes';
export {EditorConfig, SanitizerConfig, PasteConfig, LogLevels, ConversionConfig} from './configs'; export {
export {OutputData} from './data-formats/output-data'; EditorConfig,
SanitizerConfig,
PasteConfig,
LogLevels,
ConversionConfig,
I18nDictionary,
Dictionary,
DictValue,
I18nConfig,
} from './configs';
export {OutputData, OutputBlockData} from './data-formats/output-data';
export { BlockAPI } from './api'
/** /**
* We have a namespace API {@link ./api/index.d.ts} (APIMethods) but we can not use it as interface * We have a namespace API {@link ./api/index.d.ts} (APIMethods) but we can not use it as interface
@ -67,6 +87,7 @@ export interface API {
toolbar: Toolbar; toolbar: Toolbar;
inlineToolbar: InlineToolbar; inlineToolbar: InlineToolbar;
tooltip: Tooltip; tooltip: Tooltip;
i18n: I18n;
} }
/** /**

View file

@ -2,4 +2,4 @@
* Object returned by Tool's {@link BlockTool#save} method * Object returned by Tool's {@link BlockTool#save} method
* Specified by Tool developer, so leave it as object * Specified by Tool developer, so leave it as object
*/ */
export type BlockToolData = object; export type BlockToolData<T extends object = any> = T;

View file

@ -1,10 +1,11 @@
import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs'; import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs';
import { BlockToolData } from './block-tool-data'; import { BlockToolData } from './block-tool-data';
import { BaseTool, BaseToolConstructable } from './tool'; import {BaseTool, BaseToolConstructable} from './tool';
import { ToolConfig } from './tool-config'; import { ToolConfig } from './tool-config';
import { API } from '../index'; import {API, BlockAPI} from '../index';
import { PasteEvent } from './paste-events'; import { PasteEvent } from './paste-events';
import { MoveEvent } from './hook-events'; import { MoveEvent } from './hook-events';
/** /**
* Describe Block Tool object * Describe Block Tool object
* @see {@link docs/tools.md} * @see {@link docs/tools.md}
@ -73,6 +74,16 @@ export interface BlockTool extends BaseTool {
moved?(event: MoveEvent): void; moved?(event: MoveEvent): void;
} }
/**
* Describe constructor parameters
*/
export interface BlockToolConstructorOptions<D extends object = any, C extends object = any> {
api: API;
data: BlockToolData<D>;
config?: ToolConfig<C>;
block?: BlockAPI;
}
export interface BlockToolConstructable extends BaseToolConstructable { export interface BlockToolConstructable extends BaseToolConstructable {
/** /**
* Tool's Toolbox settings * Tool's Toolbox settings
@ -101,7 +112,10 @@ export interface BlockToolConstructable extends BaseToolConstructable {
/** /**
* @constructor * @constructor
*
* @param {BlockToolConstructorOptions} config - constructor parameters
*
* @return {BlockTool} * @return {BlockTool}
*/ */
new(config: { api: API, config: ToolConfig, data: BlockToolData }): BlockTool; new(config: BlockToolConstructorOptions): BlockTool;
} }

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