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 */
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module"
},
"rules": {
"arrow-spacing": [2, {
"before": true,
"after": true
}],
/** 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
"extends": [
"codex"
],
"rules": {
/**
* Temporary suppress some errors. We need to fix them partially in next patches
*/
"import/no-duplicates": ["warn"],
},
"settings": {
"jsdoc": {
"mode": "typescript"
}
},
"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
about: Create a report to help us improve
title: "⚠️"
about: Create a report to help us improve Editor.js
title: "[Bug]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
Describe a bug.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
Steps to reproduce:
1. Go to …
2. Click on …
3. …
**Expected behavior**
A clear and concise description of what you expected to happen.
Expected behavior:
**Screenshots**
If applicable, add screenshots to help explain your problem.
Screenshots:
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
Device, Browser, OS:
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
Editor.js version:
**Additional context**
Add any other context about the problem here.
Plugins you use with their versions:
<!--
🤫 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
about: Suggest an idea for this project
about: Suggest an idea to improve Editor.js
title: "\U0001F4A1"
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
1. Describe a problem.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
2. Describe the solution you'd like. Mockups are welcome.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
3. Are there any alternatives?
**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
*
* @version 2.17.0
* @version 2.18.0
*
* @licence Apache-2.0
* @author CodeX <https://codex.so>
@ -10,32 +26,6 @@
* @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
* @copyright CodeX (https://codex.so)
@ -44,6 +34,29 @@
* @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.
* Represents simple paragraph
@ -52,14 +65,3 @@
* @copyright CodeX 2018
* @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
### 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
- `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` — Fix multiline paste [#1015](https://github.com/codex-team/editor.js/issues/1015)
### 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)

View file

@ -1,9 +1,40 @@
# 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.
## 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
Common API interface.
@ -36,13 +67,13 @@ use 'move' instead)
`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
`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
`insert(type?: string, data?: BlockToolData, config?: ToolConfig, index?: number, needToFocus?: boolean)` - insert new Block with passed parameters
@ -100,11 +131,11 @@ Each method accept `position` and `offset` parameters. `Offset` should be used t
`Position` can be one of the following values:
| Value | Description
| --------- | -----------
| Value | Description
| --------- | -----------
| `start` | Caret will be set at the Block's beginning
| `end` | Caret will be set at the Block end
| `default` | More or less emulates browser behaviour, in most cases behaves as `start`
| `default` | More or less emulates browser behaviour, in most cases behaves as `start`
Each method returns `boolean` value: true if caret is set successfully or false otherwise (e.g. when there is no Block at index);
@ -148,7 +179,7 @@ this.api.notifier.show({
![](https://capella.pics/14fcdbe4-d6eb-41d4-b66e-e0e86ccf1a4b.jpg)
Check out [`codex-notifier` package page](https://github.com/codex-team/js-notifier) on GitHub to find docs, params and examples.
Check out [`codex-notifier` package page](https://github.com/codex-team/js-notifier) on GitHub to find docs, params and examples.
### Destroy API
@ -173,10 +204,10 @@ Methods for showing Tooltip helper near your elements. Parameters are the same a
#### Show
Method shows tooltip with custom content on passed element
```js
this.api.tooltip.show(element, content, options);
```
```
| parameter | type | description |
| -- | -- | -- |
@ -184,17 +215,17 @@ this.api.tooltip.show(element, content, options);
| `content` | _String_ or _Node_ | Content that will be appended to the Tooltip |
| `options` | _Object_ | Some displaying options, see below |
Available showing options
Available showing options
| name | type | action |
| -- | -- | -- |
| placement | `top`, `bottom`, `left`, `right` | Where to place the tooltip. Default value is `bottom' |
| marginTop | _Number_ | Offset above the tooltip with `top` placement |
| marginBottom | _Number_ | Offset below the tooltip with `bottom` placement |
| marginLeft | _Number_ | Offset at left from the tooltip with `left` placement |
| marginRight | _Number_ | Offset at right from the tooltip with `right` placement |
| delay | _Number_ | Delay before showing, in ms. Default is `70` |
| hidingDelay | _Number_ | Delay before hiding, in ms. Default is `0` |
| placement | `top`, `bottom`, `left`, `right` | Where to place the tooltip. Default value is `bottom' |
| marginTop | _Number_ | Offset above the tooltip with `top` placement |
| marginBottom | _Number_ | Offset below the tooltip with `bottom` placement |
| marginLeft | _Number_ | Offset at left from the tooltip with `left` placement |
| marginRight | _Number_ | Offset at right from the tooltip with `right` placement |
| delay | _Number_ | Delay before showing, in ms. Default is `70` |
| hidingDelay | _Number_ | Delay before hiding, in ms. Default is `0` |
#### Hide
@ -206,7 +237,7 @@ this.api.tooltip.hide();
#### onHover
Decorator for showing tooltip near some element by "mouseenter" and hide by "mouseleave".
Decorator for showing tooltip near some element by "mouseenter" and hide by "mouseleave".
```js
this.api.tooltip.onHover(element, content, options);
@ -214,7 +245,7 @@ this.api.tooltip.onHover(element, content, options);
### API Shorthands
Editor`s API provides some shorthands for API methods.
Editor`s API provides some shorthands for API methods.
| Alias | Method |
| ------ | --------------- |

View file

@ -12,11 +12,12 @@ Each Tool should have an installation guide.
Each Tool's instance called with an params object.
| Param | Type | Description |
| ------ | ------------------- | ----------------------------------------------- |
| api | [`IAPI`][iapi-link] | Editor.js's API methods |
| config | `object` | Special configuration params passed in «config» |
| data | `object` | Data to be rendered in this Tool |
| Param | Type | Description |
| ------ | ------------------------------------------------------ | ----------------------------------------------- |
| api | [`IAPI`](../types/index.d.ts) | Editor.js's API methods |
| config | [`ToolConfig`](../types/tools/tool-config.d.ts) | Special configuration params passed in «config» |
| 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
@ -228,14 +229,14 @@ onPaste (event) {
### Disable paste handling
If you need to disable paste handling on your Tool for some reason, you can provide `false` as `pasteConfig` value.
If you need to disable paste handling on your Tool for some reason, you can provide `false` as `pasteConfig` value.
That way paste event won't be processed if fired on your Tool:
```javascript
static get pasteConfig {
return false;
}
```
```
## Sanitize <a name="sanitize"></a>
@ -364,7 +365,7 @@ Editor.js has a Conversion Toolbar that allows user to convert one Block to anot
2. You can add ability to convert other Tools to your Tool. Specify «import» property of `conversionConfig`.
Conversion Toolbar will be shown only near Blocks that specified an «export» rule, when user selected almost all block's content.
This Toolbar will contain only Tools that specified an «import» rule.
This Toolbar will contain only Tools that specified an «import» rule.
Example:
@ -391,11 +392,11 @@ class Header {
### Your Tool -> other Tool
The «export» field specifies how to represent your Tool's data as a string to pass it to other tool.
The «export» field specifies how to represent your Tool's data as a string to pass it to other tool.
It can be a `String` or a `Function`.
`String` means a key of your Tool data object that should be used as string to export.
`String` means a key of your Tool data object that should be used as string to export.
`Function` is a method that accepts your Tool data and compose a string to export from it. See example below:
@ -411,7 +412,7 @@ class ListTool {
type: 'ordered'
}
}
static get conversionConfig() {
return {
export: (data) => {
@ -425,11 +426,11 @@ class ListTool {
### Other Tool -> your Tool
The «import» rule specifies how to create your Tool's data object from the string created by original block.
The «import» rule specifies how to create your Tool's data object from the string created by original block.
It can be a `String` or a `Function`.
It can be a `String` or a `Function`.
`String` means the key in tool data that will be filled by an exported string.
`String` means the key in tool data that will be filled by an exported string.
For example, `import: 'text'` means that `constructor` of your block will accept a `data` object with `text` property filled with string composed by original block.
`Function` allows you to specify own logic, how a string should be converted to your tool data. For example:
@ -442,13 +443,13 @@ class ListTool {
type: 'unordered'
}
}
static get conversionConfig() {
return {
// ... export rule
// ... export rule
/**
* In this example, List Tool creates items by splitting original text by a dot symbol.
* In this example, List Tool creates items by splitting original text by a dot symbol.
*/
import: (string) => {
const items = string.split('.');

View file

@ -93,7 +93,8 @@ body {
max-width: 180px;
background: #4A9DF8;
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;
border-radius: 31px;
color: #fff;
@ -103,6 +104,8 @@ body {
.ce-example__button:hover {
background: #3D8DE5;
transform: translateY(2px);
box-shadow: 0 20px 15px -4px rgba(137, 207, 255, 0.77);
}
.ce-example__output-footer {

View file

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

View file

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

View file

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

View file

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

View file

@ -6,17 +6,22 @@
*/
import $ from '../dom';
import {API, BlockTune} from '../../../types';
import { API, BlockTune } from '../../../types';
/**
*
*/
export default class MoveDownTune implements BlockTune {
/**
* Property that contains Editor.js API methods
* @see {api.md}
*
* @see {@link docs/api.md}
*/
private readonly api: API;
/**
* Styles
*
* @type {{wrapper: string}}
*/
private CSS = {
@ -28,53 +33,58 @@ export default class MoveDownTune implements BlockTune {
/**
* MoveDownTune constructor
*
* @param {{api: API}} api
* @param {API} api Editor's API
*/
public constructor({api}) {
constructor({ api }) {
this.api = api;
}
/**
* Return 'move down' button
*
* @returns {HTMLElement}
*/
public render() {
public render(): HTMLElement {
const moveDownButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
moveDownButton.appendChild($.svg('arrow-down', 14, 14));
this.api.listeners.on(
moveDownButton,
'click',
(event) => this.handleClick(event as MouseEvent, moveDownButton),
false,
false
);
/**
* 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;
}
/**
* 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();
// If Block is last do nothing
if (currentBlockIndex === this.api.blocks.getBlocksCount() - 1) {
button.classList.add(this.CSS.animation);
window.setTimeout( () => {
window.setTimeout(() => {
button.classList.remove(this.CSS.animation);
}, 500);
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();
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
*/
if (nextBlockCoords.top < window.innerHeight) {
scrollOffset = window.scrollY + nextBlockElement.offsetHeight;
}
window.scrollTo(0, scrollOffset);

View file

@ -5,18 +5,22 @@
* @copyright <CodeX Team> 2018
*/
import $ from '../dom';
import {API, BlockTune} from '../../../types';
import { API, BlockTune } from '../../../types';
/**
*
*/
export default class MoveUpTune implements BlockTune {
/**
* Property that contains Editor.js API methods
* @see {api.md}
*
* @see {@link docs/api.md}
*/
private readonly api: API;
/**
* Styles
*
* @type {{wrapper: string}}
*/
private CSS = {
@ -28,54 +32,59 @@ export default class MoveUpTune implements BlockTune {
/**
* MoveUpTune constructor
*
* @param {{api: API}} api
* @param {API} api - Editor's API
*/
public constructor({api}) {
constructor({ api }) {
this.api = api;
}
/**
* Create "MoveUp" button and add click event listener
* @returns [HTMLElement}
*
* @returns {HTMLElement}
*/
public render(): HTMLElement {
const moveUpButton = $.make('div', [this.CSS.button, this.CSS.wrapper], {});
moveUpButton.appendChild($.svg('arrow-up', 14, 14));
this.api.listeners.on(
moveUpButton,
'click',
(event) => this.handleClick(event as MouseEvent, moveUpButton),
false,
false
);
/**
* 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;
}
/**
* 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 {
const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
if (currentBlockIndex === 0) {
button.classList.add(this.CSS.animation);
window.setTimeout( () => {
window.setTimeout(() => {
button.classList.remove(this.CSS.animation);
}, 500);
return;
}
const currentBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex);
const previousBlockElement = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);
const currentBlockElement = currentBlock.holder;
const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);
const previousBlockElement = previousBlock.holder;
/**
* Here is two cases:
@ -86,7 +95,7 @@ export default class MoveUpTune implements BlockTune {
* than we scroll window to the difference between this offsets.
*/
const currentBlockCoords = currentBlockElement.getBoundingClientRect(),
previousBlockCoords = previousBlockElement.getBoundingClientRect();
previousBlockCoords = previousBlockElement.getBoundingClientRect();
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 {
API,
BlockAPI as BlockAPIInterface,
BlockTool,
BlockToolConstructable,
BlockToolData,
@ -7,27 +7,61 @@ import {
BlockTuneConstructable,
SanitizerConfig,
ToolConfig,
} from '../../types';
ToolSettings
} from '../../../types';
import {SavedData} from '../types-internal/block-data';
import $ from './dom';
import * as _ from './utils';
import { SavedData } from '../../types-internal/block-data';
import $ from '../dom';
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
* @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool
*
* @property {BlockTool} tool current block tool (Paragraph, for example)
* @property {Object} CSS block`s css classes
* @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
*/
@ -47,18 +81,17 @@ export enum BlockToolAPI {
/**
* @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance
*
* @property tool - Tool instance
* @property html - Returns HTML content of plugin
* @property holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class
* @property pluginsContent - HTML content that returns by Tool's render function
* @property {BlockTool} tool - Tool instance
* @property {HTMLElement} holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class
* @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function
*/
export default class 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 {
wrapper: 'ce-block',
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
*/
@ -340,31 +132,40 @@ export default class Block {
*/
public tunes: BlockTune[];
/**
* Tool's user configuration
*/
public readonly config: ToolConfig;
/**
* Cached inputs
*
* @type {HTMLElement[]}
*/
private cachedInputs: HTMLElement[] = [];
/**
* Editor`s API
* Editor`s API module
*/
private readonly api: API;
private readonly api: ApiModule;
/**
* Focused input index
*
* @type {number}
*/
private inputIndex = 0;
/**
* Mutation observer to handle DOM mutations
*
* @type {MutationObserver}
*/
private mutationObserver: MutationObserver;
/**
* Debounce Timer
*
* @type {number}
*/
private readonly modificationDebounceTimer = 450;
@ -387,49 +188,323 @@ export default class Block {
}, this.modificationDebounceTimer);
/**
* @constructor
* @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
* Current block API interface
*/
constructor(
toolName: string,
toolInstance: BlockTool,
toolClass: BlockToolConstructable,
settings: ToolConfig,
apiMethods: API,
) {
this.name = toolName;
this.tool = toolInstance;
this.class = toolClass;
private readonly blockAPI: BlockAPIInterface;
/**
* @param {object} options - block constructor options
* @param {string} options.name - Tool name that passed on initialization
* @param {BlockToolData} options.data - Tool's initial data
* @param {BlockToolConstructable} options.Tool Tool's class
* @param {ToolSettings} options.settings - default tool's config
* @param {ApiModule} options.api - Editor API module for pass it to the Block Tunes
*/
constructor({
name,
data,
Tool,
settings,
api,
}: BlockConstructorOptions) {
this.name = name;
this.class = Tool;
this.settings = settings;
this.api = apiMethods;
this.holder = this.compose();
this.config = settings.config || {};
this.api = api;
this.blockAPI = new BlockAPI(this);
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[]}
*/
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
*
* Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function
*
* @param {String} methodName
* @param {Object} params
* @param {string} methodName - method to call
* @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
*/
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 {
// eslint-disable-next-line no-useless-call
this.tool[methodName].call(this.tool, params);
} catch (e) {
_.log(`Error during '${methodName}' call: ${e.message}`, 'error');
@ -439,15 +514,18 @@ export default class Block {
/**
* Call plugins merge method
* @param {Object} data
*
* @param {BlockToolData} data - data to merge
*/
public async mergeWith(data: BlockToolData): Promise<void> {
await this.tool.merge(data);
}
/**
* Extracts data from Block
* Groups Tool's save processing time
* @return {Object}
*
* @returns {object}
*/
public async save(): Promise<void|SavedData> {
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
*
* @param {BlockToolData} data
* @param {BlockToolData} data - data to validate
* @returns {Promise<boolean>} valid
*/
public async validate(data: BlockToolData): Promise<boolean> {
@ -496,28 +574,43 @@ export default class Block {
/**
* Make an array with default settings
* Each block has default tune instance that have states
* @return {BlockTune[]}
*
* @returns {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
return tunesList.map( (tune: BlockTuneConstructable) => {
return new tune({
api: this.api,
settings: this.settings,
return tunesList.map(({ name, Tune }: {name: string; Tune: BlockTuneConstructable}) => {
return new Tune({
api: this.api.getMethodsForTool(name, ToolType.Tune),
settings: this.config,
});
});
}
/**
* Enumerates initialized tunes and returns fragment that can be appended to the toolbars area
* @return {DocumentFragment}
*
* @returns {DocumentFragment}
*/
public renderTunes(): DocumentFragment {
const tunesElement = document.createDocumentFragment();
this.tunes.forEach( (tune) => {
this.tunes.forEach((tune) => {
$.append(tunesElement, tune.render());
});
@ -545,28 +638,30 @@ export default class Block {
subtree: true,
characterData: true,
attributes: true,
},
}
);
}
/**
* Is fired when Block will be unselected
*/
public willUnselect() {
public willUnselect(): void {
this.mutationObserver.disconnect();
}
/**
* Make default Block wrappers and put Tool`s content there
*
* @returns {HTMLDivElement}
*/
private compose(): HTMLDivElement {
const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement,
contentNode = $.make('div', Block.CSS.content),
pluginsContent = this.tool.render();
contentNode = $.make('div', Block.CSS.content),
pluginsContent = this.tool.render();
contentNode.appendChild(pluginsContent);
wrapper.appendChild(contentNode);
return wrapper;
}
}

View file

@ -1,7 +1,7 @@
import * as _ from './utils';
import $ from './dom';
import Block, { BlockToolAPI } from './block';
import {MoveEvent, MoveEventDetail} from '../../types/tools';
import { MoveEvent } from '../../types/tools';
/**
* @class Blocks
@ -13,10 +13,30 @@ import {MoveEvent, MoveEventDetail} from '../../types/tools';
*
*/
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
*
* @returns {Number}
* @returns {number}
*/
public get length(): number {
return this.blocks.length;
@ -47,17 +67,17 @@ export default class Blocks {
* blocks[0] = new Block(...)
*
* @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
* @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 (isNaN(Number(property))) {
Reflect.set(instance, property, value);
return true;
}
@ -67,7 +87,7 @@ export default class Blocks {
* @example
* blocks[0] = new Block();
*/
instance.insert(+property, value);
instance.insert(+(property as number), value as Block);
return true;
}
@ -76,11 +96,10 @@ export default class Blocks {
* Proxy trap to implement array-like getter
*
* @param {Blocks} instance Blocks instance
* @param {Number|String} property Blocks class property
* @param {PropertyKey} property Blocks class property key
* @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
*/
@ -91,33 +110,13 @@ export default class Blocks {
/**
* If property is a number (Block index) return Block by passed index
*/
return instance.get(+property);
}
/**
* 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;
return instance.get(+(property as number));
}
/**
* 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 {
this.blocks.push(block);
@ -126,8 +125,9 @@ export default class Blocks {
/**
* 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
*/
public swap(first: number, second: number): void {
@ -147,13 +147,15 @@ export default class Blocks {
/**
* 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 {
/**
* cut out the block, move the DOM element and insert at the desired index
* again (the shifting within the blocks array will happen automatically).
*
* @see https://stackoverflow.com/a/44932690/1238150
*/
const block = this.blocks.splice(fromIndex, 1)[0];
@ -184,13 +186,14 @@ export default class Blocks {
/**
* 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 {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) {
this.push(block);
return;
}
@ -224,7 +227,8 @@ export default class Blocks {
/**
* Remove block
* @param {Number|null} index
*
* @param {number} index - index of Block to remove
*/
public remove(index: number): void {
if (isNaN(index)) {
@ -254,7 +258,7 @@ export default class Blocks {
*
* @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
*/
public insertAfter(targetBlock: Block, newBlock: Block): void {
@ -266,7 +270,7 @@ export default class Blocks {
/**
* Get Block by index
*
* @param {Number} index Block index
* @param {number} index Block index
* @returns {Block}
*/
public get(index: number): Block {
@ -276,8 +280,8 @@ export default class Blocks {
/**
* Return index of passed Block
*
* @param {Block} block
* @returns {Number}
* @param {Block} block - Block to find
* @returns {number}
*/
public indexOf(block: Block): number {
return this.blocks.indexOf(block);
@ -303,13 +307,12 @@ export default class Blocks {
/**
* Composes Block event with passed type and details
*
* @param {String} type
* @param {MoveEventDetail} detail
* @param {string} type - event type
* @param {object} detail - event detail
*/
private composeBlockEvent(type: string, detail: MoveEventDetail): MoveEvent {
private composeBlockEvent(type: string, detail: object): MoveEvent {
return new CustomEvent(type, {
detail,
},
) as MoveEvent;
detail,
}) as MoveEvent;
}
}

View file

@ -1,8 +1,11 @@
import $ from './dom';
// eslint-disable-next-line import/no-duplicates
import * as _ from './utils';
import {EditorConfig, OutputData, SanitizerConfig} from '../../types';
import {EditorModules} from '../types-internal/editor-modules';
import {LogLevels} from './utils';
// eslint-disable-next-line import/no-duplicates
import { LogLevels } from './utils';
import { EditorConfig, OutputData, SanitizerConfig } from '../../types';
import { EditorModules } from '../types-internal/editor-modules';
import I18n from './i18n';
/**
* @typedef {Core} Core - editor core class
@ -31,13 +34,12 @@ contextRequire.keys().forEach((filename) => {
*
* @classdesc Editor.js core class
*
* @property this.config - all settings
* @property this.moduleInstances - constructed editor components
* @property {EditorConfig} config - all settings
* @property {EditorModules} moduleInstances - constructed editor components
*
* @type {Core}
*/
export default class Core {
/**
* Editor configuration passed by user to the constructor
*/
@ -82,9 +84,10 @@ export default class Core {
await this.render();
if ((this.configuration as EditorConfig).autofocus) {
const {BlockManager, Caret} = this.moduleInstances;
const { BlockManager, Caret } = this.moduleInstances;
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
BlockManager.highlightCurrentNode();
}
/**
@ -110,9 +113,10 @@ export default class Core {
/**
* 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
* Make config object
@ -129,11 +133,13 @@ export default class Core {
if (config.holderId && !config.holder) {
config.holder = config.holderId;
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
*
* @type {EditorConfig}
*/
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
*
* @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
* Uses in case when there is no blocks passed
*
* @type {{type: (*), data: {text: null}}}
*/
const initialBlockData = {
type : this.config.initialBlock,
data : {},
type: this.config.initialBlock,
data: {},
};
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.tools = this.config.tools || {};
this.config.data = this.config.data || {} as OutputData;
this.config.onReady = this.config.onReady || (() => {});
this.config.onChange = this.config.onChange || (() => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.config.onReady = this.config.onReady || ((): void => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.config.onChange = this.config.onChange || ((): void => {});
/**
* Initialize Blocks to pass data to the Renderer
@ -196,18 +206,27 @@ export default class Core {
this.config.data.blocks = [ initialBlockData ];
}
}
/**
* Adjust i18n
*/
if (config.i18n && config.i18n.messages) {
I18n.setDictionary(config.i18n.messages);
}
}
/**
* Returns private property
*
* @returns {EditorConfig}
*/
get configuration(): EditorConfig|string {
public get configuration(): EditorConfig|string {
return this.config;
}
/**
* Checks for required fields in Editor's config
*
* @returns {Promise<void>}
*/
public async validate(): Promise<void> {
@ -234,7 +253,7 @@ export default class Core {
* - make and save instances
* - configure
*/
public init() {
public init(): void {
/**
* Make modules instances and save it to the @property this.moduleInstances
*/
@ -250,9 +269,10 @@ export default class Core {
* Start Editor!
*
* 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 = [
'Tools',
'UI',
@ -275,7 +295,7 @@ export default class Core {
}
// _.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
*/
private constructModules(): void {
modules.forEach( (module) => {
modules.forEach((module) => {
/**
* If module has non-default exports, passed object contains them all and default export as 'default' property
*/
const Module = typeof module === 'function' ? module : module.default;
try {
/**
* We use class name provided by displayName property
*
* 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
*
* @see https://www.npmjs.com/package/babel-plugin-class-display-name
*/
this.moduleInstances[Module.displayName] = new Module({
config : this.configuration,
config: this.configuration,
});
} catch ( e ) {
_.log(`Module ${Module.displayName} skipped because`, 'warn', e);
} catch (e) {
_.log(`Module ${Module.displayName} skipped because`, 'warn', e);
}
});
}
@ -321,7 +341,7 @@ export default class Core {
*/
private configureModules(): void {
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
*/
@ -332,6 +352,7 @@ export default class Core {
/**
* Return modules without passed name
*
* @param {string} name - module for witch modules difference should be calculated
*/
private getModulesDiff(name: string): EditorModules {

View file

@ -1,11 +1,14 @@
import * as _ from './utils';
/**
* DOM manipulations helper
*/
export default class Dom {
/**
* 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 {
return tag.tagName && [
@ -31,10 +34,10 @@ export default class Dom {
/**
* Check if element is BR or WBR
*
* @param {HTMLElement} element
* @return {boolean}
* @param {HTMLElement} element - element to check
* @returns {boolean}
*/
public static isLineBreakTag(element: HTMLElement) {
public static isLineBreakTag(element: HTMLElement): element is HTMLBRElement {
return element && element.tagName && [
'BR',
'WBR',
@ -44,22 +47,23 @@ export default class Dom {
/**
* Helper for making Elements with classname and attributes
*
* @param {string} tagName - new Element tag name
* @param {array|string} classNames - list or name of CSS classname(s)
* @param {Object} attributes - any attributes
* @return {HTMLElement}
* @param {string} tagName - new Element tag name
* @param {string[]|string} [classNames] - list or name of CSS classname(s)
* @param {object} [attributes] - any attributes
*
* @returns {HTMLElement}
*/
public static make(tagName: string, classNames: string|string[] = null, attributes: object = {}): HTMLElement {
const el = document.createElement(tagName);
if ( Array.isArray(classNames) ) {
if (Array.isArray(classNames)) {
el.classList.add(...classNames);
} else if ( classNames ) {
} else if (classNames) {
el.classList.add(classNames);
}
for (const attrName in attributes) {
if (attributes.hasOwnProperty(attrName)) {
if (Object.prototype.hasOwnProperty.call(attributes, attrName)) {
el[attrName] = attributes[attrName];
}
}
@ -69,8 +73,10 @@ export default class Dom {
/**
* 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 {
return document.createTextNode(content);
@ -78,12 +84,14 @@ export default class Dom {
/**
* Creates SVG icon linked to the sprite
*
* @param {string} name - name (id) of icon from sprite
* @param {number} width
* @param {number} height
* @return {SVGElement}
* @param {number} [width] - icon width
* @param {number} [height] - icon height
*
* @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');
icon.classList.add('icon', 'icon--' + name);
@ -97,15 +105,15 @@ export default class Dom {
/**
* Append one or several elements to the parent
*
* @param {Element|DocumentFragment} parent - where to append
* @param {Element|Element[]|Text|Text[]} elements - element or elements list
* @param {Element|DocumentFragment} parent - where to append
* @param {Element|Element[]|DocumentFragment|Text|Text[]} elements - element or elements list
*/
public static append(
parent: Element|DocumentFragment,
elements: Element|Element[]|DocumentFragment|Text|Text[],
elements: Element|Element[]|DocumentFragment|Text|Text[]
): void {
if ( Array.isArray(elements) ) {
elements.forEach( (el) => parent.appendChild(el) );
if (Array.isArray(elements)) {
elements.forEach((el) => parent.appendChild(el));
} else {
parent.appendChild(elements);
}
@ -118,9 +126,9 @@ export default class Dom {
* @param {Element|Element[]} elements - element or elements list
*/
public static prepend(parent: Element, elements: Element|Element[]): void {
if ( Array.isArray(elements) ) {
if (Array.isArray(elements)) {
elements = elements.reverse();
elements.forEach( (el) => parent.prepend(el) );
elements.forEach((el) => parent.prepend(el));
} else {
parent.prepend(elements);
}
@ -128,6 +136,7 @@ export default class Dom {
/**
* Swap two elements in parent
*
* @param {HTMLElement} el1 - from
* @param {HTMLElement} el2 - to
* @deprecated
@ -135,7 +144,7 @@ export default class Dom {
public static swap(el1: HTMLElement, el2: HTMLElement): void {
// create marker element and insert it where el1 is
const temp = document.createElement('div'),
parent = el1.parentNode;
parent = el1.parentNode;
parent.insertBefore(temp, el1);
@ -155,7 +164,7 @@ export default class Dom {
* Returns first match
*
* @param {Element} el - element we searching inside. Default - DOM Document
* @param {String} selector - searching string
* @param {string} selector - searching string
*
* @returns {Element}
*/
@ -166,10 +175,10 @@ export default class Dom {
/**
* Get Element by Id
*
* @param {string} id
* @param {string} id - id to find
* @returns {HTMLElement | null}
*/
public static get(id: string): HTMLElement {
public static get(id: string): HTMLElement | null {
return document.getElementById(id);
}
@ -178,14 +187,44 @@ export default class Dom {
*
* Returns all matches
*
* @param {Element} el - element we searching inside. Default - DOM Document
* @param {String} selector - searching string
* @param {Element|Document} el - element we searching inside. Default - DOM Document
* @param {string} selector - searching string
*
* @returns {NodeList}
*/
public static findAll(el: Element|Document = document, selector: string): NodeList {
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.
* 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
* {@link https://en.wikipedia.org/wiki/Depth-first_search}
* @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
* @param {boolean} [atLast] - find last text node
*
* @returns {Node} - it can be text Node or Element Node, so that caret will able to work with it
*/
public static getDeepestNode(node: Node, atLast: boolean = false): Node {
public static getDeepestNode(node: Node, atLast = false): Node {
/**
* Current function have two directions:
* - starts from first child and every time gets first or nextSibling in special cases
* - starts from last child and gets last or previousSibling
*
* @type {string}
*/
const child = atLast ? 'lastChild' : 'firstChild',
sibling = atLast ? 'previousSibling' : 'nextSibling';
sibling = atLast ? 'previousSibling' : 'nextSibling';
if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) {
let nodeChild = node[child] as Node;
@ -245,9 +286,11 @@ export default class Dom {
/**
* Check if object is DOM node
*
* @param {Object} node
* @param {*} node - object to check
*
* @returns {boolean}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static isElement(node: any): node is Element {
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
*
* @param {Object} node
* @param {object} node - object to check
* @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;
}
/**
* 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 {
return element.contentEditable === 'true';
@ -273,10 +319,13 @@ export default class Dom {
/**
* 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 = [
'INPUT',
'TEXTAREA',
@ -287,14 +336,16 @@ export default class Dom {
/**
* 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 {
let result = true;
if (Dom.isNativeInput(target)) {
const inputElement = target as HTMLInputElement;
switch (inputElement.type) {
switch (target.type) {
case 'file':
case 'checkbox':
case 'radio':
@ -309,6 +360,7 @@ export default class Dom {
} else {
result = Dom.isContentEditable(target);
}
return result;
}
@ -318,8 +370,9 @@ export default class Dom {
* @description Method checks simple Node without any childs for emptiness
* If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method
*
* @param {Node} node
* @return {Boolean} true if it is empty
* @param {Node} node - node to check
*
* @returns {boolean} true if it is empty
*/
public static isNodeEmpty(node: Node): boolean {
let nodeText;
@ -328,7 +381,7 @@ export default class Dom {
return false;
}
if ( this.isElement(node) && this.isNativeInput(node) ) {
if (this.isElement(node) && this.isNativeInput(node)) {
nodeText = (node as HTMLInputElement).value;
} else {
nodeText = node.textContent.replace('\u200B', '');
@ -339,8 +392,10 @@ export default class Dom {
/**
* 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 {
if (!node) {
@ -356,63 +411,42 @@ export default class Dom {
*
* @description Pushes to stack all DOM leafs and checks for emptiness
*
* @param {Node} node
* @return {boolean}
* @param {Node} node - node to check
* @returns {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
*/
node.normalize();
treeWalker.push(node.firstChild);
const treeWalker = [ node ];
while ( treeWalker.length > 0 ) {
while (treeWalker.length > 0) {
node = treeWalker.shift();
if (!node) { continue; }
if ( this.isLeaf(node) ) {
leafs.push(node);
} else {
treeWalker.push(node.firstChild);
if (!node) {
continue;
}
while ( node && node.nextSibling ) {
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)) {
if (this.isLeaf(node) && !this.isNodeEmpty(node)) {
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
*
* @param {string} str - string to check
*
* @returns {boolean}
* @param {String} str
*/
public static isHTMLString(str: string): boolean {
const wrapper = Dom.make('div');
@ -425,7 +459,8 @@ export default class Dom {
/**
* Return length of node`s text content
*
* @param {Node} node
* @param {Node} node - node with content
*
* @returns {number}
*/
public static getContentLength(node: Node): number {
@ -445,7 +480,7 @@ export default class Dom {
*
* @returns {string[]}
*/
static get blockElements(): string[] {
public static get blockElements(): string[] {
return [
'address',
'article',
@ -491,7 +526,8 @@ export default class Dom {
* Check if passed content includes only inline elements
*
* @param {string|HTMLElement} data - element or html string
* @return {boolean}
*
* @returns {boolean}
*/
public static containsOnlyInlineElements(data: string | HTMLElement): boolean {
let wrapper: HTMLElement;
@ -503,9 +539,9 @@ export default class Dom {
wrapper = data;
}
const check = (element: HTMLElement) => {
return !Dom.blockElements.includes(element.tagName.toLowerCase())
&& Array.from(element.children).every(check);
const check = (element: HTMLElement): boolean => {
return !Dom.blockElements.includes(element.tagName.toLowerCase()) &&
Array.from(element.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)
*
* @param {HTMLElement} parent
* @param {HTMLElement} parent - root element
*
* @return {HTMLElement[]}
* @returns {HTMLElement[]}
*/
public static getDeepestBlockElements(parent: HTMLElement): HTMLElement[] {
if (Dom.containsOnlyInlineElements(parent)) {
return [parent];
return [ parent ];
}
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
* @param element
*
* @param {string | HTMLElement} element - holder's id or holder's HTML Element
*
* @returns {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;
}
/**
* Method checks passed Node if it is some extension Node
*
* @param {Node} node - any node
*
* @returns {boolean}
*/
public static isExtensionNode(node: Node): boolean {
const extensions = [
@ -552,9 +597,11 @@ export default class Dom {
/**
* 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';
}
}

View file

@ -1,4 +1,6 @@
import Dom from './dom';
import * as _ from './utils';
import SelectionUtils from './selection';
/**
* Iterator above passed Elements list.
@ -7,6 +9,7 @@ import Dom from './dom';
export default class DomIterator {
/**
* This is a static property that defines iteration directions
*
* @type {{RIGHT: string, LEFT: string}}
*/
public static directions = {
@ -22,9 +25,10 @@ export default class DomIterator {
/**
* Focused button index.
* Default is -1 which means nothing is active
*
* @type {number}
*/
private cursor: number = -1;
private cursor = -1;
/**
* Items to flip
@ -37,7 +41,7 @@ export default class DomIterator {
*/
constructor(
nodeList: HTMLElement[],
focusedCssClass: string,
focusedCssClass: string
) {
this.items = nodeList || [];
this.focusedCssClass = focusedCssClass;
@ -45,7 +49,8 @@ export default class DomIterator {
/**
* Returns Focused button Node
* @return {HTMLElement}
*
* @returns {HTMLElement}
*/
public get currentItem(): HTMLElement {
if (this.cursor === -1) {
@ -57,7 +62,8 @@ export default class DomIterator {
/**
* Sets items. Can be used when iterable items changed dynamically
* @param {HTMLElement[]} nodeList
*
* @param {HTMLElement[]} nodeList - nodes to iterate
*/
public setItems(nodeList: HTMLElement[]): void {
this.items = nodeList;
@ -93,7 +99,7 @@ export default class DomIterator {
* Leafs nodes inside the target list from active element
*
* @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 {
/**
@ -138,6 +144,7 @@ export default class DomIterator {
if (direction === DomIterator.directions.RIGHT) {
/**
* If we go right then choose next (+1) Tool
*
* @type {number}
*/
focusedButtonIndex = (focusedButtonIndex + 1) % this.items.length;
@ -145,16 +152,17 @@ export default class DomIterator {
/**
* If we go left then choose previous (-1) Tool
* Before counting module we need to add length before because of "The JavaScript Modulo Bug"
*
* @type {number}
*/
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
*
* @interface FlipperOptions
*/
export interface FlipperOptions {
/**
@ -29,28 +31,30 @@ export interface FlipperOptions {
/**
* 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
*/
export default class Flipper {
/**
* Instance of flipper iterator
*
* @type {DomIterator|null}
*/
private readonly iterator: DomIterator = null;
/**
* Flag that defines activation status
*
* @type {boolean}
*/
private activated: boolean = false;
private activated = false;
/**
* Flag that allows arrows usage to flip items
*
* @type {boolean}
*/
private readonly allowArrows: boolean = true;
@ -58,62 +62,21 @@ export default class Flipper {
/**
* Call back for button click/enter
*/
private readonly activateCallback: () => void;
private readonly activateCallback: (item: HTMLElement) => void;
/**
* @constructor
*
* @param {FlipperOptions} options - different constructing settings
* @
*/
constructor(options: FlipperOptions) {
this.allowArrows = typeof options.allowArrows === 'boolean' ? options.allowArrows : true;
this.iterator = new DomIterator(options.items, options.focusedItemClass);
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
* 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)
*/
public static get usedKeys(): number[] {
@ -129,6 +92,7 @@ export default class Flipper {
/**
* Active tab/arrows handling by flipper
*
* @param {HTMLElement[]} items - Some modules (like, InlineToolbar, BlockSettings) might refresh buttons dynamically
*/
public activate(items?: HTMLElement[]): void {
@ -137,6 +101,13 @@ export default class Flipper {
if (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 {
this.activated = false;
this.dropCursor();
document.removeEventListener('keydown', this.onKeyDown);
}
/**
* Return current focused button
* @return {HTMLElement|null}
*
* @returns {HTMLElement|null}
*/
public get currentItem(): HTMLElement|null {
return this.iterator.currentItem;
@ -163,19 +137,73 @@ export default class Flipper {
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
*
* @see DomIterator#dropCursor
*/
private dropCursor(): void {
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
* 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 {
const handlingKeyCodeList = [
@ -183,30 +211,29 @@ export default class Flipper {
_.keyCodes.ENTER,
];
if (this.allowArrows) {
const isCurrentItemIsFocusedInput = this.iterator.currentItem == document.activeElement;
if (this.allowArrows && !isCurrentItemIsFocusedInput) {
handlingKeyCodeList.push(
_.keyCodes.LEFT,
_.keyCodes.RIGHT,
_.keyCodes.UP,
_.keyCodes.DOWN,
_.keyCodes.DOWN
);
}
if (!this.activated || handlingKeyCodeList.indexOf(event.keyCode) === -1) {
return false;
}
return true;
return this.activated && handlingKeyCodeList.indexOf(event.keyCode) !== -1;
}
/**
* When flipper is activated tab press will leaf the items
* @param {KeyboardEvent} event
*
* @param {KeyboardEvent} event - tab keydown event
*/
private handleTabPress(event: KeyboardEvent): void {
/** this property defines leaf direction */
const shiftKey = event.shiftKey,
direction = shiftKey ? DomIterator.directions.LEFT : DomIterator.directions.RIGHT;
direction = shiftKey ? DomIterator.directions.LEFT : DomIterator.directions.RIGHT;
switch (direction) {
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
* @param {KeyboardEvent} event
*
* @param {KeyboardEvent} event - enter keydown event
*/
private handleEnterPress(event: KeyboardEvent): void {
if (!this.activated) {
@ -246,7 +260,7 @@ export default class Flipper {
}
if (typeof this.activateCallback === 'function') {
this.activateCallback();
this.activateCallback(this.iterator.currentItem);
}
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 {API, InlineTool, SanitizerConfig} from '../../../types';
import { InlineTool, SanitizerConfig } from '../../../types';
/**
* Bold Tool
@ -9,25 +9,25 @@ import {API, InlineTool, SanitizerConfig} from '../../../types';
* Makes selected text bolder
*/
export default class BoldInlineTool implements InlineTool {
/**
* Specifies Tool as Inline Toolbar Tool
*
* @return {boolean}
* @returns {boolean}
*/
public static isInline = true;
/**
* Title for hover-tooltip
*/
public static title: string = 'Bold';
public static title = 'Bold';
/**
* Sanitizer Rule
* Leave <b> tags
* @return {object}
*
* @returns {object}
*/
static get sanitize(): SanitizerConfig {
public static get sanitize(): SanitizerConfig {
return {
b: {},
} as SanitizerConfig;
@ -62,12 +62,14 @@ export default class BoldInlineTool implements InlineTool {
this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('bold', 12, 14));
return this.nodes.button;
}
/**
* Wrap range with <b> tag
* @param {Range} range
*
* @param {Range} range - range to wrap
*/
public surround(range: Range): void {
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
* @param {Selection} selection
*
* @param {Selection} selection - selection to check
*
* @returns {boolean}
*/
public checkState(selection: Selection): boolean {
const isActive = document.queryCommandState(this.commandName);
this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);
return isActive;
}
/**
* Set a shortcut
*
* @returns {boolean}
*/
public get shortcut(): string {
return 'CMD+B';

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import Module from '../../__module';
import {Events} from '../../../../types/api';
import { Events } from '../../../../types/api';
/**
* @class EventsAPI
@ -8,20 +8,22 @@ import {Events} from '../../../../types/api';
export default class EventsAPI extends Module {
/**
* Available methods
* @return {Events}
*
* @returns {Events}
*/
get methods(): Events {
public get methods(): Events {
return {
emit: (eventName: string, data: object) => this.emit(eventName, data),
off: (eventName: string, callback: () => void) => this.off(eventName, callback),
on: (eventName: string, callback: () => void) => this.on(eventName, callback),
emit: (eventName: string, data: object): void => this.emit(eventName, data),
off: (eventName: string, callback: () => void): void => this.off(eventName, callback),
on: (eventName: string, callback: () => void): void => this.on(eventName, callback),
};
}
/**
* 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 {
this.Editor.Events.on(eventName, callback);
@ -29,8 +31,9 @@ export default class EventsAPI extends Module {
/**
* 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 {
this.Editor.Events.emit(eventName, data);
@ -38,11 +41,11 @@ export default class EventsAPI extends Module {
/**
* 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 {
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
* @copyright <CodeX Team> 2018
* @copyright <CodeX> 2018
*
* 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
*/
import Module from '../../__module';
import {API as APIInterfaces} from '../../../../types';
import { API as APIInterfaces } from '../../../../types';
import { ToolType } from '../tools';
/**
* @class API
*/
export default class API extends Module {
/**
* Editor.js Core API modules
*/
public get methods(): APIInterfaces {
return {
blocks: this.Editor.BlocksAPI.methods,
@ -26,6 +30,24 @@ export default class API extends Module {
toolbar: this.Editor.ToolbarAPI.methods,
inlineToolbar: this.Editor.InlineToolbarAPI.methods,
tooltip: this.Editor.TooltipAPI.methods,
i18n: this.Editor.I18nAPI.methods,
} 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 {
/**
* Available methods
* @return {InlineToolbar}
*
* @returns {InlineToolbar}
*/
get methods(): InlineToolbar {
public get methods(): InlineToolbar {
return {
close: () => this.close(),
open: () => this.open(),
close: (): void => this.close(),
open: (): void => this.open(),
};
}

View file

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

View file

@ -1,19 +1,26 @@
import Module from '../../__module';
import {Notifier} from '../../../../types/api';
import {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier';
import { Notifier } from '../../../../types/api';
import { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier';
/**
*
*/
export default class NotifierAPI extends Module {
/**
* Available methods
*/
get methods(): Notifier {
public get methods(): Notifier {
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);
}
}

View file

@ -1,5 +1,6 @@
import Module from '../../__module';
import {Sanitizer} from '../../../../types/api';
import { Sanitizer } from '../../../../types/api';
import { SanitizerConfig } from '../../../../types/configs';
/**
* @class SanitizerAPI
@ -8,16 +9,24 @@ import {Sanitizer} from '../../../../types/api';
export default class SanitizerAPI extends Module {
/**
* Available methods
* @return {Sanitizer}
*
* @returns {Sanitizer}
*/
get methods(): Sanitizer {
public get methods(): Sanitizer {
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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,10 +6,13 @@ import * as _ from '../utils';
import SelectionUtils from '../selection';
import Flipper from '../flipper';
/**
*
*/
export default class BlockEvents extends Module {
/**
* All keydowns on Block
*
* @param {KeyboardEvent} event - keydown
*/
public keydown(event: KeyboardEvent): void {
@ -43,18 +46,12 @@ export default class BlockEvents extends Module {
case _.keyCodes.TAB:
this.tabPressed(event);
break;
case _.keyCodes.ESC:
this.escapePressed(event);
break;
default:
this.defaultHandler();
break;
}
}
/**
* Fires on keydown before event processing
*
* @param {KeyboardEvent} event - keydown
*/
public beforeKeydownProcessing(event: KeyboardEvent): void {
@ -77,6 +74,7 @@ export default class BlockEvents extends Module {
/**
* Allow to use shortcuts with selected blocks
*
* @type {boolean}
*/
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
@ -92,9 +90,10 @@ export default class BlockEvents extends Module {
* Key up on Block:
* - shows Inline Toolbar if something selected
* - shows conversion toolbar with 85% of block selection
*
* @param {KeyboardEvent} event - keyup event
*/
public keyup(event): void {
/**
* 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();
}
/**
* Mouse up on Block:
*/
public mouseUp(): void {
}
/**
* Set up mouse selection handlers
*
* @param {MouseEvent} event
* @param {MouseEvent} event - mouse down event
*/
public mouseDown(event: MouseEvent): void {
/**
@ -131,7 +124,8 @@ export default class BlockEvents extends Module {
/**
* Open Toolbox to leaf Tools
* @param {KeyboardEvent} event
*
* @param {KeyboardEvent} event - tab keydown event
*/
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
*
* @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);
block.dropTarget = true;
@ -199,9 +168,9 @@ export default class BlockEvents extends Module {
/**
* 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);
block.dropTarget = false;
@ -211,7 +180,7 @@ export default class BlockEvents extends Module {
* Copying selected blocks
* 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 {
const { BlockSelection } = this.Editor;
@ -226,7 +195,8 @@ export default class BlockEvents extends Module {
/**
* Copy and Delete selected Blocks
* @param {ClipboardEvent} event
*
* @param {ClipboardEvent} event - clipboard event
*/
public handleCommandX(event: ClipboardEvent): void {
const { BlockSelection, BlockManager, Caret } = this.Editor;
@ -247,6 +217,7 @@ export default class BlockEvents extends Module {
/**
* ENTER pressed on block
*
* @param {KeyboardEvent} event - keydown
*/
private enter(event: KeyboardEvent): void {
@ -314,6 +285,7 @@ export default class BlockEvents extends Module {
/**
* Handle backspace keydown on Block
*
* @param {KeyboardEvent} event - keydown
*/
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
*/
if (currentBlock.selected || currentBlock.isEmpty && currentBlock.currentInput === currentBlock.firstInput) {
if (currentBlock.selected || (currentBlock.isEmpty && currentBlock.currentInput === currentBlock.firstInput)) {
event.preventDefault();
const index = BlockManager.currentBlockIndex;
@ -339,7 +311,7 @@ export default class BlockEvents extends Module {
Caret.setToBlock(
BlockManager.currentBlock,
index ? Caret.positions.END : Caret.positions.START,
index ? Caret.positions.END : Caret.positions.START
);
/** Close Toolbar */
@ -347,6 +319,7 @@ export default class BlockEvents extends Module {
/** Clear selection */
BlockSelection.clearSelection(event);
return;
}
@ -382,7 +355,7 @@ export default class BlockEvents extends Module {
/**
* Merge current and previous Blocks if they have the same type
*/
private mergeBlocks() {
private mergeBlocks(): void {
const { BlockManager, Caret, Toolbar } = this.Editor;
const targetBlock = BlockManager.previousBlock;
const blockToMerge = BlockManager.currentBlock;
@ -401,6 +374,7 @@ export default class BlockEvents extends Module {
Caret.setToBlock(BlockManager.currentBlock);
Toolbar.close();
return;
}
@ -413,7 +387,7 @@ export default class BlockEvents extends Module {
Caret.createShadow(targetBlock.pluginsContent);
BlockManager.mergeBlocks(targetBlock, blockToMerge)
.then( () => {
.then(() => {
/** Restore caret position after merge */
Caret.restoreCaret(targetBlock.pluginsContent as HTMLElement);
targetBlock.pluginsContent.normalize();
@ -423,6 +397,8 @@ export default class BlockEvents extends Module {
/**
* Handle right and down keyboard keys
*
* @param {KeyboardEvent} event - keyboard event
*/
private arrowRightAndDown(event: KeyboardEvent): void {
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) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState();
return;
}
@ -474,6 +451,8 @@ export default class BlockEvents extends Module {
/**
* Handle left and up keyboard keys
*
* @param {KeyboardEvent} event - keyboard event
*/
private arrowLeftAndUp(event: KeyboardEvent): void {
/**
@ -498,6 +477,7 @@ export default class BlockEvents extends Module {
if (event.shiftKey && event.keyCode === _.keyCodes.UP && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState(false);
return;
}
@ -524,20 +504,17 @@ export default class BlockEvents extends Module {
this.Editor.BlockSelection.clearSelection(event);
}
/**
* Default keydown handler
*/
private defaultHandler(): void {}
/**
* 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),
blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened),
inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened),
conversionToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.ConversionToolbar.opened),
flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened),
inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened),
conversionToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.ConversionToolbar.opened),
flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
/**
* 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
* 3. When Toolbar's component is opened and some its item selected
*/
return !(event.shiftKey
|| flippingToolbarItems
|| toolboxItemSelected
|| blockSettingsItemSelected
|| inlineToolbarItemSelected
|| conversionToolbarItemSelected
return !(event.shiftKey ||
flippingToolbarItems ||
toolboxItemSelected ||
blockSettingsItemSelected ||
inlineToolbarItemSelected ||
conversionToolbarItemSelected
);
}
@ -559,7 +536,7 @@ export default class BlockEvents extends Module {
*/
private activateToolbox(): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(false , false);
this.Editor.Toolbar.open(false, false);
this.Editor.Toolbar.plusButton.show();
}

View file

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

View file

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

View file

@ -19,14 +19,13 @@ import * as _ from '../utils';
* @typedef {Caret} Caret
*/
export default class Caret extends Module {
/**
* Allowed caret positions in input
*
* @static
* @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 {
START: 'start',
END: 'end',
@ -45,7 +44,8 @@ export default class Caret extends Module {
/**
* Get's deepest first node and checks if offset is zero
* @return {boolean}
*
* @returns {boolean}
*/
public get isAtStart(): boolean {
const selection = Selection.get();
@ -65,6 +65,7 @@ export default class Caret extends Module {
/**
* Workaround case when caret in the text like " |Hello!"
* selection.anchorOffset is 1, but real caret visible position is 0
*
* @type {number}
*/
@ -84,6 +85,7 @@ export default class Caret extends Module {
* So we use child with focusOffset index as new anchorNode.
*/
let focusOffset = selection.focusOffset;
if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {
if (focusNode.childNodes[focusOffset]) {
focusNode = focusNode.childNodes[focusOffset];
@ -106,6 +108,7 @@ export default class Caret extends Module {
const nothingAtLeft = leftSiblings.every((node) => {
/**
* Workaround case when block starts with several <br>'s (created by SHIFT+ENTER)
*
* @see https://github.com/codex-team/editor.js/issues/726
* We need to allow to delete such linebreaks, so in this case caret IS NOT AT START
*/
@ -128,12 +131,13 @@ export default class Caret extends Module {
* We use <= comparison for case:
* "| 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
* @return {boolean}
*
* @returns {boolean}
*/
public get isAtEnd(): boolean {
const selection = Selection.get();
@ -161,6 +165,7 @@ export default class Caret extends Module {
* So we use child with anchofocusOffset - 1 as new focusNode.
*/
let focusOffset = selection.focusOffset;
if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {
if (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);
return (isLastBR) || $.isEmpty(node) && !$.isLineBreakTag(node);
return isLastBR || ($.isEmpty(node) && !$.isLineBreakTag(node));
});
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
*
* @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
* @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 {
const {BlockManager} = this.Editor;
public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void {
const { BlockManager } = this.Editor;
let element;
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
*/
_.delay( () => {
_.delay(() => {
this.set(nodeToSet as HTMLElement, offset);
}, 20)();
@ -267,12 +272,12 @@ export default class Caret extends Module {
* Set caret to the current input of current Block.
*
* @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
* @param {number} offset - caret offset regarding to the text node
*/
public setToInput(input: HTMLElement, position: string = this.positions.DEFAULT, offset: number = 0): void {
const {currentBlock} = this.Editor.BlockManager;
public setToInput(input: HTMLElement, position: string = this.positions.DEFAULT, offset = 0): void {
const { currentBlock } = this.Editor.BlockManager;
const nodeToSet = $.getDeepestNode(input);
switch (position) {
@ -281,9 +286,7 @@ export default class Caret extends Module {
break;
case this.positions.END:
const contentLength = $.getContentLength(nodeToSet);
this.set(nodeToSet as HTMLElement, contentLength);
this.set(nodeToSet as HTMLElement, $.getContentLength(nodeToSet));
break;
default:
@ -297,39 +300,24 @@ export default class Caret extends Module {
/**
* Creates Document Range and sets caret to the element with offset
*
* @param {HTMLElement} element - target node.
* @param {Number} offset - offset
* @param {number} offset - offset
*/
public set(element: HTMLElement, offset: number = 0): void {
const range = document.createRange(),
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);
public set(element: HTMLElement, offset = 0): void {
const { top, bottom } = Selection.setCursor(element, offset);
/** If new cursor position is not visible, scroll to it */
const {top, bottom} = element.nodeType === Node.ELEMENT_NODE
? element.getBoundingClientRect()
: range.getBoundingClientRect();
const {innerHeight} = window;
const { innerHeight } = window;
if (top < 0) { window.scrollBy(0, top); }
if (bottom > innerHeight) { window.scrollBy(0, bottom - innerHeight); }
if (top < 0) {
window.scrollBy(0, top);
}
if (bottom > innerHeight) {
window.scrollBy(0, bottom - innerHeight);
}
}
/**
* Set Caret to the last 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.setStart(selectRange.endContainer, selectRange.endOffset);
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
* 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 {
const {currentBlock, nextContentfulBlock} = this.Editor.BlockManager;
const {nextInput} = currentBlock;
public navigateNext(force = false): boolean {
const { BlockManager, Tools } = this.Editor;
const { currentBlock, nextContentfulBlock } = BlockManager;
const { nextInput } = currentBlock;
if (!nextContentfulBlock && !nextInput) {
return false;
let nextBlock = nextContentfulBlock;
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 next Tool`s input exists, focus on it. Otherwise set caret to the next Block */
if (!nextInput) {
this.setToBlock(nextContentfulBlock, this.positions.START);
this.setToBlock(nextBlock, this.positions.START);
} else {
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
* 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 {
const {currentBlock, previousContentfulBlock} = this.Editor.BlockManager;
public navigatePrevious(force = false): boolean {
const { currentBlock, previousContentfulBlock } = this.Editor.BlockManager;
if (!currentBlock) {
return false;
}
const {previousInput} = currentBlock;
const { previousInput } = currentBlock;
if (!previousContentfulBlock && !previousInput) {
return false;
@ -432,10 +435,11 @@ export default class Caret extends Module {
if (force || this.isAtStart) {
/** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */
if (!previousInput) {
this.setToBlock( previousContentfulBlock, this.positions.END );
this.setToBlock(previousContentfulBlock, this.positions.END);
} else {
this.setToInput(previousInput, this.positions.END);
}
return true;
}
@ -444,18 +448,20 @@ export default class Caret extends Module {
/**
* 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');
shadowCaret.classList.add(Caret.CSS.shadowCaret);
element.insertAdjacentElement('beforeEnd', shadowCaret);
element.insertAdjacentElement('beforeend', shadowCaret);
}
/**
* Restores caret position
* @param {HTMLElement} element
*
* @param {HTMLElement} element - element where caret should be restored
*/
public restoreCaret(element: HTMLElement): void {
const shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`);
@ -519,18 +525,21 @@ export default class Caret extends Module {
*
* @example
* <div contenteditable>
* <p></p> |
* <p></p> | left first-level siblings
* <p></p> |
* <blockquote><a><b>adaddad</b><a><blockquote> <-- passed node for example <b>
* <p></p> |
* <p></p> | right first-level siblings
* <p></p> |
* <p></p> |
* <p></p> | left first-level siblings
* <p></p> |
* <blockquote><a><b>adaddad</b><a><blockquote> <-- passed node for example <b>
* <p></p> |
* <p></p> | right first-level siblings
* <p></p> |
* </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;
const siblings = [];

View file

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

View file

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

View file

@ -11,23 +11,23 @@ import Module from '../__module';
* @version 1.0.0
*
* @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 {
/**
* Object with events` names as key and array of callback functions as value
*
* @type {{}}
*/
private subscribers: {[name: string]: Array<(data?: any) => any>} = {};
private subscribers: {[name: string]: Array<(data?: object) => object>} = {};
/**
* Subscribe any event on callback
*
* @param {String} eventName - event name
* @param {string} eventName - event name
* @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)) {
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.
*
* @param {String} eventName - event name
* @param {string} eventName - event name
* @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)) {
this.subscribers[eventName] = [];
}
const wrappedCallback = (data: any) => {
const wrappedCallback = (data: object): object => {
const result = callback(data);
const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback);
@ -66,10 +66,10 @@ export default class Events extends Module {
/**
* Emit callbacks with passed data
*
* @param {String} eventName - event name
* @param {Object} data - subscribers get this data when they were fired
* @param {string} eventName - event name
* @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]) {
return;
}
@ -77,17 +77,17 @@ export default class Events extends Module {
this.subscribers[eventName].reduce((previousData, currentHandler) => {
const newData = currentHandler(previousData);
return newData ? newData : previousData;
return newData || previousData;
}, data);
}
/**
* Unsubsribe callback from event
* Unsubscribe callback from event
*
* @param eventName
* @param callback
* @param {string} eventName - event name
* @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++) {
if (this.subscribers[eventName][i] === callback) {
delete this.subscribers[eventName][i];

View file

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

View file

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

View file

@ -5,19 +5,18 @@ import Module from '../__module';
*
* @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
*/
export default class Notifier extends Module {
/**
* 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);
}
}

View file

@ -4,12 +4,12 @@ import * as _ from '../utils';
import {
BlockTool,
BlockToolConstructable,
BlockToolData,
PasteConfig,
PasteEvent,
PasteEventDetail,
PasteEventDetail
} from '../../../types';
import Block from '../block';
import { SavedData } from '../../types-internal/block-data';
/**
* Tag substitute object.
@ -17,6 +17,7 @@ import Block from '../block';
interface TagSubstitute {
/**
* Name of related Tool
*
* @type {string}
*/
tool: string;
@ -28,18 +29,21 @@ interface TagSubstitute {
interface PatternSubstitute {
/**
* Pattern`s key
*
* @type {string}
*/
key: string;
/**
* Pattern regexp
*
* @type {RegExp}
*/
pattern: RegExp;
/**
* Name of related Tool
*
* @type {string}
*/
tool: string;
@ -51,12 +55,14 @@ interface PatternSubstitute {
interface FilesSubstitution {
/**
* Array of file extensions Tool can handle
*
* @type {string[]}
*/
extensions: string[];
/**
* Array of MIME types Tool can handle
*
* @type {string[]}
*/
mimeTypes: string[];
@ -64,16 +70,20 @@ interface FilesSubstitution {
/**
* Processed paste data object.
*
* @interface PasteData
*/
interface PasteData {
/**
* Name of related Tool
*
* @type {string}
*/
tool: string;
/**
* Pasted data. Processed and wrapped to HTML element
*
* @type {HTMLElement}
*/
content: HTMLElement;
@ -85,6 +95,7 @@ interface PasteData {
/**
* True if content should be inserted as new Block
*
* @type {boolean}
*/
isBlock: boolean;
@ -99,7 +110,6 @@ interface PasteData {
* @version 2.0.0
*/
export default class Paste extends Module {
/** If string`s length is greater than this number we don't check paste patterns */
public static readonly PATTERN_PROCESSING_MAX_LENGTH = 450;
@ -121,7 +131,7 @@ export default class Paste extends Module {
/** Files` substitutions parameters */
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
*
* @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> {
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
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const includesFiles = types.includes ? types.includes('Files') : (types as any).contains('Files');
if (includesFiles) {
await this.processFiles(dataTransfer.files);
return;
}
const editorJSData = dataTransfer.getData(this.MIME_TYPE);
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
@ -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 (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 */
@ -189,7 +201,7 @@ export default class Paste extends Module {
return result;
}, {});
const customConfig = Object.assign({}, toolsTags, Sanitizer.getAllInlineToolsConfig(), {br: {}});
const customConfig = Object.assign({}, toolsTags, Sanitizer.getAllInlineToolsConfig(), { br: {} });
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 {boolean} isHTML - if passed string is HTML, this parameter should be true
*/
public async processText(data: string, isHTML: boolean = false) {
const {Caret, BlockManager, Tools} = this.Editor;
public async processText(data: string, isHTML = false): Promise<void> {
const { Caret, BlockManager, Tools } = this.Editor;
const dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data);
if (!dataToInsert.length) {
@ -221,6 +233,7 @@ export default class Paste extends Module {
} else {
this.processSingleBlock(dataToInsert.pop());
}
return;
}
@ -228,7 +241,7 @@ export default class Paste extends Module {
const needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;
dataToInsert.map(
async (content, i) => this.insertBlock(content, i === 0 && needToReplaceCurrentBlock),
async (content, i) => this.insertBlock(content, i === 0 && needToReplaceCurrentBlock)
);
if (BlockManager.currentBlock) {
@ -240,7 +253,7 @@ export default class Paste extends Module {
* Set onPaste callback handler
*/
private setCallback(): void {
const {Listeners} = this.Editor;
const { Listeners } = this.Editor;
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
*
* @param {string} name
* @param {Tool} tool
*/
private processTool = ([name, tool]: [string, BlockToolConstructable]): void => {
try {
const toolInstance = new this.Editor.Tools.blockTools[name]({
api: this.Editor.API.methods,
api: this.Editor.API.getMethodsForTool(name),
config: {},
data: {},
}) as BlockTool;
if (tool.pasteConfig === false) {
this.exceptionList.push(name);
return;
}
@ -286,7 +297,7 @@ export default class Paste extends Module {
_.log(
`Paste handling for «${name}» Tool hasn't been set up because of the error`,
'warn',
e,
e
);
}
}
@ -301,12 +312,13 @@ export default class Paste extends Module {
const tags = toolPasteConfig.tags || [];
tags.forEach((tag) => {
if (this.toolsTags.hasOwnProperty(tag)) {
if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
_.log(
`Paste handler for «${name}» Tool on «${tag}» tag is skipped ` +
`because it is already used by «${this.toolsTags[tag].tool}» Tool.`,
'warn',
'warn'
);
return;
}
@ -325,9 +337,8 @@ export default class Paste extends Module {
* @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
*/
private getFilesConfig(name: string, toolPasteConfig: PasteConfig): void {
const {files = {}} = toolPasteConfig;
let {extensions, mimeTypes} = files;
const { files = {} } = toolPasteConfig;
let { extensions, mimeTypes } = files;
if (!extensions && !mimeTypes) {
return;
@ -347,6 +358,7 @@ export default class Paste extends Module {
mimeTypes = mimeTypes.filter((type) => {
if (!_.isValidMimeType(type)) {
_.log(`MIME type value «${type}» for the «${name}» Tool is not a valid MIME type`, 'warn');
return false;
}
@ -376,7 +388,7 @@ export default class Paste extends Module {
if (!(pattern instanceof RegExp)) {
_.log(
`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
*
* @param {EventTarget} element - element where content has been pasted
*
* @returns {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
*
* @param {ClipboardEvent} event
* @param {ClipboardEvent} event - clipboard event
*/
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 (
!BlockManager.currentBlock ||
this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files')
!BlockManager.currentBlock || (this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files'))
) {
return;
}
@ -433,15 +445,15 @@ export default class Paste extends Module {
*
* @param {FileList} items - pasted or dropped items
*/
private async processFiles(items: FileList) {
const {BlockManager, Tools} = this.Editor;
private async processFiles(items: FileList): Promise<void> {
const { BlockManager, Tools } = this.Editor;
let dataToInsert: Array<{type: string, event: PasteEvent}>;
let dataToInsert: Array<{type: string; event: PasteEvent}>;
dataToInsert = await Promise.all(
Array
.from(items)
.map((item) => this.processFile(item)),
.map((item) => this.processFile(item))
);
dataToInsert = dataToInsert.filter((data) => !!data);
@ -451,21 +463,21 @@ export default class Paste extends Module {
dataToInsert.forEach(
(data, i) => {
BlockManager.paste(data.type, data.event, i === 0 && needToReplaceCurrentBlock);
},
}
);
}
/**
* 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 foundConfig = Object
.entries(this.toolsFiles)
.find(([toolName, {mimeTypes, extensions}]) => {
.find(([toolName, { mimeTypes, extensions } ]) => {
const [fileType, fileSubtype] = file.type.split('/');
const foundExt = extensions.find((ext) => ext.toLowerCase() === extension.toLowerCase());
@ -482,7 +494,7 @@ export default class Paste extends Module {
return;
}
const [tool] = foundConfig;
const [ tool ] = foundConfig;
const pasteEvent = this.composePasteEvent('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
*
* @param {string} innerHTML
* @param {string} innerHTML - html string to process
*
* @returns {PasteData[]}
*/
private processHTML(innerHTML: string): PasteData[] {
const {Tools, Sanitizer} = this.Editor;
const { Tools, Sanitizer } = this.Editor;
const initialTool = this.config.initialBlock;
const wrapper = $.make('DIV');
@ -530,7 +543,7 @@ export default class Paste extends Module {
break;
}
const {tags} = Tools.blockTools[tool].pasteConfig as PasteConfig;
const { tags } = Tools.blockTools[tool].pasteConfig as PasteConfig;
const toolTags = tags.reduce((result, tag) => {
result[tag.toLowerCase()] = {};
@ -545,7 +558,12 @@ export default class Paste extends Module {
data: content,
});
return {content, isBlock, tool, event};
return {
content,
isBlock,
tool,
event,
};
})
.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
*
* @param {string} plain
* @param {string} plain - string to process
*
* @returns {PasteData[]}
*/
private processPlain(plain: string): PasteData[] {
const {initialBlock} = this.config as {initialBlock: string},
{Tools} = this.Editor;
const { initialBlock } = this.config as {initialBlock: string};
if (!plain) {
return [];
@ -578,18 +596,23 @@ export default class Paste extends Module {
data: content,
});
return {content, tool, isBlock: false, event};
return {
content,
tool,
isBlock: false,
event,
};
});
}
/**
* 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> {
const {Caret, BlockManager, Tools} = this.Editor;
const {currentBlock} = BlockManager;
const { Caret, BlockManager, Tools } = this.Editor;
const { currentBlock } = BlockManager;
/**
* 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)
) {
this.insertBlock(dataToInsert, currentBlock && Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty);
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
* 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> {
const {BlockManager, Caret, Sanitizer, Tools} = this.Editor;
const {content, tool} = dataToInsert;
const { BlockManager, Caret, Sanitizer, Tools } = this.Editor;
const { content } = dataToInsert;
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);
if (blockData) {
let insertedBlock;
const needToReplaceCurrentBlock = BlockManager.currentBlock &&
Tools.isInitial(BlockManager.currentBlock.tool) &&
BlockManager.currentBlock.isEmpty;
const needToReplaceCurrentBlock = BlockManager.currentBlock
&& Tools.isInitial(BlockManager.currentBlock.tool)
&& BlockManager.currentBlock.isEmpty;
insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock);
const insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock);
Caret.setToBlock(insertedBlock, Caret.positions.END);
return;
}
}
@ -644,7 +667,7 @@ export default class Paste extends Module {
document.execCommand(
'insertHTML',
false,
Sanitizer.clean(content.innerHTML, currentToolSanitizeConfig),
Sanitizer.clean(content.innerHTML, currentToolSanitizeConfig)
);
} else {
this.insertBlock(dataToInsert);
@ -654,11 +677,12 @@ export default class Paste extends Module {
/**
* Get patterns` matches
*
* @param {string} text
* @returns Promise<{data: BlockToolData, tool: string}>
* @param {string} text - text to process
*
* @returns {Promise<{event: PasteEvent, tool: string}>}
*/
private async processPattern(text: string): Promise<{event: PasteEvent, tool: string}> {
const pattern = this.toolsPatterns.find((substitute) => {
private async processPattern(text: string): Promise<{event: PasteEvent; tool: string}> {
const pattern = this.toolsPatterns.find((substitute) => {
const execResult = substitute.pattern.exec(text);
if (!execResult) {
@ -686,18 +710,20 @@ export default class Paste extends Module {
/**
* Insert pasted Block content to Editor
*
* @param {PasteData} data
* @param {Boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block
* @param {PasteData} data - data to insert
* @param {boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block
*
* @returns {void}
*/
private insertBlock(data: PasteData, canReplaceCurrentBlock: boolean = false): void {
const {BlockManager, Caret} = this.Editor;
const {currentBlock} = BlockManager;
private insertBlock(data: PasteData, canReplaceCurrentBlock = false): void {
const { BlockManager, Caret } = this.Editor;
const { currentBlock } = BlockManager;
let block: Block;
if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) {
block = BlockManager.paste(data.tool, data.event, true);
Caret.setToBlock(block, Caret.positions.END);
return;
}
@ -709,16 +735,14 @@ export default class Paste extends Module {
/**
* 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;
blocks.forEach(({ tool, data }, i) => {
const settings = this.Editor.Tools.getToolSettings(tool);
let needToReplaceCurrentBlock = false;
if (i === 0) {
@ -727,25 +751,70 @@ export default class Paste extends Module {
needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;
}
if (needToReplaceCurrentBlock) {
BlockManager.replace(tool, data, settings);
} else {
BlockManager.insert(tool, data, settings);
}
BlockManager.insert({
tool,
data,
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:
* 1. Block element
* 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[]}
*/
private getNodes(wrapper: Node): Node[] {
const children = Array.from(wrapper.childNodes),
tags = Object.keys(this.toolsTags);
const children = Array.from(wrapper.childNodes);
let elementNodeProcessingResult: Node[] | void;
const reducer = (nodes: Node[], node: Node): Node[] => {
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
*/
case Node.ELEMENT_NODE:
const element = node as HTMLElement;
elementNodeProcessingResult = this.processElementNode(node, nodes, destNode);
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];
if (elementNodeProcessingResult) {
return elementNodeProcessingResult;
}
break;
@ -803,6 +848,7 @@ export default class Paste extends Module {
*/
case Node.TEXT_NODE:
destNode.appendChild(node);
return [...nodes, destNode];
default:
@ -818,8 +864,8 @@ export default class Paste extends Module {
/**
* Compose paste event with passed type and detail
*
* @param {string} type
* @param {PasteEventDetail} detail
* @param {string} type - event type
* @param {PasteEventDetail} detail - event detail
*/
private composePasteEvent(type: string, detail: PasteEventDetail): PasteEvent {
return new CustomEvent(type, {

View file

@ -11,12 +11,16 @@ import $ from '../dom';
import SelectionUtils from '../selection';
import Block from '../block';
/**
*
*/
export default class RectangleSelection extends Module {
/**
* 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 {
overlay: 'codex-editor-overlay',
overlayContainer: 'codex-editor-overlay__container',
@ -28,9 +32,10 @@ export default class RectangleSelection extends Module {
/**
* Using the selection rectangle
*
* @type {boolean}
*/
private isRectSelectionActivated: boolean = false;
private isRectSelectionActivated = false;
/**
* Speed of Scrolling
@ -56,12 +61,12 @@ export default class RectangleSelection extends Module {
/**
* Mouse is clamped
*/
private mousedown: boolean = false;
private mousedown = false;
/**
* Is scrolling now
*/
private isScrolling: boolean = false;
private isScrolling = false;
/**
* Mouse is in scroll zone
@ -71,10 +76,10 @@ export default class RectangleSelection extends Module {
/**
* Coords of rect
*/
private startX: number = 0;
private startY: number = 0;
private mouseX: number = 0;
private mouseY: number = 0;
private startX = 0;
private startY = 0;
private mouseX = 0;
private mouseY = 0;
/**
* Selected blocks
@ -96,8 +101,8 @@ export default class RectangleSelection extends Module {
* Creating rect and hang handlers
*/
public prepare(): void {
const {Listeners} = this.Editor;
const {container} = this.genHTML();
const { Listeners } = this.Editor;
const { container } = this.genHTML();
Listeners.on(container, 'mousedown', (event: MouseEvent) => {
if (event.button !== this.MAIN_MOUSE_BUTTON) {
@ -127,10 +132,11 @@ export default class RectangleSelection extends Module {
/**
* Init rect params
*
* @param {number} pageX - X 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);
/**
@ -152,7 +158,7 @@ export default class RectangleSelection extends Module {
];
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
@ -169,7 +175,7 @@ export default class RectangleSelection extends Module {
/**
* Clear all params to end selection
*/
public endSelection() {
public endSelection(): void {
this.mousedown = false;
this.startX = 0;
this.startY = 0;
@ -179,22 +185,23 @@ export default class RectangleSelection extends Module {
/**
* is RectSelection Activated
*/
public isRectActivated() {
public isRectActivated(): boolean {
return this.isRectSelectionActivated;
}
/**
* Mark that selection is end
*/
public clearSelection() {
public clearSelection(): void {
this.isRectSelectionActivated = false;
}
/**
* Scroll If mouse in scroll zone
*
* @param {number} clientY - Y coord of mouse
*/
private scrollByZones(clientY) {
private scrollByZones(clientY): void {
this.inScrollZone = null;
if (clientY <= this.HEIGHT_OF_SCROLL_ZONE) {
this.inScrollZone = this.TOP_SCROLL_ZONE;
@ -205,6 +212,7 @@ export default class RectangleSelection extends Module {
if (!this.inScrollZone) {
this.isScrolling = false;
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 overlay = $.make('div', RectangleSelection.CSS.overlay, {});
@ -227,6 +240,7 @@ export default class RectangleSelection extends Module {
container.appendChild(overlay);
this.overlayRectangle = overlayRectangle as HTMLDivElement;
return {
container,
overlay,
@ -235,13 +249,15 @@ export default class RectangleSelection extends Module {
/**
* Activates scrolling if blockSelection is active and mouse is in scroll zone
*
* @param {number} speed - speed of scrolling
*/
private scrollVertical(speed) {
private scrollVertical(speed): void {
if (!(this.inScrollZone && this.mousedown)) {
return;
}
const lastOffset = window.pageYOffset;
window.scrollBy(0, speed);
this.mouseY += window.pageYOffset - lastOffset;
setTimeout(() => {
@ -251,9 +267,10 @@ export default class RectangleSelection extends Module {
/**
* 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) {
return;
}
@ -263,11 +280,12 @@ export default class RectangleSelection extends Module {
this.mouseY = event.pageY;
}
const {rightPos, leftPos, index} = this.genInfoForMouseSelection();
const { rightPos, leftPos, index } = this.genInfoForMouseSelection();
// There is not new block in selection
const rectIsOnRighSideOfredactor = this.startX > rightPos && this.mouseX > rightPos;
const rectISOnLeftSideOfRedactor = this.startX < leftPos && this.mouseX < leftPos;
this.rectCrossesBlocks = !(rectIsOnRighSideOfredactor || rectISOnLeftSideOfRedactor);
if (!this.isRectSelectionActivated) {
@ -294,7 +312,7 @@ export default class RectangleSelection extends Module {
/**
* Shrink rect to singular point
*/
private shrinkRectangleToPoint() {
private shrinkRectangleToPoint(): void {
this.overlayRectangle.style.left = `${this.startX - window.pageXOffset}px`;
this.overlayRectangle.style.top = `${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
*/
private inverseSelection() {
private inverseSelection(): void {
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) {
this.Editor.BlockSelection.selectBlockByIndex(it);
}
}
if (!this.rectCrossesBlocks && isSelecteMode) {
if (!this.rectCrossesBlocks && isSelectedMode) {
for (const it of this.stackOfSelected) {
this.Editor.BlockSelection.unSelectBlockByIndex(it);
}
@ -324,7 +342,7 @@ export default class RectangleSelection extends Module {
/**
* Updates size of rectangle
*/
private updateRectangleSize() {
private updateRectangleSize(): void {
// Depending on the position of the mouse relative to the starting point,
// change this.e distance from the desired edge of the screen*/
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
* @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 centerOfRedactor = widthOfRedactor / 2;
const Y = this.mouseY - window.pageYOffset;
const elementUnderMouse = document.elementFromPoint(centerOfRedactor, Y);
const blockInCurrentPos = this.Editor.BlockManager.getBlockByChildNode(elementUnderMouse);
let index;
if (blockInCurrentPos !== undefined) {
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
*
* @param index - index of block in redactor
*/
private addBlockInSelection(index) {
private addBlockInSelection(index): void {
if (this.rectCrossesBlocks) {
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
*
* @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 sizeStack = this.stackOfSelected.length;
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 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 generalSelection = selectionInDownDurection || selectionInUpDirection || direction === undef;
const generalSelection = selectionInDownDirection || selectionInUpDirection || direction === undef;
const reduction = !generalSelection;
// 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++) {
this.addBlockInSelection(ind);
}
return;
}
@ -417,6 +446,7 @@ export default class RectangleSelection extends Module {
for (let ind = this.stackOfSelected[sizeStack - 1] - 1; ind >= index; ind--) {
this.addBlockInSelection(ind);
}
return;
}
@ -429,9 +459,9 @@ export default class RectangleSelection extends Module {
// cmp for different directions
if (index > this.stackOfSelected[sizeStack - 1]) {
cmp = () => index > this.stackOfSelected[i];
cmp = (): boolean => index > this.stackOfSelected[i];
} else {
cmp = () => index < this.stackOfSelected[i];
cmp = (): boolean => index < this.stackOfSelected[i];
}
// Remove blocks missed due to speed.
@ -443,6 +473,5 @@ export default class RectangleSelection extends Module {
this.stackOfSelected.pop();
i--;
}
return;
}
}

View file

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

View file

@ -20,8 +20,8 @@ import Module from '../__module';
import * as _ from '../utils';
/**
* @typedef {Object} SanitizerConfig
* @property {Object} tags - define tags restrictions
* @typedef {object} SanitizerConfig
* @property {object} tags - define tags restrictions
*
* @example
*
@ -36,8 +36,12 @@ import * as _ from '../utils';
*/
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 {
/**
* Memoize tools config
@ -54,12 +58,11 @@ export default class Sanitizer extends Module {
*
* Enumerate blocks and clean data
*
* @param {{tool, data: BlockToolData}[]} blocksData[]
* @param {Array<{tool, data: BlockToolData}>} blocksData - blocks' data to sanitize
*/
public sanitizeBlocks(
blocksData: Array<{tool: string, data: BlockToolData}>,
): Array<{tool: string, data: BlockToolData}> {
blocksData: Array<Pick<SavedData, 'data' | 'tool'>>
): Array<Pick<SavedData, 'data' | 'tool'>> {
return blocksData.map((block) => {
const toolConfig = this.composeToolConfig(block.tool);
@ -67,7 +70,7 @@ export default class Sanitizer extends Module {
return block;
}
block.data = this.deepSanitize(block.data, toolConfig);
block.data = this.deepSanitize(block.data, toolConfig) as BlockToolData;
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 {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:
* - Array
@ -105,6 +108,7 @@ export default class Sanitizer extends Module {
if (typeof dataToSanitize === 'string') {
return this.cleanOneItem(dataToSanitize, rules);
}
return dataToSanitize;
}
}
@ -116,10 +120,9 @@ export default class Sanitizer extends Module {
* @param {string} taintString - taint string
* @param {SanitizerConfig} customConfig - allowed tags
*
* @return {string} clean HTML
* @returns {string} clean HTML
*/
public clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {
const sanitizerConfig = {
tags: customConfig,
};
@ -128,15 +131,16 @@ export default class Sanitizer extends Module {
* API client can use custom config to manage sanitize process
*/
const sanitizerInstance = this.createHTMLJanitorInstance(sanitizerConfig);
return sanitizerInstance.clean(taintString);
}
/**
* Merge with inline tool config
*
* @param {string} toolName
* @param {SanitizerConfig} toolRules
* @return {SanitizerConfig}
* @param {string} toolName - tool name
*
* @returns {SanitizerConfig}
*/
public composeToolConfig(toolName: string): SanitizerConfig {
/**
@ -160,9 +164,11 @@ export default class Sanitizer extends Module {
const toolRules = toolClass.sanitize;
const toolConfig = {} as SanitizerConfig;
for (const fieldName in toolRules) {
if (toolRules.hasOwnProperty(fieldName)) {
if (Object.prototype.hasOwnProperty.call(toolRules, fieldName)) {
const rule = toolRules[fieldName];
if (typeof rule === 'object') {
toolConfig[fieldName] = Object.assign({}, baseConfig, rule);
} else {
@ -179,9 +185,11 @@ export default class Sanitizer extends Module {
* Returns Sanitizer config
* When Tool's "inlineToolbar" value is True, get all sanitizer rules from all tools,
* otherwise get only enabled
*
* @param {string} name - Inline Tool name
*/
public getInlineToolsConfig(name: string): SanitizerConfig {
const {Tools} = this.Editor;
const { Tools } = this.Editor;
const toolsConfig = Tools.getToolSettings(name);
const enableInlineTools = toolsConfig.inlineToolbar || [];
@ -196,10 +204,10 @@ export default class Sanitizer extends Module {
/**
* getting only enabled
*/
(enableInlineTools as string[]).map( (inlineToolName) => {
(enableInlineTools as string[]).map((inlineToolName) => {
config = Object.assign(
config,
Tools.inline[inlineToolName][Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG],
Tools.inline[inlineToolName][Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]
) as SanitizerConfig;
});
}
@ -217,7 +225,7 @@ export default class Sanitizer extends Module {
* Return general config for all inline tools
*/
public getAllInlineToolsConfig(): SanitizerConfig {
const {Tools} = this.Editor;
const { Tools } = this.Editor;
if (this.inlineToolsConfigCache) {
return this.inlineToolsConfigCache;
@ -226,7 +234,7 @@ export default class Sanitizer extends Module {
const config: SanitizerConfig = {} as SanitizerConfig;
Object.entries(Tools.inline)
.forEach( ([name, inlineTool]: [string, InlineToolConstructable]) => {
.forEach(([, inlineTool]: [string, InlineToolConstructable]) => {
Object.assign(config, inlineTool[Tools.INTERNAL_SETTINGS.SANITIZE_CONFIG]);
});
@ -237,24 +245,26 @@ export default class Sanitizer extends Module {
/**
* 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[] {
return array.map( (arrayItem) => this.deepSanitize(arrayItem, ruleForItem));
private cleanArray(array: Array<object | string>, ruleForItem: SanitizerConfig): Array<object | string> {
return array.map((arrayItem) => this.deepSanitize(arrayItem, ruleForItem));
}
/**
* Clean object
*
* @param {object} object - {level: 0, text: 'adada', items: [1,2,3]}}
* @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 = {};
for (const fieldName in object) {
if (!object.hasOwnProperty(fieldName)) {
if (!Object.prototype.hasOwnProperty.call(object, fieldName)) {
continue;
}
@ -269,13 +279,17 @@ export default class Sanitizer extends Module {
cleanData[fieldName] = this.deepSanitize(currentIterationItem, ruleForItem as SanitizerConfig);
}
return cleanData;
}
/**
* @param {string} taintString
* @param {SanitizerConfig|boolean} rule
* @return {string}
* Clean primitive value
*
* @param {string} taintString - string to clean
* @param {SanitizerConfig|boolean} rule - sanitizer rule
*
* @returns {string}
*/
private cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string {
if (typeof rule === 'object') {
@ -291,7 +305,8 @@ export default class Sanitizer extends Module {
* Check if passed item is a HTML Janitor rule:
* { a : true }, {}, false, true, function(){} correct rules
* undefined, null, 0, 1, 2 not a rules
* @param config
*
* @param {SanitizerConfig} config - config to check
*/
private isRule(config: SanitizerConfig): boolean {
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
* 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
* @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
*/
@ -311,6 +327,7 @@ export default class Sanitizer extends Module {
if (config) {
return new HTMLJanitor(config);
}
return null;
}
}

View file

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

View file

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

View file

@ -1,7 +1,8 @@
import Module from '../../__module';
import $ from '../../dom';
import Flipper, {FlipperOptions} from '../../flipper';
import Flipper, { FlipperOptions } from '../../flipper';
import * as _ from '../../utils';
import SelectionUtils from '../../selection';
/**
* Block Settings
@ -15,12 +16,12 @@ import * as _ from '../../utils';
* |________________________|
*/
export default class BlockSettings extends Module {
/**
* 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 {
opened: 'block-settings-opened',
closed: 'block-settings-closed',
@ -29,9 +30,10 @@ export default class BlockSettings extends Module {
/**
* 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 {
// Settings Panel
wrapper: 'ce-settings',
@ -41,13 +43,14 @@ export default class BlockSettings extends Module {
button: 'ce-settings__button',
focusedButton : 'ce-settings__button--focused',
focusedButton: 'ce-settings__button--focused',
focusedButtonAnimated: 'ce-settings__button--focused-animated',
};
}
/**
* Is Block Settings opened or not
*
* @returns {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
*
* @type {Flipper|null}
*/
private flipper: Flipper = null;
/**
* Page selection utils
*/
private selection: SelectionUtils = new SelectionUtils();
/**
* Panel with block settings with 2 sections:
* - Tool's Settings
* - Default Settings [Move, Remove, etc]
*
* @return {Element}
*/
public make(): void {
this.nodes.wrapper = $.make('div', this.CSS.wrapper);
@ -102,6 +109,12 @@ export default class BlockSettings extends Module {
public open(): void {
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
*/
@ -129,6 +142,19 @@ export default class BlockSettings extends Module {
public close(): void {
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 */
this.nodes.toolSettings.innerHTML = '';
this.nodes.defaultSettings.innerHTML = '';
@ -145,9 +171,12 @@ export default class BlockSettings extends Module {
/**
* Returns Tools Settings and Default Settings
* @return {HTMLElement[]}
*
* @returns {HTMLElement[]}
*/
public get blockTunesButtons(): HTMLElement[] {
const { StylesAPI } = this.Editor;
/**
* Return from cache
* if exists
@ -156,7 +185,10 @@ export default class BlockSettings extends Module {
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}`);
toolSettings.forEach((item) => {
@ -193,14 +225,26 @@ export default class BlockSettings extends Module {
private enableFlipper(): void {
this.flipper = new Flipper({
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.
* 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);
}, 10)();
}, 50)();
},
} as FlipperOptions);
}

View file

@ -1,10 +1,11 @@
import Module from '../../__module';
import $ from '../../dom';
import {BlockToolConstructable} from '../../../../types';
import { BlockToolConstructable } from '../../../../types';
import * as _ from '../../utils';
import {SavedData} from '../../../types-internal/block-data';
import Block from '../../block';
import { SavedData } from '../../../types-internal/block-data';
import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* Block Converter
@ -23,8 +24,8 @@ export default class ConversionToolbar extends Module {
conversionToolHidden: 'ce-conversion-tool--hidden',
conversionToolIcon: 'ce-conversion-tool__icon',
conversionToolFocused : 'ce-conversion-tool--focused',
conversionToolActive : 'ce-conversion-tool--active',
conversionToolFocused: 'ce-conversion-tool--focused',
conversionToolActive: 'ce-conversion-tool--active',
};
}
@ -38,9 +39,10 @@ export default class ConversionToolbar extends Module {
/**
* Conversion Toolbar open/close state
*
* @type {boolean}
*/
public opened: boolean = false;
public opened = false;
/**
* Available tools
@ -49,6 +51,7 @@ export default class ConversionToolbar extends Module {
/**
* Instance of class that responses for leafing buttons by arrows/tab
*
* @type {Flipper|null}
*/
private flipper: Flipper = null;
@ -66,7 +69,7 @@ export default class ConversionToolbar extends Module {
this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);
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
* @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 {
if (!this.opened) {
@ -152,11 +156,12 @@ export default class ConversionToolbar extends Module {
* Replaces one Block with another
* 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> {
/**
* At first, we get current Block data
*
* @type {BlockToolConstructable}
*/
const currentBlockClass = this.Editor.BlockManager.currentBlock.class;
@ -175,6 +180,7 @@ export default class ConversionToolbar extends Module {
/**
* Getting a class of replacing Tool
*
* @type {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
*/
let exportData: string = '';
let exportData = '';
const exportProp = currentBlockClass[INTERNAL_SETTINGS.CONVERSION_CONFIG].export;
if (typeof exportProp === 'function') {
@ -196,6 +202,7 @@ export default class ConversionToolbar extends Module {
} else {
_.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.');
return;
}
@ -204,7 +211,7 @@ export default class ConversionToolbar extends Module {
*/
const cleaned: string = this.Editor.Sanitizer.clean(
exportData,
replacingTool.sanitize,
replacingTool.sanitize
);
/**
@ -222,10 +229,14 @@ export default class ConversionToolbar extends Module {
} else {
_.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.');
return;
}
this.Editor.BlockManager.replace(replacingToolName, newBlockData);
this.Editor.BlockManager.replace({
tool: replacingToolName,
data: newBlockData,
});
this.Editor.BlockSelection.clearSelection();
this.close();
@ -244,7 +255,7 @@ export default class ConversionToolbar extends Module {
const tools = this.Editor.Tools.blockTools;
for (const toolName in tools) {
if (!tools.hasOwnProperty(toolName)) {
if (!Object.prototype.hasOwnProperty.call(tools, toolName)) {
continue;
}
@ -273,6 +284,10 @@ export default class ConversionToolbar extends Module {
/**
* 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 {
const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);
@ -282,7 +297,7 @@ export default class ConversionToolbar extends Module {
icon.innerHTML = toolIcon;
$.append(tool, icon);
$.append(tool, $.text(title || _.capitalize(toolName)));
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, title || _.capitalize(toolName))));
$.append(this.nodes.tools, tool);
this.tools[toolName] = tool;

View file

@ -1,6 +1,8 @@
import Module from '../../__module';
import $ from '../../dom';
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
*
* @typedef {Toolbar} Toolbar
* @property {Object} nodes
* @property {object} nodes - Toolbar nodes
* @property {Element} nodes.wrapper - Toolbar main element
* @property {Element} nodes.content - Zone with Plus button and toolbox.
* @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
*/
public nodes: {[key: string]: HTMLElement} = {
wrapper : null,
content : null,
actions : null,
wrapper: null,
content: null,
actions: null,
// Content Zone
plusButton : null,
plusButton: null,
// Actions Zone
blockActionsButtons: null,
settingsToggler : null,
settingsToggler: null,
};
/**
* CSS styles
* @return {Object}
*
* @returns {object}
*/
public get CSS() {
public get CSS(): {[name: string]: string} {
return {
toolbar: 'ce-toolbar',
content: 'ce-toolbar__content',
@ -104,7 +107,7 @@ export default class Toolbar extends Module {
/**
* Make Content Zone and Actions Zone
*/
['content', 'actions'].forEach( (el) => {
['content', 'actions'].forEach((el) => {
this.nodes[el] = $.make('div', this.CSS[el]);
});
@ -130,7 +133,7 @@ export default class Toolbar extends Module {
*/
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, {
textContent: '⇥ Tab',
}));
@ -149,16 +152,20 @@ export default class Toolbar extends Module {
* - Settings Panel
*/
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);
$.append(this.nodes.settingsToggler, settingsIcon);
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
this.Editor.Tooltip.onHover(this.nodes.settingsToggler, 'Click to tune', {
placement: 'top',
});
this.Editor.Tooltip.onHover(
this.nodes.settingsToggler,
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
{
placement: 'top',
}
);
/**
* Make and append Settings Panel
@ -179,9 +186,10 @@ export default class Toolbar extends Module {
/**
* 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) {
/** Close Toolbox when we move toolbar */
this.Editor.Toolbox.close();
@ -222,6 +230,7 @@ export default class Toolbar extends Module {
/**
* Open Toolbar with Plus Button and Actions
*
* @param {boolean} withBlockActions - by default, Toolbar opens with Block Actions.
* This flag allows to open Toolbar without Actions.
* @param {boolean} needToCloseToolbox - by default, Toolbar will be moved with opening
@ -229,7 +238,7 @@ export default class Toolbar extends Module {
* with closing Toolbox and Block Settings
* 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(() => {
this.move(needToCloseToolbox);
this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);
@ -244,7 +253,8 @@ export default class Toolbar extends Module {
/**
* returns toolbar opened state
* @return {Boolean}
*
* @returns {boolean}
*/
public get opened(): boolean {
return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);
@ -264,12 +274,13 @@ export default class Toolbar extends Module {
/**
* 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 {
hide: () => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
show: () => {
hide: (): void => this.nodes.plusButton.classList.add(this.CSS.plusButtonHidden),
show: (): void => {
if (this.Editor.Toolbox.isEmpty) {
return;
}
@ -280,14 +291,15 @@ export default class Toolbar extends Module {
/**
* 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 {
hide: () => {
hide: (): void => {
this.nodes.actions.classList.remove(this.CSS.actionsOpened);
},
show : () => {
show: (): void => {
this.nodes.actions.classList.add(this.CSS.actionsOpened);
},
};
@ -295,7 +307,6 @@ export default class Toolbar extends Module {
/**
* Handler for Plus Button
* @param {MouseEvent} event
*/
private plusButtonClicked(): void {
this.Editor.Toolbox.toggle();

View file

@ -1,10 +1,11 @@
import Module from '../../__module';
import $ from '../../dom';
import SelectionUtils from '../../selection';
import * as _ from '../../utils';
import {InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings} from '../../../../types';
import { InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings } from '../../../../types';
import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* Inline toolbar with actions that modifies selected text fragment
@ -14,7 +15,6 @@ import Flipper from '../../flipper';
* |________________________|
*/
export default class InlineToolbar extends Module {
/**
* CSS styles
*/
@ -37,19 +37,20 @@ export default class InlineToolbar extends Module {
/**
* State of inline toolbar
*
* @type {boolean}
*/
public opened: boolean = false;
public opened = false;
/**
* Inline Toolbar elements
*/
private nodes: {
wrapper: HTMLElement,
buttons: HTMLElement,
conversionToggler: HTMLElement,
conversionTogglerContent: HTMLElement,
actions: HTMLElement,
wrapper: HTMLElement;
buttons: HTMLElement;
conversionToggler: HTMLElement;
conversionTogglerContent: HTMLElement;
actions: HTMLElement;
} = {
wrapper: null,
buttons: null,
@ -74,15 +75,17 @@ export default class InlineToolbar extends Module {
/**
* Buttons List
*
* @type {NodeList}
*/
private buttonsList: NodeList = null;
/**
* Cache for Inline Toolbar width
*
* @type {number}
*/
private width: number = 0;
private width = 0;
/**
* Instance of class that responses for leafing buttons by arrows/tab
@ -92,15 +95,15 @@ export default class InlineToolbar extends Module {
/**
* 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) {
const allTools = this.inlineTools;
this.toolsInstances = new Map();
for (const tool in allTools) {
if (allTools.hasOwnProperty(tool)) {
if (Object.prototype.hasOwnProperty.call(allTools, tool)) {
this.toolsInstances.set(tool, allTools[tool]);
}
}
@ -112,7 +115,7 @@ export default class InlineToolbar extends Module {
/**
* Making DOM
*/
public make() {
public make(): void {
this.nodes.wrapper = $.make('div', this.CSS.inlineToolbar);
this.nodes.buttons = $.make('div', this.CSS.buttonsWrapper);
this.nodes.actions = $.make('div', this.CSS.actionsWrapper);
@ -169,14 +172,16 @@ export default class InlineToolbar extends Module {
/**
* Shows Inline Toolbar if something is selected
*
* @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.
*/
public tryToShow(needToClose: boolean = false): void {
public tryToShow(needToClose = false): void {
if (!this.allowedToShow()) {
if (needToClose) {
this.close();
}
return;
}
@ -196,11 +201,11 @@ export default class InlineToolbar extends Module {
const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
const newCoords = {
x: selectionRect.x - wrapperOffset.left,
y: selectionRect.y
+ selectionRect.height
y: selectionRect.y +
selectionRect.height -
// + window.scrollY
- wrapperOffset.top
+ this.toolbarVerticalMargin,
wrapperOffset.top +
this.toolbarVerticalMargin,
};
/**
@ -222,12 +227,12 @@ export default class InlineToolbar extends Module {
*/
this.nodes.wrapper.classList.toggle(
this.CSS.inlineToolbarLeftOriented,
realLeftCoord < this.Editor.UI.contentRect.left,
realLeftCoord < this.Editor.UI.contentRect.left
);
this.nodes.wrapper.classList.toggle(
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';
@ -331,7 +336,7 @@ export default class InlineToolbar extends Module {
return false;
}
const target = !$.isElement(currentSelection.anchorNode )
const target = !$.isElement(currentSelection.anchorNode)
? currentSelection.anchorNode.parentElement
: currentSelection.anchorNode;
@ -363,13 +368,14 @@ export default class InlineToolbar extends Module {
*/
private filterTools(): void {
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),
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
*
* @type {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',
hidingDelay: 100,
});
@ -458,7 +464,7 @@ export default class InlineToolbar extends Module {
* Changes Conversion Dropdown content for current block's Tool
*/
private setConversionTogglerContent(): void {
const {BlockManager, Tools} = this.Editor;
const { BlockManager, Tools } = this.Editor;
const toolName = BlockManager.currentBlock.name;
/**
@ -478,11 +484,11 @@ export default class InlineToolbar extends Module {
const userToolboxSettings = toolSettings.toolbox || {};
this.nodes.conversionTogglerContent.innerHTML =
userToolboxSettings.icon
|| toolboxSettings.icon
|| userToolboxSettings.title
|| toolboxSettings.title
|| _.capitalize(toolName);
userToolboxSettings.icon ||
toolboxSettings.icon ||
userToolboxSettings.title ||
toolboxSettings.title ||
_.capitalize(toolName);
}
/**
@ -510,6 +516,9 @@ export default class InlineToolbar extends Module {
/**
* 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 {
const {
@ -522,6 +531,7 @@ export default class InlineToolbar extends Module {
if (!button) {
_.log('Render method must return an instance of Node', 'warn', toolName);
return;
}
@ -530,6 +540,7 @@ export default class InlineToolbar extends Module {
if (typeof tool.renderActions === 'function') {
const actions = tool.renderActions();
this.nodes.actions.appendChild(actions);
}
@ -551,23 +562,26 @@ export default class InlineToolbar extends Module {
*/
const internalTools: string[] = Object
.entries(Tools.internalTools)
.filter(([name, toolClass]: [string, ToolConstructable | ToolSettings]) => {
.filter(([, toolClass]: [string, ToolConstructable | ToolSettings]) => {
if (_.isFunction(toolClass)) {
return toolClass[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'
* 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)) {
shortcut = this.inlineTools[toolName][Tools.INTERNAL_SETTINGS.SHORTCUT];
} else if (toolSettings && toolSettings[Tools.USER_SETTINGS.SHORTCUT]) {
shortcut = toolSettings[Tools.USER_SETTINGS.SHORTCUT];
} else if (tool.shortcut) {
shortcut = tool.shortcut;
}
if (shortcut) {
@ -578,7 +592,10 @@ export default class InlineToolbar extends Module {
* Enable tooltip module on button
*/
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));
@ -592,11 +609,11 @@ export default class InlineToolbar extends Module {
placement: 'top',
hidingDelay: 100,
});
}
/**
* Enable Tool shortcut with Editor Shortcuts Module
*
* @param {InlineTool} tool - Tool instance
* @param {string} shortcut - shortcut according to the ShortcutData Module format
*/
@ -604,7 +621,7 @@ export default class InlineToolbar extends Module {
this.Editor.Shortcuts.add({
name: shortcut,
handler: (event) => {
const {currentBlock} = this.Editor.BlockManager;
const { currentBlock } = this.Editor.BlockManager;
/**
* 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:
* 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);
@ -634,6 +651,7 @@ export default class InlineToolbar extends Module {
/**
* Inline Tool button clicks
*
* @param {InlineTool} tool - Tool's instance
*/
private toolClicked(tool: InlineTool): void {
@ -660,10 +678,10 @@ export default class InlineToolbar extends Module {
const result = {};
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);
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 $ from '../../dom';
import * as _ from '../../utils';
import {BlockToolConstructable} from '../../../../types';
import { BlockToolConstructable } from '../../../../types';
import Flipper from '../../flipper';
import {BlockToolAPI} from '../../block';
import { BlockToolAPI } from '../../block';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* @class Toolbox
* @classdesc Holder for Tools
*
* @typedef {Toolbox} Toolbox
* @property {Boolean} opened - opening state
* @property {Object} nodes - Toolbox nodes
* @property {Object} CSS - CSS class names
* @property {boolean} opened - opening state
* @property {object} nodes - Toolbox nodes
* @property {object} CSS - CSS class names
*
*/
export default class Toolbox extends Module {
/**
* CSS styles
* @return {{toolbox: string, toolboxButton string, toolboxButtonActive: string,
* toolboxOpened: string, tooltip: string, tooltipShown: string, tooltipShortcut: string}}
*
* @returns {object.<string, string>}
*/
get CSS() {
return {
public get CSS(): {[name: string]: string} {
return {
toolbox: 'ce-toolbox',
toolboxButton: 'ce-toolbox__button',
toolboxButtonActive : 'ce-toolbox__button--active',
toolboxButtonActive: 'ce-toolbox__button--active',
toolboxOpened: 'ce-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
* @return {boolean}
*
* @returns {boolean}
*/
public get isEmpty(): boolean {
return this.displayedToolsCount === 0;
@ -45,16 +47,17 @@ export default class Toolbox extends Module {
/**
* Opening state
*
* @type {boolean}
*/
public opened: boolean = false;
public opened = false;
/**
* HTMLElements used for Toolbox UI
*/
public nodes: {
toolbox: HTMLElement,
buttons: HTMLElement[],
toolbox: HTMLElement;
buttons: HTMLElement[];
} = {
toolbox: null,
buttons: [],
@ -62,12 +65,14 @@ export default class Toolbox extends Module {
/**
* How many tools displayed in Toolbox
*
* @type {number}
*/
private displayedToolsCount: number = 0;
private displayedToolsCount = 0;
/**
* Instance of class that responses for leafing buttons by arrows/tab
*
* @type {Flipper|null}
*/
private flipper: Flipper = null;
@ -86,8 +91,8 @@ export default class Toolbox extends Module {
/**
* Toolbox Tool's button click handler
*
* @param {MouseEvent|KeyboardEvent} event
* @param {string} toolName
* @param {MouseEvent|KeyboardEvent} event - event that activates toolbox button
* @param {string} toolName - button to activate
*/
public toolButtonActivate(event: MouseEvent|KeyboardEvent, toolName: string): void {
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;
for (const toolName in tools) {
if (tools.hasOwnProperty(toolName)) {
this.addTool(toolName, tools[toolName] as BlockToolConstructable);
if (Object.prototype.hasOwnProperty.call(tools, toolName)) {
this.addTool(toolName, tools[toolName] as BlockToolConstructable);
}
}
}
@ -166,6 +171,7 @@ export default class Toolbox extends Module {
if (toolToolboxSettings && !toolToolboxSettings.icon) {
_.log('Toolbar icon is missed. Tool %o skipped', 'warn', toolName);
return;
}
@ -222,14 +228,14 @@ export default class Toolbox extends Module {
/**
* Draw tooltip for toolbox tools
*
* @param {String} toolName - toolbox tool name
* @return { HTMLElement }
* @param {string} toolName - toolbox tool name
* @returns {HTMLElement}
*/
private drawTooltip(toolName: string): HTMLElement {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {};
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];
@ -251,11 +257,12 @@ export default class Toolbox extends Module {
/**
* Enable shortcut Block Tool implemented shortcut
*
* @param {BlockToolConstructable} tool - Tool class
* @param {String} toolName - Tool name
* @param {String} shortcut - shortcut according to the ShortcutData Module format
* @param {string} toolName - Tool name
* @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({
name: shortcut,
handler: (event: KeyboardEvent) => {
@ -270,6 +277,7 @@ export default class Toolbox extends Module {
*/
private enableFlipper(): void {
const tools = Array.from(this.nodes.toolbox.childNodes) as HTMLElement[];
this.flipper = new Flipper({
items: tools,
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
*
* @param {BlockToolConstructable} tool - Tool Class
* @param {String} toolName - Tool name
* @param {string} toolName - Tool name
*/
private insertNewBlock(tool: BlockToolConstructable, toolName: string) {
const {BlockManager, Caret} = this.Editor;
/**
* @type {Block}
*/
const {currentBlock} = BlockManager;
private insertNewBlock(tool: BlockToolConstructable, toolName: string): void {
const { BlockManager, Caret } = this.Editor;
const { currentBlock } = BlockManager;
let newBlock;
if (currentBlock.isEmpty) {
newBlock = BlockManager.replace(toolName);
} else {
newBlock = BlockManager.insert(toolName);
}
const newBlock = BlockManager.insert({
tool: toolName,
replace: currentBlock.isEmpty,
});
/**
* Apply callback before inserting html

View file

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

View file

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

View file

@ -1,3 +1,4 @@
/* eslint-disable jsdoc/no-undefined-types */
/**
* Prebuilded sprite of SVG icons
*/
@ -28,35 +29,36 @@ import Flipper from '../flipper';
*
* @typedef {UI} UI
* @property {EditorConfig} config - editor configuration {@link EditorJS#configuration}
* @property {Object} Editor - available editor modules {@link EditorJS#moduleInstances}
* @property {Object} nodes -
* @property {object} Editor - available editor modules {@link EditorJS#moduleInstances}
* @property {object} nodes -
* @property {Element} nodes.holder - element where we need to append redactor
* @property {Element} nodes.wrapper - <codex-editor>
* @property {Element} nodes.redactor - <ce-redactor>
*/
export default class UI extends Module {
/**
* Editor.js UI CSS class names
* @return {{editorWrapper: string, editorZone: string}}
*
* @returns {{editorWrapper: string, editorZone: string}}
*/
public get CSS(): {
editorWrapper: string, editorWrapperNarrow: string, editorZone: string, editorZoneHidden: string,
editorLoader: string, editorEmpty: string,
} {
editorWrapper: string; editorWrapperNarrow: string; editorZone: string; editorZoneHidden: string;
editorLoader: string; editorEmpty: string;
} {
return {
editorWrapper : 'codex-editor',
editorWrapperNarrow : 'codex-editor--narrow',
editorZone : 'codex-editor__redactor',
editorZoneHidden : 'codex-editor__redactor--hidden',
editorLoader : 'codex-editor__loader',
editorEmpty : 'codex-editor--empty',
editorWrapper: 'codex-editor',
editorWrapperNarrow: 'codex-editor--narrow',
editorZone: 'codex-editor__redactor',
editorZoneHidden: 'codex-editor__redactor--hidden',
editorLoader: 'codex-editor__loader',
editorEmpty: 'codex-editor--empty',
};
}
/**
* Return Width of center column of Editor
* @return {DOMRect}
*
* @returns {DOMRect}
*/
public get contentRect(): DOMRect {
if (this.contentRectCache) {
@ -83,9 +85,10 @@ export default class UI extends Module {
/**
* Flag that became true on mobile viewport
*
* @type {boolean}
*/
public isMobile: boolean = false;
public isMobile = false;
/**
* HTML Elements used for UI
@ -99,12 +102,14 @@ export default class UI extends Module {
/**
* Cache for center column rectangle info
* Invalidates on window resize
*
* @type {DOMRect}
*/
private contentRectCache: DOMRect = undefined;
/**
* Handle window resize only when it finished
*
* @type {() => void}
*/
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
*/
public checkEmptiness(): void {
const {BlockManager} = this.Editor;
const { BlockManager } = this.Editor;
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
* Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar
* @return {boolean}
*
* @returns {boolean}
*/
public get someToolbarOpened(): boolean {
const { Toolbox, BlockSettings, InlineToolbar, ConversionToolbar } = this.Editor;
@ -199,9 +205,10 @@ export default class UI extends Module {
public get someFlipperButtonFocused(): boolean {
return Object.entries(this.Editor).filter(([moduleName, moduleClass]) => {
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
*/
private checkIsMobile() {
private checkIsMobile(): void {
this.isMobile = window.innerWidth < 650;
}
/**
* Makes Editor.js interface
* @return {Promise<void>}
*
* @returns {Promise<void>}
*/
private async make(): Promise<void> {
/**
* Element where we need to append Editor.js
*
* @type {Element}
*/
this.nodes.holder = $.getHolder(this.config.holder);
@ -244,7 +253,7 @@ export default class UI extends Module {
/**
* Create and save main UI elements
*/
this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);
this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);
this.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.holder.appendChild(this.nodes.wrapper);
}
/**
@ -271,12 +279,22 @@ export default class UI extends Module {
/**
* Load CSS
*/
// eslint-disable-next-line @typescript-eslint/no-var-requires
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
*/
const tag = $.make('style', null, {
id: styleTagId,
textContent: styles.toString(),
});
@ -294,17 +312,17 @@ export default class UI extends Module {
this.nodes.redactor,
'click',
(event) => this.redactorClicked(event as MouseEvent),
false,
false
);
this.Editor.Listeners.on(this.nodes.redactor,
'mousedown',
(event) => this.documentTouched(event as MouseEvent),
true,
true
);
this.Editor.Listeners.on(this.nodes.redactor,
'touchstart',
(event) => this.documentTouched(event as MouseEvent),
true,
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
* @param {Event} event
*
* @param {KeyboardEvent} event - keyboard event
*/
private documentKeydown(event: KeyboardEvent): void {
switch (event.keyCode) {
case _.keyCodes.ENTER:
this.enterPressed(event);
break;
case _.keyCodes.BACKSPACE:
this.backspacePressed(event);
break;
case _.keyCodes.ESC:
this.escapePressed(event);
break;
default:
this.defaultBehaviour(event);
break;
@ -359,11 +384,12 @@ export default class UI extends Module {
/**
* Ignore all other document's keydown events
* @param {KeyboardEvent} event
*
* @param {KeyboardEvent} event - keyboard event
*/
private defaultBehaviour(event: KeyboardEvent): void {
const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`);
const {currentBlock} = this.Editor.BlockManager;
const { currentBlock } = this.Editor.BlockManager;
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 {
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();
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** 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
* @param event
*
* @param {KeyboardEvent} event - keyboard event
*/
private enterPressed(event: KeyboardEvent): void {
const { BlockManager, BlockSelection, Caret } = this.Editor;
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();
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
@ -431,6 +493,7 @@ export default class UI extends Module {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
return;
}
@ -466,7 +529,8 @@ export default class UI extends Module {
/**
* All clicks on document
* @param {MouseEvent} event - Click
*
* @param {MouseEvent} event - Click event
*/
private documentClicked(event: MouseEvent): void {
/**
@ -519,6 +583,8 @@ export default class UI extends Module {
* Also:
* - Move and show the Toolbar
* - Set a Caret
*
* @param {MouseEvent | TouchEvent} event - touch or mouse event
*/
private documentTouched(event: MouseEvent | TouchEvent): void {
let clickedNode = event.target as HTMLElement;
@ -569,7 +635,7 @@ export default class UI extends Module {
/**
* All clicks on the redactor zone
*
* @param {MouseEvent} event
* @param {MouseEvent} event - click event
*
* @description
* - By clicks on the Editor's bottom zone:
@ -596,6 +662,7 @@ export default class UI extends Module {
const validUrl = _.getValidUrl(href);
_.openTab(validUrl);
return;
}
@ -625,7 +692,8 @@ export default class UI extends Module {
/**
* Handle selection changes on mobile devices
* Uses for showing the Inline Toolbar
* @param {Event} event
*
* @param {Event} event - selection event
*/
private selectionChanged(event: Event): void {
const focusedElement = Selection.anchorElement as Element;
@ -635,7 +703,6 @@ export default class UI extends Module {
* We need to skip such firings
*/
if (!focusedElement || !focusedElement.closest(`.${Block.CSS.content}`)) {
/**
* 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;
* 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) {
Element.prototype.matches = Element.prototype.matchesSelector ||
@ -26,7 +28,7 @@ if (!Element.prototype.matches) {
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
function (s): boolean {
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
let i = matches.length;
@ -43,10 +45,13 @@ if (!Element.prototype.matches) {
* matches the selectors given in parameter.
* 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) {
Element.prototype.closest = function(s) {
Element.prototype.closest = function (s): Element | null {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let el = this;
if (!document.documentElement.contains(el)) {
@ -70,20 +75,22 @@ if (!Element.prototype.closest) {
* or DOMString objects before the first child of the ParentNode.
* 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) {
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();
if (!Array.isArray(nodes)) {
nodes = [ nodes ];
}
nodes.forEach((node: Node|any) => {
nodes.forEach((node: Node | string) => {
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);

View file

@ -30,15 +30,16 @@ interface Document {
/**
* Working with selection
*
* @typedef {SelectionUtils} SelectionUtils
*/
export default class SelectionUtils {
/**
* 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 {
editorWrapper: 'codex-editor',
editorZone: 'codex-editor__redactor',
@ -48,9 +49,10 @@ export default class SelectionUtils {
/**
* Returns selected anchor
* {@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();
return selection ? selection.anchorNode : null;
@ -58,9 +60,10 @@ export default class SelectionUtils {
/**
* 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();
if (!selection) {
@ -83,9 +86,10 @@ export default class SelectionUtils {
/**
* Returns selection offset according to the anchor node
* {@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();
return selection ? selection.anchorOffset : null;
@ -93,9 +97,10 @@ export default class SelectionUtils {
/**
* 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();
return selection ? selection.isCollapsed : null;
@ -103,9 +108,10 @@ export default class SelectionUtils {
/**
* 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();
/**
@ -118,6 +124,7 @@ export default class SelectionUtils {
}
let editorZone = null;
if (selectedNode) {
editorZone = selectedNode.closest(`.${SelectionUtils.CSS.editorZone}`);
}
@ -129,10 +136,20 @@ export default class SelectionUtils {
}
/**
* Return first range
* @return {Range|null}
* Methods return boolean that true if selection exists on the page
*/
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();
return selection && selection.rangeCount ? selection.getRangeAt(0) : null;
@ -140,11 +157,12 @@ export default class SelectionUtils {
/**
* 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,
range: TextRange | Range;
range: TextRange | Range;
let rect = {
x: 0,
@ -166,6 +184,7 @@ export default class SelectionUtils {
if (!window.getSelection) {
_.log('Method window.getSelection is not supported', 'warn');
return rect;
}
@ -173,6 +192,7 @@ export default class SelectionUtils {
if (sel.rangeCount === null || isNaN(sel.rangeCount)) {
_.log('Method SelectionUtils.rangeCount is not supported', 'warn');
return rect;
}
@ -210,26 +230,24 @@ export default class SelectionUtils {
/**
* Returns selected text as String
*
* @returns {string}
*/
static get text(): string {
public static get text(): string {
return window.getSelection ? window.getSelection().toString() : '';
}
/**
* Returns window SelectionUtils
* {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}
* @return {Selection}
* Selection instances
*
* @todo Check if this is still relevant
*/
public static get(): Selection {
return window.getSelection();
}
public instance: Selection = null;
public selection: Selection = null;
/**
* This property can store SelectionUtils's range for restoring later
*
* @type {Range|null}
*/
public savedSelectionRange: Range = null;
@ -237,7 +255,7 @@ export default class SelectionUtils {
/**
* Fake background is active
*
* @return {boolean}
* @returns {boolean}
*/
public isFakeBackgroundEnabled = false;
@ -247,10 +265,53 @@ export default class SelectionUtils {
private readonly commandBackground: string = 'backColor';
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
*/
public removeFakeBackground() {
public removeFakeBackground(): void {
if (!this.isFakeBackgroundEnabled) {
return;
}
@ -262,7 +323,7 @@ export default class SelectionUtils {
/**
* Sets fake background
*/
public setFakeBackground() {
public setFakeBackground(): void {
document.execCommand(this.commandBackground, false, '#a8d6ff');
this.isFakeBackgroundEnabled = true;
@ -312,10 +373,11 @@ export default class SelectionUtils {
/**
* Looks ahead to find passed tag from current selection
*
* @param {String} tagName - tag to found
* @param {String} [className] - tag's class name
* @param {Number} [searchDepth] - count of tags that can be included. For better performance.
* @return {HTMLElement|null}
* @param {string} tagName - tag to found
* @param {string} [className] - tag's class name
* @param {number} [searchDepth] - count of tags that can be included. For better performance.
*
* @returns {HTMLElement|null}
*/
public findParentTag(tagName: string, className?: string, searchDepth = 10): HTMLElement | null {
const selection = window.getSelection();
@ -388,7 +450,7 @@ export default class SelectionUtils {
/**
* 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 {
const selection = window.getSelection();

View file

@ -1,10 +1,20 @@
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 {
/**
* Stub styles
* @type {{wrapper: string; info: string; title: string; subtitle: string}}
*
* @type {{wrapper: string, info: string, title: string, subtitle: string}}
*/
private CSS = {
wrapper: 'ce-stub',
@ -18,6 +28,11 @@ export default class Stub implements BlockTool {
*/
private readonly wrapper: HTMLElement;
/**
* Editor.js API
*/
private readonly api: API;
/**
* Stub title tool name
*/
@ -33,9 +48,15 @@ export default class Stub implements BlockTool {
*/
private readonly savedData: BlockToolData;
constructor({data, config, api}) {
this.title = data.title || 'Error';
this.subtitle = 'The block can not be displayed correctly.';
/**
* @param options - constructor options
* @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.wrapper = this.make();
@ -43,7 +64,8 @@ export default class Stub implements BlockTool {
/**
* Returns stub holder
* @return {HTMLElement}
*
* @returns {HTMLElement}
*/
public render(): HTMLElement {
return this.wrapper;
@ -51,7 +73,8 @@ export default class Stub implements BlockTool {
/**
* Return original Tool data
* @return {BlockToolData}
*
* @returns {BlockToolData}
*/
public save(): BlockToolData {
return this.savedData;
@ -59,7 +82,8 @@ export default class Stub implements BlockTool {
/**
* Create Tool html markup
* @return {HTMLElement}
*
* @returns {HTMLElement}
*/
private make(): HTMLElement {
const wrapper = $.make('div', this.CSS.wrapper);

View file

@ -20,12 +20,15 @@ export enum LogLevels {
declare const VERSION: string;
/**
* @typedef {Object} ChainData
* @property {Object} data - data that will be passed to the success or fallback
* @property {Function} function - function's that must be called asynchronically
* @typedef {object} ChainData
* @property {object} data - data that will be passed to the success or fallback
* @property {Function} function - function's that must be called asynchronously
*
* @interface ChainData
*/
export interface ChainData {
data?: any;
data?: object;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function: (...args: any[]) => any;
}
@ -35,7 +38,8 @@ export interface ChainData {
/**
* Returns basic keycodes as constants
* @return {{}}
*
* @returns {{}}
*/
export const keyCodes = {
BACKSPACE: 8,
@ -73,17 +77,16 @@ export const mouseButtons = {
* @param {string} type - logging type 'log'|'warn'|'error'|'info'
* @param {*} [args] - argument to log with a message
* @param {string} style - additional styling to message
* @param labeled
*/
function _log(
labeled: boolean,
msg: string,
type: string = 'log',
type = 'log',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any,
style: string = 'color: inherit',
style = 'color: inherit'
): void {
if ( !('console' in window) || !window.console[ type ] ) {
if (!('console' in window) || !window.console[type]) {
return;
}
@ -156,7 +159,7 @@ _log.logLevel = LogLevels.VERBOSE;
*
* @param {LogLevels} logLevel - log level to set
*/
export function setLogLevel(logLevel: LogLevels) {
export function setLogLevel(logLevel: LogLevels): void {
_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.
* @param {number} keyCode
* @return {boolean}
*
* @param {number} keyCode - key code
*
* @returns {boolean}
*/
export function isPrintableKey( keyCode: number ): boolean {
return (keyCode > 47 && keyCode < 58) || // number keys
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)
(keyCode > 64 && keyCode < 91) || // letter keys
(keyCode > 95 && keyCode < 112) || // Numpad keys
export function isPrintableKey(keyCode: number): boolean {
return (keyCode > 47 && keyCode < 58) || // number keys
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)
(keyCode > 64 && keyCode < 91) || // letter keys
(keyCode > 95 && keyCode < 112) || // Numpad keys
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
(keyCode > 218 && keyCode < 223); // [\]' (in order)
(keyCode > 218 && keyCode < 223); // [\]' (in order)
}
/**
* Fires a promise sequence asyncronically
* Fires a promise sequence asynchronously
*
* @param {ChainData[]} chains - list or ChainData's
* @param {Function} success - success callback
* @param {Function} fallback - callback that fires in case of errors
*
* @return {Promise}
* @returns {Promise}
*/
export async function sequence(
chains: ChainData[],
success: (data: any) => void = () => {},
fallback: (data: any) => void = () => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
success: (data: object) => void = (): void => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
fallback: (data: object) => void = (): void => {}
): Promise<void> {
/**
* Decorator
*
* @param {ChainData} chainData
* @param {ChainData} chainData - Chain data
*
* @param {Function} successCallback
* @param {Function} fallbackCallback
* @param {Function} successCallback - success callback
* @param {Function} fallbackCallback - fail callback
*
* @return {Promise}
* @returns {Promise}
*/
async function waitNextBlock(
chainData: ChainData,
successCallback: (data: any) => void,
fallbackCallback: (data: any) => void,
successCallback: (data: object) => void,
fallbackCallback: (data: object) => void
): Promise<void> {
try {
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
* a resolved Promise
*/
return await chains.reduce(async (previousValue, currentValue) => {
return chains.reduce(async (previousValue, currentValue) => {
await previousValue;
return waitNextBlock(currentValue, success, fallback);
}, Promise.resolve());
}
@ -237,28 +245,35 @@ export async function sequence(
/**
* 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[] {
return Array.prototype.slice.call(collection);
}
/**
* 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';
}
/**
* 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 {
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
*
* @param {Object} object
* @return {boolean}
* @param {object} object - object to check
*
* @returns {boolean}
*/
export function isEmpty(object: object): boolean {
if (!object) {
@ -279,23 +295,28 @@ export function isEmpty(object: object): boolean {
/**
* Check if passed object is a Promise
*
* @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;
}
/**
* Delays method execution
*
* @param {Function} method
* @param {Number} timeout
* @param {Function} method - method to execute
* @param {number} timeout - timeout in ms
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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,
args = arguments;
// eslint-disable-next-line prefer-rest-params
args = arguments;
window.setTimeout(() => method.apply(context, args), timeout);
};
@ -304,8 +325,9 @@ export function delay(method: (...args: any[]) => any, timeout: number) {
/**
* Get file extension
*
* @param {File} file
* @return string
* @param {File} file - file
*
* @returns {string}
*/
export function getFileExtension(file: File): string {
return file.name.split('.').pop();
@ -314,8 +336,9 @@ export function getFileExtension(file: File): string {
/**
* Check if string is MIME type
*
* @param {string} type
* @return boolean
* @param {string} type - string to check
*
* @returns {boolean}
*/
export function isValidMimeType(type: string): boolean {
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
*
* @param {Function} func - function that we're throttling
* @param {Number} wait - time in milliseconds
* @param {Boolean} immediate - call now
* @return {Function}
* @param {number} wait - time in milliseconds
* @param {boolean} immediate - call now
* @returns {Function}
*/
export function debounce(func: () => void, wait?: number , immediate?: boolean): () => void {
export function debounce(func: () => void, wait?: number, immediate?: boolean): () => void {
let timeout;
return () => {
return (): void => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
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 = () => {
timeout = null;
if (!immediate) {
@ -358,9 +384,10 @@ export function debounce(func: () => void, wait?: number , immediate?: boolean):
/**
* 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', {
innerHTML: text,
});
@ -369,6 +396,7 @@ export function copyTextToClipboard(text) {
const selection = window.getSelection();
const range = document.createRange();
range.selectNode(el);
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
*
* @return {[key: string]: boolean}
*/
export function getUserOS(): {[key: string]: boolean} {
const OS = {
@ -395,6 +421,7 @@ export function getUserOS(): {[key: string]: boolean} {
if (userOS) {
OS[userOS] = true;
return OS;
}
@ -403,23 +430,40 @@ export function getUserOS(): {[key: string]: boolean} {
/**
* 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 {
return text[0].toUpperCase() + text.slice(1);
}
/**
* Merge to objects recursively
* @param {object} target
* @param {object[]} sources
* @return {object}
* Return string representation of the object type
*
* @param {*} object - object to get type
*
* @returns {string}
*/
export function deepMerge(target, ...sources) {
const isObject = (item) => item && typeOf(item) === 'object';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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();
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.
* To detect touch devices more carefully, use 'touchstart' event listener
*
* @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
*
* @return {boolean}
* @returns {boolean}
*/
export const isTouchSupported: boolean = 'ontouchstart' in document.documentElement;
/**
* Return string representation of the object type
*
* @param {any} object
*/
export function typeOf(object: any): string {
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
/**
* Make shortcut command more human-readable
*
* @param {string} shortcut string like 'CMD+B'
*/
export function beautifyShortcut(shortcut: string): string {
@ -493,7 +530,7 @@ export function beautifyShortcut(shortcut: string): string {
* If url has `one slash`, then it concatenates with window location origin
* 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 {
try {
@ -514,7 +551,7 @@ export function getValidUrl(url: string): string {
/**
* 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 {
window.open(url, '_blank');

View file

@ -117,6 +117,8 @@
*/
--toolbar-button: {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
line-height: 34px;
@ -135,11 +137,6 @@
background-color: var(--bg-light);
}
& > svg,
.icon {
margin: auto;
}
&--active {
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 ConversionToolbar from '../components/modules/toolbar/conversion';
import TooltipAPI from '../components/modules/api/tooltip';
import I18nAPI from '../components/modules/api/i18n';
export interface EditorModules {
UI: UI;
@ -74,4 +75,5 @@ export interface EditorModules {
CrossBlockSelection: CrossBlockSelection;
NotifierAPI: NotifierAPI;
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",
"declaration": false,
"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 {BlockToolData, ToolConfig} from "../tools";
import {BlockToolData, ToolConfig} from '../tools';
import {BlockAPI} from './block';
/**
* Describes methods to manipulate with Editor`s blocks
@ -26,8 +27,9 @@ export interface Blocks {
/**
* Removes current Block
* @param {number} index - index of a block to delete
*/
delete(): void;
delete(index?: number): void;
/**
* Swaps two Blocks
@ -49,7 +51,7 @@ export interface Blocks {
* @param {number} index
* @returns {HTMLElement}
*/
getBlockByIndex(index: number): HTMLElement;
getBlockByIndex(index: number): BlockAPI;
/**
* Returns current Block index
@ -61,6 +63,8 @@ export interface Blocks {
* Mark Block as stretched
* @param {number} index - Block to mark
* @param {boolean} status - stretch status
*
* @deprecated Use BlockAPI interface to stretch Blocks
*/
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 './tooltip';
export * from './inline-toolbar';
export * from './block';
export * from './i18n';

View file

@ -1,11 +1,12 @@
import {ToolConstructable, ToolSettings} from '../tools';
import {LogLevels, OutputData, API} from '../index';
import {API, LogLevels, OutputData} from '../index';
import {SanitizerConfig} from './sanitizer-config';
import {I18nConfig} from './i18n-config';
export interface EditorConfig {
/**
* 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;
@ -62,6 +63,11 @@ export interface EditorConfig {
*/
logLevel?: LogLevels;
/**
* Internalization config
*/
i18n?: I18nConfig;
/**
* 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 './conversion-config';
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
*/

View file

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

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 {
Blocks,
Caret,
@ -18,6 +24,7 @@ import {
Styles,
Toolbar,
Tooltip,
I18n,
} from './api';
import {OutputData} from './data-formats/output-data';
@ -29,7 +36,9 @@ export {
BaseToolConstructable,
InlineTool,
InlineToolConstructable,
InlineToolConstructorOptions,
BlockToolConstructable,
BlockToolConstructorOptions,
BlockTool,
BlockToolData,
Tool,
@ -47,8 +56,19 @@ export {
FilePasteEventDetail,
} from './tools';
export {BlockTune, BlockTuneConstructable} from './block-tunes';
export {EditorConfig, SanitizerConfig, PasteConfig, LogLevels, ConversionConfig} from './configs';
export {OutputData} from './data-formats/output-data';
export {
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
@ -67,6 +87,7 @@ export interface API {
toolbar: Toolbar;
inlineToolbar: InlineToolbar;
tooltip: Tooltip;
i18n: I18n;
}
/**

View file

@ -2,4 +2,4 @@
* Object returned by Tool's {@link BlockTool#save} method
* 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 { BlockToolData } from './block-tool-data';
import { BaseTool, BaseToolConstructable } from './tool';
import {BaseTool, BaseToolConstructable} from './tool';
import { ToolConfig } from './tool-config';
import { API } from '../index';
import {API, BlockAPI} from '../index';
import { PasteEvent } from './paste-events';
import { MoveEvent } from './hook-events';
/**
* Describe Block Tool object
* @see {@link docs/tools.md}
@ -73,6 +74,16 @@ export interface BlockTool extends BaseTool {
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 {
/**
* Tool's Toolbox settings
@ -101,7 +112,10 @@ export interface BlockToolConstructable extends BaseToolConstructable {
/**
* @constructor
*
* @param {BlockToolConstructorOptions} config - constructor parameters
*
* @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