[Release] Version 2.15 (#826)

This commit is contained in:
George Berezhnoy 2019-07-01 14:10:30 +03:00 committed by GitHub
parent 23a4b2ef93
commit 13dc0c9bad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1675 additions and 1263 deletions

View file

@ -157,7 +157,7 @@ Copy [editor.js](build/editor.js) file to your project and load it.
## Load Tools
Each Block at the Editor.js represented by [Tools](docs/tools.md). There are simple external scripts with own logic. Probably you want to use several Block Tools that should be connected.
Each Block at the Editor.js is represented by [Tools](docs/tools.md). There are simple external scripts with their own logic. Probably you want to use several Block Tools that should be connected.
For example check out our [Header](https://github.com/editor-js/header) Tool that represents heading blocks.

10
dist/editor.js vendored

File diff suppressed because one or more lines are too long

12
dist/sprite.svg vendored
View file

@ -8,9 +8,7 @@
<path d="M8.024 4.1v8.6a1.125 1.125 0 0 1-2.25 0V4.1L2.18 7.695A1.125 1.125 0 1 1 .59 6.104L6.103.588c.44-.439 1.151-.439 1.59 0l5.516 5.516a1.125 1.125 0 0 1-1.59 1.59L8.023 4.1z"/>
</symbol>
<symbol id="bold" viewBox="0 0 13 15">
<path d="M5.996 13.9H1.752c-.613 0-1.05-.137-1.312-.412-.262-.275-.393-.712-.393-1.312V1.737C.047 1.125.18.684.449.416.718.147 1.152.013 1.752.013h4.5a10.5 10.5 0 0 1 1.723.123c.487.082.922.24 1.308.474a3.43 3.43 0 0 1 1.449 1.738c.132.363.199.747.199 1.151 0 1.39-.695 2.406-2.084 3.05 1.825.581 2.737 1.712 2.737 3.391 0 .777-.199 1.477-.596 2.099a3.581 3.581 0 0 1-1.61 1.378c-.424.177-.91.301-1.46.374-.549.073-1.19.109-1.922.109zm-.209-6.167H2.86v4.055h3.022c1.9 0 2.851-.686 2.851-2.056 0-.7-.246-1.21-.739-1.525-.492-.316-1.228-.474-2.207-.474zM2.86 2.125v3.59h2.577c.7 0 1.242-.066 1.624-.198a1.55 1.55 0 0 0 .876-.758c.158-.265.237-.562.237-.89 0-.702-.25-1.167-.748-1.398-.499-.23-1.26-.346-2.283-.346H2.86z"/>
<symbol id="bold"><path d="M5.997 14H1.72c-.618 0-1.058-.138-1.323-.415C.132 13.308 0 12.867 0 12.262V1.738C0 1.121.135.676.406.406.676.136 1.114 0 1.719 0h4.536c.669 0 1.248.041 1.738.124.49.083.93.242 1.318.478a3.458 3.458 0 0 1 1.461 1.752c.134.366.2.753.2 1.16 0 1.401-.7 2.426-2.1 3.075 1.84.586 2.76 1.726 2.76 3.42 0 .782-.2 1.487-.602 2.114a3.61 3.61 0 0 1-1.623 1.39 5.772 5.772 0 0 1-1.471.377c-.554.073-1.2.11-1.939.11zm-.21-6.217h-2.95v4.087h3.046c1.916 0 2.874-.69 2.874-2.072 0-.707-.248-1.22-.745-1.537-.496-.319-1.238-.478-2.225-.478zM2.837 2.13v3.619h2.597c.707 0 1.252-.067 1.638-.2.385-.134.68-.389.883-.765.16-.267.239-.566.239-.897 0-.707-.252-1.176-.755-1.409-.503-.232-1.27-.348-2.301-.348H2.836z"/>
</symbol>
<symbol id="cross" viewBox="0 0 237 237">
<path transform="rotate(45 280.675 51.325)" d="M191 191V73c0-5.523 4.477-10 10-10h25c5.523 0 10 4.477 10 10v118h118c5.523 0 10 4.477 10 10v25c0 5.523-4.477 10-10 10H236v118c0 5.523-4.477 10-10 10h-25c-5.523 0-10-4.477-10-10V236H73c-5.523 0-10-4.477-10-10v-25c0-5.523 4.477-10 10-10h118z"/>
@ -24,13 +22,11 @@
</g>
</symbol>
<symbol id="italic" viewBox="0 0 6 15">
<path d="M4 5.2l-1.368 7.474c-.095.518-.29.91-.585 1.175a1.468 1.468 0 0 1-1.01.398c-.379 0-.662-.136-.85-.407-.186-.272-.234-.66-.141-1.166L1.4 5.276c.093-.511.282-.896.567-1.155a1.43 1.43 0 0 1 .994-.389c.38 0 .668.13.867.389.199.259.256.618.172 1.08zm-.79-2.67c-.36 0-.648-.111-.863-.332-.215-.221-.286-.534-.212-.938.067-.366.253-.668.559-.905A1.57 1.57 0 0 1 3.673 0c.334 0 .612.107.831.322.22.215.292.527.217.938-.073.398-.256.709-.55.933a1.55 1.55 0 0 1-.961.336z"/>
<symbol id="italic">
<path d="M19.211 15.326l-1.44 7.108c-.1.493-.305.865-.615 1.117a1.64 1.64 0 0 1-1.064.379c-.4 0-.697-.13-.894-.388-.197-.258-.247-.627-.15-1.108l1.426-7.036c.098-.486.297-.853.597-1.1.299-.245.648-.368 1.047-.368.399 0 .703.123.912.369.21.246.27.588.181 1.027zm-.831-2.663c-.38 0-.682-.116-.909-.35-.227-.232-.301-.561-.223-.987.07-.385.266-.703.588-.952.322-.25.665-.374 1.03-.374.353 0 .645.113.876.34.232.225.308.554.229.986-.077.42-.27.747-.58.983-.308.236-.646.354-1.011.354z"/>
</symbol>
<symbol id="link" viewBox="0 0 15 14">
<path transform="rotate(-45 11.83 6.678)" d="M11.332 4.013a51.07 51.07 0 0 1-2.28.001A1.402 1.402 0 0 0 7.7 2.25H3.65a1.4 1.4 0 1 0 0 2.8h.848c.206.86.693 1.61 1.463 2.25H3.65a3.65 3.65 0 1 1 0-7.3H7.7a3.65 3.65 0 0 1 3.632 4.013zM10.9 0h2a3.65 3.65 0 0 1 0 7.3H8.85a3.65 3.65 0 0 1-3.632-4.011A62.68 62.68 0 0 1 7.5 3.273 1.401 1.401 0 0 0 8.85 5.05h4.05a1.4 1.4 0 0 0 0-2.8h-.48C12.274 1.664 11.694.785 10.9 0z"/>
<symbol id="link"><path d="M15.439 21.153a4.202 4.202 0 0 0 2.72.63l-.985.986a4.202 4.202 0 1 1-5.943-5.945l2.093-2.093a4.202 4.202 0 0 1 5.934-.009l-1.655 1.656a5.886 5.886 0 0 1-.02.019 1.835 1.835 0 0 0-2.585.009l-2.093 2.093a1.836 1.836 0 0 0 2.534 2.654zm3.122-8.306a4.202 4.202 0 0 0-2.72-.63l.985-.986a4.202 4.202 0 1 1 5.943 5.945l-2.093 2.093a4.202 4.202 0 0 1-5.934.009l1.655-1.656.02-.019a1.835 1.835 0 0 0 2.585-.009l2.093-2.093a1.836 1.836 0 0 0-2.534-2.654z"/>
</symbol>
<symbol id="plus" viewBox="0 0 14 14">
<path d="M8.05 5.8h4.625a1.125 1.125 0 0 1 0 2.25H8.05v4.625a1.125 1.125 0 0 1-2.25 0V8.05H1.125a1.125 1.125 0 0 1 0-2.25H5.8V1.125a1.125 1.125 0 0 1 2.25 0V5.8z"/>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -1,5 +1,19 @@
# Changelog
### 2.15
- `New` — New [`blocks.insert()`](api.md) API method [#715](https://github.com/codex-team/editor.js/issues/715).
- `New` *Conversion Toolbar* — Ability to convert one block to another [#704](https://github.com/codex-team/editor.js/issues/704)
- `New` *Cross-block selection* — Ability to select multiple blocks by mouse and with SHIFT+ARROWS [#703](https://github.com/codex-team/editor.js/issues/703)
- `Deprecated` — [`blocks.insertNewBlock()`](api.md) method is deprecated. Use `blocks.insert()` instead.
- `Improvements` — Inline Toolbar now works on mobile devices [#706](https://github.com/codex-team/editor.js/issues/706)
- `Improvements` — Toolbar looks better on mobile devices [#706](https://github.com/codex-team/editor.js/issues/706)
- `Improvements` — Now `pasteConfig` can return `false` to disable paste handling on your Tool [#801](https://github.com/codex-team/editor.js/issues/801)
- `Fix` — EditorConfig's `onChange` callback now fires when native inputs\` content has been changed [#794](https://github.com/codex-team/editor.js/issues/794)
- `Fix` — Resolve bug with deleting leading new lines [#726](https://github.com/codex-team/editor.js/issues/726)
- `Fix` — Fix inline link Tool to support different link types like `mailto` and `tel` [#809](https://github.com/codex-team/editor.js/issues/809)
- `Fix` — Added `typeof` util method to check exact object type [#805](https://github.com/codex-team/editor.js/issues/805)
- `Fix` — Remove internal `enableLineBreaks` option from external Tool settings type description [#825](https://github.com/codex-team/editor.js/pull/825)
### 2.14

View file

@ -38,7 +38,9 @@ Methods that working with Blocks
`stretchBlock(index: number, status: boolean)` - make Block stretched
`insertNewBlock()` - 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
#### SanitizerAPI

View file

@ -58,6 +58,8 @@ Options that Tool can specify. All settings should be passed as static propertie
| `toolbox` | _Object_ | `undefined` | Pass here `icon` and `title` to display this `Tool` in the Editor's `Toolbox` <br /> `icon` - HTML string with icon for Toolbox <br /> `title` - optional title to display in Toolbox |
| `enableLineBreaks` | _Boolean_ | `false` | With this option, Editor.js won't handle Enter keydowns. Can be helpful for Tools like `<code>` where line breaks should be handled by default behaviour. |
| `isInline` | _Boolean_ | `false` | Describes Tool as a [Tool for the Inline Toolbar](tools-inline.md) |
| `sanitize` | _Object_ | `undefined` | Config for automatic sanitizing of saved data. See [Sanitize](#sanitize) section. |
| `conversionConfig` | _Object_ | `undefined` | Config allows Tool to specify how it can be converted into/from another Tool. See [Conversion config](#conversion-config) section. |
## User configuration
@ -224,7 +226,18 @@ onPaste (event) {
}
```
## Sanitize
### Disable paste handling
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>
Editor.js provides [API](sanitizer.md) to clean taint strings.
Use it manually at the `save()` method or or pass `sanitizer` config to do it automatically.
@ -341,4 +354,111 @@ static get sanitize() {
}
```
## Conversion config <a name="conversion-config"></a>
Editor.js has a Conversion Toolbar that allows user to convert one Block to another.
![](https://capella.pics/6c1f708b-a30c-4ffd-a427-5b59a1a472e0.jpg)
1. You can add ability to your Tool to be converted. Specify «export» property of `conversionConfig`.
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.
Example:
```js
class Header {
constructor(){
this.data = {
text: '',
level: 2
}
}
/**
* Rules specified how our Tool can be converted to/from other Tool.
*/
static get conversionConfig() {
return {
export: 'text', // this property of tool data will be used as string to pass to other tool
import: 'text' // to this property imported string will be passed
};
}
}
```
### Your Tool -> 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.
`Function` is a method that accepts your Tool data and compose a string to export from it. See example below:
```js
class ListTool {
constructor(){
this.data = {
items: [
'Fisrt item',
'Second item',
'Third item'
],
type: 'ordered'
}
}
static get conversionConfig() {
return {
export: (data) => {
return data.items.join('.'); // in this example, all list items will be concatenated to an export string
},
// ... import rule
};
}
}
```
### Other Tool -> your Tool
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`.
`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:
```js
class ListTool {
constructor(data){
this.data = data || {
items: [],
type: 'unordered'
}
}
static get conversionConfig() {
return {
// ... export rule
/**
* In this example, List Tool creates items by splitting original text by a dot symbol.
*/
import: (string) => {
const items = string.split('.');
return {
items: items.filter( (text) => text.trim() !== ''),
type: 'unordered'
};
}
};
}
}
```

@ -1 +1 @@
Subproject commit 37ad8316c138f55b5c210a0c8bb6b83db483187b
Subproject commit 4be61b52257911ce4bfbbddd55199e7eb952e839

@ -1 +1 @@
Subproject commit 019fe76e6065d9d59be6c47b8f4a4706f285eee1
Subproject commit f537cf6ecb26fece34c56a85b51e79b07451e69e

@ -1 +1 @@
Subproject commit 414c02a28bed932cb396329787c0adadf8a9111f
Subproject commit c306bcb33c88eaa3c172eaf387fbcd06ae6b297f

@ -1 +1 @@
Subproject commit c1db3e8b40e98c36fedb1af0b574e5a161e2a471
Subproject commit 58bf8bd571ae259e3d150ac0c12d1676e5706470

View file

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

View file

@ -1,3 +1 @@
<svg width="13" height="15" viewBox="0 0 13 15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M5.996 13.9H1.752c-.613 0-1.05-.137-1.312-.412-.262-.275-.393-.712-.393-1.312V1.737C.047 1.125.18.684.449.416.718.147 1.152.013 1.752.013h4.5a10.5 10.5 0 0 1 1.723.123c.487.082.922.24 1.308.474a3.43 3.43 0 0 1 1.449 1.738c.132.363.199.747.199 1.151 0 1.39-.695 2.406-2.084 3.05 1.825.581 2.737 1.712 2.737 3.391 0 .777-.199 1.477-.596 2.099a3.581 3.581 0 0 1-1.61 1.378c-.424.177-.91.301-1.46.374-.549.073-1.19.109-1.922.109zm-.209-6.167H2.86v4.055h3.022c1.9 0 2.851-.686 2.851-2.056 0-.7-.246-1.21-.739-1.525-.492-.316-1.228-.474-2.207-.474zM2.86 2.125v3.59h2.577c.7 0 1.242-.066 1.624-.198a1.55 1.55 0 0 0 .876-.758c.158-.265.237-.562.237-.89 0-.702-.25-1.167-.748-1.398-.499-.23-1.26-.346-2.283-.346H2.86z"/>
</svg>
<svg width="12" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M5.997 14H1.72c-.618 0-1.058-.138-1.323-.415C.132 13.308 0 12.867 0 12.262V1.738C0 1.121.135.676.406.406.676.136 1.114 0 1.719 0h4.536c.669 0 1.248.041 1.738.124.49.083.93.242 1.318.478a3.458 3.458 0 0 1 1.461 1.752c.134.366.2.753.2 1.16 0 1.401-.7 2.426-2.1 3.075 1.84.586 2.76 1.726 2.76 3.42 0 .782-.2 1.487-.602 2.114a3.61 3.61 0 0 1-1.623 1.39 5.772 5.772 0 0 1-1.471.377c-.554.073-1.2.11-1.939.11zm-.21-6.217h-2.95v4.087h3.046c1.916 0 2.874-.69 2.874-2.072 0-.707-.248-1.22-.745-1.537-.496-.319-1.238-.478-2.225-.478zM2.837 2.13v3.619h2.597c.707 0 1.252-.067 1.638-.2.385-.134.68-.389.883-.765.16-.267.239-.566.239-.897 0-.707-.252-1.176-.755-1.409-.503-.232-1.27-.348-2.301-.348H2.836z"/></svg>

Before

Width:  |  Height:  |  Size: 857 B

After

Width:  |  Height:  |  Size: 774 B

View file

@ -1,3 +1,3 @@
<svg width="6" height="15" viewBox="0 0 6 15" xmlns="http://www.w3.org/2000/svg">
<path d="M4 5.2l-1.368 7.474c-.095.518-.29.91-.585 1.175a1.468 1.468 0 0 1-1.01.398c-.379 0-.662-.136-.85-.407-.186-.272-.234-.66-.141-1.166L1.4 5.276c.093-.511.282-.896.567-1.155a1.43 1.43 0 0 1 .994-.389c.38 0 .668.13.867.389.199.259.256.618.172 1.08zm-.79-2.67c-.36 0-.648-.111-.863-.332-.215-.221-.286-.534-.212-.938.067-.366.253-.668.559-.905A1.57 1.57 0 0 1 3.673 0c.334 0 .612.107.831.322.22.215.292.527.217.938-.073.398-.256.709-.55.933a1.55 1.55 0 0 1-.961.336z" />
<svg width="34" height="34" xmlns="http://www.w3.org/2000/svg">
<path d="M19.211 15.326l-1.44 7.108c-.1.493-.305.865-.615 1.117a1.64 1.64 0 0 1-1.064.379c-.4 0-.697-.13-.894-.388-.197-.258-.247-.627-.15-1.108l1.426-7.036c.098-.486.297-.853.597-1.1.299-.245.648-.368 1.047-.368.399 0 .703.123.912.369.21.246.27.588.181 1.027zm-.831-2.663c-.38 0-.682-.116-.909-.35-.227-.232-.301-.561-.223-.987.07-.385.266-.703.588-.952.322-.25.665-.374 1.03-.374.353 0 .645.113.876.34.232.225.308.554.229.986-.077.42-.27.747-.58.983-.308.236-.646.354-1.011.354z"/>
</svg>

Before

Width:  |  Height:  |  Size: 566 B

After

Width:  |  Height:  |  Size: 557 B

View file

@ -1,3 +1 @@
<svg width="15" height="14" viewBox="0 0 15 14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path transform="rotate(-45 11.83 6.678)" d="M11.332 4.013a51.07 51.07 0 0 1-2.28.001A1.402 1.402 0 0 0 7.7 2.25H3.65a1.4 1.4 0 1 0 0 2.8h.848c.206.86.693 1.61 1.463 2.25H3.65a3.65 3.65 0 1 1 0-7.3H7.7a3.65 3.65 0 0 1 3.632 4.013zM10.9 0h2a3.65 3.65 0 0 1 0 7.3H8.85a3.65 3.65 0 0 1-3.632-4.011A62.68 62.68 0 0 1 7.5 3.273 1.401 1.401 0 0 0 8.85 5.05h4.05a1.4 1.4 0 0 0 0-2.8h-.48C12.274 1.664 11.694.785 10.9 0z"/>
</svg>
<svg width="34" height="34" xmlns="http://www.w3.org/2000/svg"><path d="M15.439 21.153a4.202 4.202 0 0 0 2.72.63l-.985.986a4.202 4.202 0 1 1-5.943-5.945l2.093-2.093a4.202 4.202 0 0 1 5.934-.009l-1.655 1.656a5.886 5.886 0 0 1-.02.019 1.835 1.835 0 0 0-2.585.009l-2.093 2.093a1.836 1.836 0 0 0 2.534 2.654zm3.122-8.306a4.202 4.202 0 0 0-2.72-.63l.985-.986a4.202 4.202 0 1 1 5.943 5.945l-2.093 2.093a4.202 4.202 0 0 1-5.934.009l1.655-1.656.02-.019a1.835 1.835 0 0 0 2.585-.009l2.093-2.093a1.836 1.836 0 0 0-2.534-2.654z"/></svg>

Before

Width:  |  Height:  |  Size: 554 B

After

Width:  |  Height:  |  Size: 526 B

View file

@ -162,13 +162,13 @@ export default class Block {
/**
* Returns Plugins content
* @return {Node}
* @return {HTMLElement}
*/
get pluginsContent(): Node {
get pluginsContent(): HTMLElement {
const pluginsContent = this.holder.querySelector(`.${Block.CSS.content}`);
if (pluginsContent && pluginsContent.childNodes.length) {
return pluginsContent.childNodes[0];
return pluginsContent.childNodes[0] as HTMLElement;
}
return null;

View file

@ -56,7 +56,7 @@ export default class BoldInlineTool implements InlineTool {
this.nodes.button = document.createElement('button') as HTMLButtonElement;
this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('bold', 13, 15));
this.nodes.button.appendChild($.svg('bold', 12, 14));
return this.nodes.button;
}

View file

@ -56,7 +56,7 @@ export default class ItalicInlineTool implements InlineTool {
this.nodes.button = document.createElement('button') as HTMLButtonElement;
this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('italic', 6, 15));
this.nodes.button.appendChild($.svg('italic', 34, 34));
return this.nodes.button;
}

View file

@ -112,7 +112,7 @@ export default class LinkInlineTool implements InlineTool {
this.nodes.button = document.createElement('button') as HTMLButtonElement;
this.nodes.button.type = 'button';
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
this.nodes.button.appendChild($.svg('link', 15, 14));
this.nodes.button.appendChild($.svg('link', 34, 34));
this.nodes.button.appendChild($.svg('unlink', 16, 18));
return this.nodes.button;
}
@ -330,7 +330,7 @@ export default class LinkInlineTool implements InlineTool {
/**
* If protocol already exists, do nothing
*/
if (/^(\w+):\/\//.test(link)) {
if (/^(\w+):(\/\/)?/.test(link)) {
return link;
}

View file

@ -1,9 +1,8 @@
import Module from '../../__module';
import {Blocks} from '../../../../types/api';
import {OutputData} from '../../../../types';
import Block from '../../block';
import {ModuleConfig} from '../../../types-internal/module-config';
import {BlockToolData, OutputData, ToolConfig} from '../../../../types';
import _ from './../../utils';
/**
* @class BlocksAPI
@ -26,6 +25,7 @@ export default class BlocksAPI extends Module {
getBlocksCount: () => this.getBlocksCount(),
stretchBlock: (index: number, status: boolean = true) => this.stretchBlock(index, status),
insertNewBlock: () => this.insertNewBlock(),
insert: this.insert,
};
}
@ -142,10 +142,40 @@ export default class BlocksAPI extends Module {
/**
* Insert new Block
* After set caret to this Block
*
* @param {string} type Tool name
* @param {BlockToolData} data Tool data to insert
* @param {ToolConfig} config Tool config
* @param {number?} index index where to insert new Block
* @param {boolean?} needToFocus - flag to focus inserted Block
*/
public insertNewBlock() {
const newBlock = this.Editor.BlockManager.insert();
this.Editor.Caret.setToBlock(newBlock);
public insert = (
type: string = this.config.initialBlock,
data: BlockToolData = {},
config: ToolConfig = {},
index?: number,
needToFocus?: boolean,
): void => {
this.Editor.BlockManager.insert(
type,
data,
config,
index,
needToFocus,
);
}
/**
* Insert new Block
* After set caret to this Block
*
* @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. ' +
'Use blocks.insert() instead.', 'warn');
this.insert();
}
}

View file

@ -21,7 +21,7 @@ export default class InlineToolbarAPI extends Module {
* Open Inline Toolbar
*/
public open(): void {
this.Editor.InlineToolbar.open();
this.Editor.InlineToolbar.tryToShow();
}
/**

View file

@ -56,7 +56,7 @@ export default class BlockEvents extends Module {
* Fires on keydown before event processing
* @param {KeyboardEvent} event - keydown
*/
public beforeKeydownProcessing(event): void {
public beforeKeydownProcessing(event: KeyboardEvent): void {
/**
* Do not close Toolbox on Tabs or on Enter with opened Toolbox
*/
@ -65,45 +65,61 @@ export default class BlockEvents extends Module {
}
/**
* Close Toolbar on any keypress except TAB, because TAB leafs Tools
* When user type something:
* - close Toolbar
* - close Conversion Toolbar
* - clear block highlighting
*/
if (event.keyCode !== _.keyCodes.TAB) {
if (_.isPrintableKey(event.keyCode)) {
this.Editor.Toolbar.close();
}
this.Editor.ConversionToolbar.close();
const cmdKey = event.ctrlKey || event.metaKey;
const altKey = event.altKey;
const shiftKey = event.shiftKey;
/** clear selecton when it is not CMD, SHIFT, ALT keys */
if (cmdKey || altKey || shiftKey) {
return;
}
/**
* Clear all highlightings
*/
this.Editor.BlockManager.clearFocused();
if (event.keyCode !== _.keyCodes.ENTER && event.keyCode !== _.keyCodes.BACKSPACE) {
/**
* Clear selection and restore caret before navigation
* Allow to use shortcuts with selected blocks
* @type {boolean}
*/
this.Editor.BlockSelection.clearSelection(true);
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
if (!isShortcut) {
this.Editor.BlockManager.clearFocused();
this.Editor.BlockSelection.clearSelection(event);
}
}
}
/**
* Key up on Block:
* - shows Inline Toolbar if something selected
* - shows conversion toolbar with 85% of block selection
*/
public keyup(event): void {
this.Editor.InlineToolbar.handleShowingEvent(event);
/**
* If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows)
*/
if (event.shiftKey) {
return;
}
const { InlineToolbar, ConversionToolbar, UI, BlockManager } = this.Editor;
const block = BlockManager.getBlock(event.target);
/**
* Conversion Toolbar will be opened when user selects 85% of plugins content
* that why we must with the length of pluginsContent
*/
if (SelectionUtils.almostAllSelected(block.pluginsContent.textContent)) {
InlineToolbar.close();
ConversionToolbar.tryToShow(block);
} else {
ConversionToolbar.close();
InlineToolbar.tryToShow(true);
}
/**
* Check if editor is empty on each keyup and add special css class to wrapper
*/
this.Editor.UI.checkEmptiness();
UI.checkEmptiness();
}
/**
@ -111,7 +127,45 @@ export default class BlockEvents extends Module {
* - shows Inline Toolbar if something selected
*/
public mouseUp(event): void {
this.Editor.InlineToolbar.handleShowingEvent(event);
const { InlineToolbar, ConversionToolbar, BlockManager, BlockSelection } = this.Editor;
const block = BlockManager.getBlock(event.target);
/**
* Timeout uses to wait if selection will cleared after mouse up (regular click on block)
*/
_.delay(() => {
/**
* 1) selected 85% of block - open Conversion Toolbar
* 2) select something inside block - open Inline Toolbar
* 3) nothing selected - close Toolbars
*/
if (SelectionUtils.almostAllSelected(block.pluginsContent.textContent)) {
InlineToolbar.close();
ConversionToolbar.tryToShow(block);
} else if (!SelectionUtils.isCollapsed) {
InlineToolbar.tryToShow();
ConversionToolbar.close();
} else {
InlineToolbar.close();
/**
* Don't close Conversion toolbar when Rectangle Selection ended with one block selected
* @see RectangleSelection#endSelection
*/
if (BlockSelection.selectedBlocks.length !== 1) {
ConversionToolbar.close();
}
}
}, 30)();
}
/**
* Set up mouse selection handlers
*
* @param {MouseEvent} event
*/
public mouseDown(event: MouseEvent): void {
this.Editor.CrossBlockSelection.watchSelection(event);
}
/**
@ -119,8 +173,13 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event
*/
public tabPressed(event): void {
/**
* Clear blocks selection by tab
*/
this.Editor.BlockSelection.clearSelection(event);
const {currentBlock} = this.Editor.BlockManager;
const { BlockManager, Tools, ConversionToolbar, InlineToolbar } = this.Editor;
const currentBlock = BlockManager.currentBlock;
if (!currentBlock) {
return;
@ -134,58 +193,21 @@ export default class BlockEvents extends Module {
const shiftKey = event.shiftKey,
direction = shiftKey ? 'left' : 'right';
const canLeafToolbox = Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty;
const canLeafInlineToolbar = !currentBlock.isEmpty && !SelectionUtils.isCollapsed && InlineToolbar.opened;
const canLeafConversionToolbar = !currentBlock.isEmpty && ConversionToolbar.opened;
/**
* For empty Blocks we show Plus button via Toobox only for initial Blocks
*/
if (this.Editor.Tools.isInitial(currentBlock.tool) && currentBlock.isEmpty) {
/**
* Work with Toolbox
* ------------------
*
* If Toolbox is not open, then just open it and show plus button
* Next Tab press will leaf Toolbox Tools
*/
if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(false , false);
this.Editor.Toolbar.plusButton.show();
} else {
this.Editor.Toolbox.leaf(direction);
}
this.Editor.Toolbox.open();
} else if (!currentBlock.isEmpty && !SelectionUtils.isCollapsed) {
/**
* Work with Inline Tools
* -----------------------
*
* If InlineToolbar is not open, just open it and focus first button
* Next Tab press will leaf InlineToolbar Tools
*/
if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.leaf(direction);
}
if (canLeafToolbox) {
this.leafToolboxTools(direction);
} else if (canLeafInlineToolbar) {
this.leafInlineToolbarTools(direction);
} else if (canLeafConversionToolbar) {
this.leafConversionToolbarTools(direction);
} else {
/**
* Open Toolbar and show BlockSettings
*/
if (!this.Editor.Toolbar.opened) {
this.Editor.BlockManager.currentBlock.focused = true;
this.Editor.Toolbar.open(true, false);
this.Editor.Toolbar.plusButton.hide();
}
/**
* Work with Block Tunes
* ----------------------
*
* If BlockSettings is not open, then open BlockSettings
* Next Tab press will leaf Settings Buttons
*/
if (!this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.open();
}
this.Editor.BlockSettings.leaf(direction);
this.leafBlockSettingsTools(direction);
}
}
@ -196,6 +218,11 @@ export default class BlockEvents extends Module {
* @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) {
@ -274,10 +301,10 @@ export default class BlockEvents extends Module {
BlockSelection.copySelectedBlocks();
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertAtIndex(selectionPositionIndex, true), Caret.positions.START);
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection();
BlockSelection.clearSelection(event);
}
/**
@ -285,7 +312,7 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keydown
*/
private enter(event: KeyboardEvent): void {
const {BlockManager, Tools} = this.Editor;
const { BlockManager, Toolbox, BlockSettings, InlineToolbar, ConversionToolbar, Tools } = this.Editor;
const currentBlock = BlockManager.currentBlock;
const tool = Tools.available[currentBlock.name];
@ -294,27 +321,28 @@ export default class BlockEvents extends Module {
* Uses for Tools like <code> where line breaks should be handled by default behaviour.
*/
if (tool
&& tool[this.Editor.Tools.apiSettings.IS_ENABLED_LINE_BREAKS]
&& !this.Editor.BlockSettings.opened
&& !this.Editor.InlineToolbar.opened) {
&& tool[Tools.apiSettings.IS_ENABLED_LINE_BREAKS]
&& !BlockSettings.opened
&& !InlineToolbar.opened
&& !ConversionToolbar.opened) {
return;
}
if (this.Editor.Toolbox.opened && this.Editor.Toolbox.getActiveTool) {
if (Toolbox.opened && Toolbox.getActiveTool) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
this.Editor.Toolbox.toolButtonActivate(event, this.Editor.Toolbox.getActiveTool);
Toolbox.toolButtonActivate(event, Toolbox.getActiveTool);
return;
}
if (this.Editor.InlineToolbar.opened && this.Editor.InlineToolbar.focusedButton) {
if (InlineToolbar.opened && InlineToolbar.focusedButton) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
this.Editor.InlineToolbar.focusedButton.click();
InlineToolbar.focusedButton.click();
return;
}
@ -331,7 +359,7 @@ export default class BlockEvents extends Module {
* If enter has been pressed at the start of the text, just insert paragraph Block above
*/
if (this.Editor.Caret.isAtStart && !this.Editor.BlockManager.currentBlock.hasMedia) {
this.Editor.BlockManager.insertAtIndex(this.Editor.BlockManager.currentBlockIndex);
this.Editor.BlockManager.insertInitialBlockAtIndex(this.Editor.BlockManager.currentBlockIndex);
} else {
/**
* Split the Current Block into two blocks
@ -396,7 +424,7 @@ export default class BlockEvents extends Module {
this.Editor.Toolbar.close();
/** Clear selection */
BlockSelection.clearSelection();
BlockSelection.clearSelection(event);
return;
}
@ -472,6 +500,13 @@ export default class BlockEvents extends Module {
* Handle right and down keyboard keys
*/
private arrowRightAndDown(event: KeyboardEvent): void {
const shouldEnableCBS = this.Editor.Caret.isAtEnd || this.Editor.BlockSelection.anyBlockSelected;
if (event.shiftKey && event.keyCode === _.keyCodes.DOWN && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState();
return;
}
if (this.Editor.Caret.navigateNext()) {
/**
* Default behaviour moves cursor by 1 character, we need to prevent it
@ -482,15 +517,30 @@ export default class BlockEvents extends Module {
* After caret is set, update Block input index
*/
_.delay(() => {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
/** Check currentBlock for case when user moves selection out of Editor */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
}, 20)();
}
/**
* Clear blocks selection by arrows
*/
this.Editor.BlockSelection.clearSelection(event);
}
/**
* Handle left and up keyboard keys
*/
private arrowLeftAndUp(event: KeyboardEvent): void {
const shouldEnableCBS = this.Editor.Caret.isAtStart || this.Editor.BlockSelection.anyBlockSelected;
if (event.shiftKey && event.keyCode === _.keyCodes.UP && shouldEnableCBS) {
this.Editor.CrossBlockSelection.toggleBlockSelectedState(false);
return;
}
if (this.Editor.Caret.navigatePrevious()) {
/**
* Default behaviour moves cursor by 1 character, we need to prevent it
@ -501,9 +551,17 @@ export default class BlockEvents extends Module {
* After caret is set, update Block input index
*/
_.delay(() => {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
/** Check currentBlock for case when user ends selection out of Editor and then press arrow-key */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
}, 20)();
}
/**
* Clear blocks selection by arrows
*/
this.Editor.BlockSelection.clearSelection(event);
}
/**
@ -517,6 +575,8 @@ export default class BlockEvents extends Module {
private needToolbarClosing(event) {
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;
/**
@ -525,6 +585,71 @@ 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);
return !(event.shiftKey
|| flippingToolbarItems
|| toolboxItemSelected
|| blockSettingsItemSelected
|| inlineToolbarItemSelected
|| conversionToolbarItemSelected
);
}
/**
* If Toolbox is not open, then just open it and show plus button
* Next Tab press will leaf Toolbox Tools
*
* @param {string} direction
*/
private leafToolboxTools(direction: string): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.Toolbar.open(false , false);
this.Editor.Toolbar.plusButton.show();
} else {
this.Editor.Toolbox.leaf(direction);
}
this.Editor.Toolbox.open();
}
/**
* If InlineToolbar is not open, just open it and focus first button
* Next Tab press will leaf InlineToolbar Tools
*
* @param {string} direction
*/
private leafInlineToolbarTools(direction: string): void {
if (this.Editor.InlineToolbar.opened) {
this.Editor.InlineToolbar.leaf(direction);
}
}
/**
* Leaf Conversion Toolbar Tools
* @param {string} direction
*/
private leafConversionToolbarTools(direction: string): void {
this.Editor.ConversionToolbar.leaf(direction);
}
/**
* Open Toolbar and show BlockSettings before flipping Tools
* @param {string} direction
*/
private leafBlockSettingsTools(direction: string): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.BlockManager.currentBlock.focused = true;
this.Editor.Toolbar.open(true, false);
this.Editor.Toolbar.plusButton.hide();
}
/**
* If BlockSettings is not open, then open BlockSettings
* Next Tab press will leaf Settings Buttons
*/
if (!this.Editor.BlockSettings.opened) {
this.Editor.BlockSettings.open();
}
this.Editor.BlockSettings.leaf(direction);
}
}

View file

@ -223,6 +223,8 @@ export default class BlockManager extends Module {
* @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
*
* @return {Block}
*/
@ -230,13 +232,17 @@ export default class BlockManager extends Module {
toolName: string = this.config.initialBlock,
data: BlockToolData = {},
settings: ToolConfig = {},
index: number = this.currentBlockIndex + 1,
needToFocus: boolean = true,
): Block {
// Increment index before construct,
// because developers can use API/Blocks/getCurrentInputIndex on the render() method
const newIndex = ++this.currentBlockIndex;
const block = this.composeBlock(toolName, data, settings);
this._blocks[newIndex] = block;
this._blocks[index] = block;
if (needToFocus) {
this.currentBlockIndex = index;
}
return block;
}
@ -274,9 +280,11 @@ export default class BlockManager extends Module {
* @param {number} index - index where Block should be inserted
* @param {boolean} needToFocus - if true, updates current Block index
*
* TODO: Remove method and use insert() with index instead (?)
*
* @return {Block} inserted Block
*/
public insertAtIndex(index: number, needToFocus: boolean = false) {
public insertInitialBlockAtIndex(index: number, needToFocus: boolean = false) {
const block = this.composeBlock(this.config.initialBlock, {}, {});
this._blocks[index] = block;
@ -591,6 +599,7 @@ export default class BlockManager extends Module {
Listeners.on(block.holder, 'keydown', (event) => BlockEvents.keydown(event as KeyboardEvent), true);
Listeners.on(block.holder, 'mouseup', (event) => BlockEvents.mouseUp(event));
Listeners.on(block.holder, 'mousedown', (event: MouseEvent) => BlockEvents.mouseDown(event));
Listeners.on(block.holder, 'keyup', (event) => BlockEvents.keyup(event));
Listeners.on(block.holder, 'dragover', (event) => BlockEvents.dragOver(event as DragEvent));
Listeners.on(block.holder, 'dragleave', (event) => BlockEvents.dragLeave(event as DragEvent));

View file

@ -6,6 +6,7 @@
* @version 1.0.0
*/
import Module from '../__module';
import Block from '../block';
import _ from '../utils';
import $ from '../dom';
@ -74,6 +75,14 @@ export default class BlockSelection extends Module {
return BlockManager.blocks.some((block) => block.selected === true);
}
/**
* Return selected Blocks array
* @return {Block[]}
*/
public get selectedBlocks(): Block[] {
return this.Editor.BlockManager.blocks.filter((block: Block) => block.selected);
}
/**
* Flag used to define block selection
* First CMD+A defines it as true and then second CMD+A selects all Blocks
@ -152,13 +161,34 @@ export default class BlockSelection extends Module {
/**
* Clear selection from Blocks
*
* @param {Event} reason - event caused clear of selection
* @param {boolean} restoreSelection - if true, restore saved selection
*/
public clearSelection(restoreSelection = false) {
public clearSelection(reason?: Event, restoreSelection = false) {
const {BlockManager, Caret, RectangleSelection} = this.Editor;
this.needToSelectAll = false;
this.nativeInputSelected = false;
this.readyToBlockSelection = false;
if (!this.anyBlockSelected || this.Editor.RectangleSelection.isRectActivated()) {
/**
* 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)) {
const indexToInsert = BlockManager.removeSelectedBlocks();
BlockManager.insertInitialBlockAtIndex(indexToInsert, true);
Caret.setToBlock(BlockManager.currentBlock);
_.delay(() => {
Caret.insertContentAtCaretPosition(reason.key);
}, 20)();
}
this.Editor.CrossBlockSelection.clear(reason);
if (!this.anyBlockSelected || RectangleSelection.isRectActivated()) {
this.Editor.RectangleSelection.clearSelection();
return;
}
@ -179,15 +209,13 @@ export default class BlockSelection extends Module {
* Reduce each Block and copy its content
*/
public copySelectedBlocks(): void {
const {BlockManager, Sanitizer} = this.Editor;
const fakeClipboard = $.make('div');
BlockManager.blocks.filter((block) => block.selected)
.forEach((block) => {
this.selectedBlocks.forEach((block) => {
/**
* Make <p> tag that holds clean HTML
*/
const cleanHTML = Sanitizer.clean(block.holder.innerHTML, this.sanitizerConfig);
const cleanHTML = this.Editor.Sanitizer.clean(block.holder.innerHTML, this.sanitizerConfig);
const fragment = $.make('p');
fragment.innerHTML = cleanHTML;
@ -226,12 +254,12 @@ export default class BlockSelection extends Module {
}
/**
* First CMD+A Selects current focused blocks,
* and consequent second CMD+A keypress selects all blocks
* First CMD+A selects all input content by native behaviour,
* next CMD+A keypress selects all blocks
*
* @param {keydown} event
* @param {KeyboardEvent} event
*/
private handleCommandA(event): void {
private handleCommandA(event: KeyboardEvent): void {
this.Editor.RectangleSelection.clearSelection();
/** allow default selection on native inputs */
@ -240,7 +268,8 @@ export default class BlockSelection extends Module {
return;
}
const inputs = this.Editor.BlockManager.currentBlock.inputs;
const workingBlock = this.Editor.BlockManager.getBlock(event.target as HTMLElement);
const inputs = workingBlock.inputs;
/**
* If Block has more than one editable element allow native selection
@ -251,14 +280,30 @@ export default class BlockSelection extends Module {
return;
}
/** Prevent default selection */
event.preventDefault();
if (this.needToSelectAll) {
/** Prevent default selection */
event.preventDefault();
/**
* Save selection
* Will be restored when closeSelection fired
*/
this.selection.save();
/**
* Remove Ranges from Selection
*/
SelectionUtils.get()
.removeAllRanges();
this.selectAllBlocks();
this.needToSelectAll = false;
/**
* Close ConversionToolbar when all Blocks selected
*/
this.Editor.ConversionToolbar.close();
} else {
this.selectBlockByIndex();
this.needToSelectAll = true;
}
}

View file

@ -48,28 +48,27 @@ export default class Caret extends Module {
* @return {boolean}
*/
public get isAtStart(): boolean {
/**
* Don't handle ranges
*/
if (!Selection.isCollapsed) {
return false;
}
const selection = Selection.get();
const firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput);
let anchorNode = selection.anchorNode;
let focusNode = selection.focusNode;
/** In case lastNode is native input */
if ($.isNativeInput(firstNode)) {
return (firstNode as HTMLInputElement).selectionEnd === 0;
}
/** Case when selection have been cleared programmatically, for example after CBS */
if (!selection.anchorNode) {
return false;
}
/**
* Workaround case when caret in the text like " |Hello!"
* selection.anchorOffset is 1, but real caret visible position is 0
* @type {number}
*/
let firstLetterPosition = anchorNode.textContent.search(/\S/);
let firstLetterPosition = focusNode.textContent.search(/\S/);
if (firstLetterPosition === -1) { // empty text
firstLetterPosition = 0;
@ -82,16 +81,16 @@ export default class Caret extends Module {
* In this case, anchor node has ELEMENT_NODE node type.
* Anchor offset shows amount of children between start of the element and caret position.
*
* So we use child with anchorOffset index as new anchorNode.
* So we use child with focusOffset index as new anchorNode.
*/
let anchorOffset = selection.anchorOffset;
if (anchorNode.nodeType !== Node.TEXT_NODE && anchorNode.childNodes.length) {
if (anchorNode.childNodes[anchorOffset]) {
anchorNode = anchorNode.childNodes[anchorOffset];
anchorOffset = 0;
let focusOffset = selection.focusOffset;
if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {
if (focusNode.childNodes[focusOffset]) {
focusNode = focusNode.childNodes[focusOffset];
focusOffset = 0;
} else {
anchorNode = anchorNode.childNodes[anchorOffset - 1];
anchorOffset = anchorNode.textContent.length;
focusNode = focusNode.childNodes[focusOffset - 1];
focusOffset = focusNode.textContent.length;
}
}
@ -99,14 +98,28 @@ export default class Caret extends Module {
* In case of
* <div contenteditable>
* <p><b></b></p> <-- first (and deepest) node is <b></b>
* |adaddad <-- anchor node
* |adaddad <-- focus node
* </div>
*/
if ($.isLineBreakTag(firstNode as HTMLElement) || $.isEmpty(firstNode)) {
const leftSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'left');
const nothingAtLeft = leftSiblings.every((node, i) => $.isEmpty(node));
const leftSiblings = this.getHigherLevelSiblings(focusNode as HTMLElement, 'left');
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
*/
const regularLineBreak = $.isLineBreakTag(node);
/**
* Workaround SHIFT+ENTER in Safari, that creates <div><br></div> instead of <br>
*/
const lineBreakInSafari = node.children.length === 1 && $.isLineBreakTag(node.children[0] as HTMLElement);
const isLineBreak = regularLineBreak || lineBreakInSafari;
if (nothingAtLeft && anchorOffset === firstLetterPosition) {
return $.isEmpty(node) && !isLineBreak;
});
if (nothingAtLeft && focusOffset === firstLetterPosition) {
return true;
}
}
@ -115,7 +128,7 @@ export default class Caret extends Module {
* We use <= comparison for case:
* "| Hello" <--- selection.anchorOffset is 0, but firstLetterPosition is 1
*/
return firstNode === null || anchorNode === firstNode && anchorOffset <= firstLetterPosition;
return firstNode === null || focusNode === firstNode && focusOffset <= firstLetterPosition;
}
/**
@ -123,15 +136,8 @@ export default class Caret extends Module {
* @return {boolean}
*/
public get isAtEnd(): boolean {
/**
* Don't handle ranges
*/
if (!Selection.isCollapsed) {
return false;
}
const selection = Selection.get();
let anchorNode = selection.anchorNode;
let focusNode = selection.focusNode;
const lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput, true);
@ -140,6 +146,11 @@ export default class Caret extends Module {
return (lastNode as HTMLInputElement).selectionEnd === (lastNode as HTMLInputElement).value.length;
}
/** Case when selection have been cleared programmatically, for example after CBS */
if (!selection.focusNode) {
return false;
}
/**
* If caret was set by external code, it might be set to text node wrapper.
* <div>hello|</div> <---- Selection references to <div> instead of text node
@ -147,16 +158,16 @@ export default class Caret extends Module {
* In this case, anchor node has ELEMENT_NODE node type.
* Anchor offset shows amount of children between start of the element and caret position.
*
* So we use child with anchorOffset - 1 as new anchorNode.
* So we use child with anchofocusOffset - 1 as new focusNode.
*/
let anchorOffset = selection.anchorOffset;
if (anchorNode.nodeType !== Node.TEXT_NODE && anchorNode.childNodes.length) {
if (anchorNode.childNodes[anchorOffset - 1]) {
anchorNode = anchorNode.childNodes[anchorOffset - 1];
anchorOffset = anchorNode.textContent.length;
let focusOffset = selection.focusOffset;
if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length) {
if (focusNode.childNodes[focusOffset - 1]) {
focusNode = focusNode.childNodes[focusOffset - 1];
focusOffset = focusNode.textContent.length;
} else {
anchorNode = anchorNode.childNodes[0];
anchorOffset = 0;
focusNode = focusNode.childNodes[0];
focusOffset = 0;
}
}
@ -168,12 +179,17 @@ export default class Caret extends Module {
* </div>
*/
if ($.isLineBreakTag(lastNode as HTMLElement) || $.isEmpty(lastNode)) {
const rightSiblings = this.getHigherLevelSiblings(anchorNode as HTMLElement, 'right');
const rightSiblings = this.getHigherLevelSiblings(focusNode as HTMLElement, 'right');
const nothingAtRight = rightSiblings.every((node, i) => {
return i === 0 && $.isLineBreakTag(node as HTMLElement) || $.isEmpty(node);
/**
* If last right sibling is BR isEmpty returns false, but there actually nothing at right
*/
const isLastBR = i === rightSiblings.length - 1 && $.isLineBreakTag(node as HTMLElement);
return (isLastBR) || $.isEmpty(node) && !$.isLineBreakTag(node);
});
if (nothingAtRight && anchorOffset === anchorNode.textContent.length) {
if (nothingAtRight && focusOffset === focusNode.textContent.length) {
return true;
}
}
@ -190,7 +206,7 @@ export default class Caret extends Module {
* We use >= comparison for case:
* "Hello |" <--- selection.anchorOffset is 7, but rightTrimmedText is 6
*/
return anchorNode === lastNode && anchorOffset >= rightTrimmedText.length;
return focusNode === lastNode && focusOffset >= rightTrimmedText.length;
}
/**

View file

@ -0,0 +1,187 @@
import Module from '../__module';
import Block from '../block';
import SelectionUtils from '../selection';
import _ from '../utils';
export default class CrossBlockSelection extends Module {
/**
* Block where selection is started
*/
private firstSelectedBlock: Block;
/**
* Last selected Block
*/
private lastSelectedBlock: Block;
/**
* Sets up listeners
*
* @param {MouseEvent} event - mouse down event
*/
public watchSelection(event: MouseEvent): void {
if (event.button !== _.mouseButtons.LEFT) {
return;
}
const {BlockManager, UI, Listeners} = this.Editor;
this.firstSelectedBlock = BlockManager.getBlock(event.target as HTMLElement);
this.lastSelectedBlock = this.firstSelectedBlock;
Listeners.on(document, 'mouseover', this.onMouseOver);
Listeners.on(document, 'mouseup', this.onMouseUp);
}
/**
* Change selection state of the next Block
* Used for CBS via Shift + arrow keys
*
* @param {boolean} next - if true, toggle next block. Previous otherwise
*/
public toggleBlockSelectedState(next: boolean = true): void {
const {BlockManager} = this.Editor;
if (!this.lastSelectedBlock) {
this.lastSelectedBlock = this.firstSelectedBlock = BlockManager.currentBlock;
}
if (this.firstSelectedBlock === this.lastSelectedBlock) {
this.firstSelectedBlock.selected = true;
SelectionUtils.get().removeAllRanges();
}
const nextBlockIndex = BlockManager.blocks.indexOf(this.lastSelectedBlock) + (next ? 1 : -1);
const nextBlock = BlockManager.blocks[nextBlockIndex];
if (!nextBlock) {
return;
}
if (this.lastSelectedBlock.selected !== nextBlock.selected) {
nextBlock.selected = true;
} else {
this.lastSelectedBlock.selected = false;
}
this.lastSelectedBlock = nextBlock;
}
/**
* Clear saved state
*
* @param {Event} reason - event caused clear of selection
*/
public clear(reason?: Event) {
const {BlockManager, BlockSelection, Caret} = this.Editor;
const fIndex = BlockManager.blocks.indexOf(this.firstSelectedBlock);
const lIndex = BlockManager.blocks.indexOf(this.lastSelectedBlock);
if (BlockSelection.anyBlockSelected && fIndex > -1 && lIndex > -1) {
if (reason && reason instanceof KeyboardEvent) {
/**
* Set caret depending on pressed key if pressed key is an arrow.
*/
switch (reason.keyCode) {
case _.keyCodes.DOWN:
case _.keyCodes.RIGHT:
Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);
break;
case _.keyCodes.UP:
case _.keyCodes.LEFT:
Caret.setToBlock(BlockManager.blocks[Math.min(fIndex, lIndex)], Caret.positions.START);
break;
default:
Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);
}
} else {
/**
* By default set caret at the end of the last selected block
*/
Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);
}
}
this.firstSelectedBlock = this.lastSelectedBlock = null;
}
/**
* Mouse up event handler.
* Removes the listeners
*/
private onMouseUp = (): void => {
const {Listeners} = this.Editor;
Listeners.off(document, 'mouseover', this.onMouseOver);
Listeners.off(document, 'mouseup', this.onMouseUp);
}
/**
* Mouse over event handler
* Gets target and related blocks and change selected state for blocks in between
*
* @param {MouseEvent} event
*/
private onMouseOver = (event: MouseEvent): void => {
const {BlockManager} = this.Editor;
const relatedBlock = BlockManager.getBlockByChildNode(event.relatedTarget as Node) || this.lastSelectedBlock;
const targetBlock = BlockManager.getBlockByChildNode(event.target as Node);
if (!relatedBlock || !targetBlock) {
return;
}
if (targetBlock === relatedBlock) {
return;
}
if (relatedBlock === this.firstSelectedBlock) {
SelectionUtils.get().removeAllRanges();
relatedBlock.selected = true;
targetBlock.selected = true;
return;
}
if (targetBlock === this.firstSelectedBlock) {
relatedBlock.selected = false;
targetBlock.selected = false;
return;
}
this.toggleBlocksSelectedState(relatedBlock, targetBlock);
this.lastSelectedBlock = targetBlock;
}
/**
* Change blocks selection state between passed two blocks.
*
* @param {Block} firstBlock
* @param {Block} lastBlock
*/
private toggleBlocksSelectedState(firstBlock: Block, lastBlock: Block): void {
const {BlockManager} = this.Editor;
const fIndex = BlockManager.blocks.indexOf(firstBlock);
const lIndex = BlockManager.blocks.indexOf(lastBlock);
/**
* If first and last block have the different selection state
* it means we should't toggle selection of the first selected block.
* In the other case we shouldn't toggle the last selected block.
*/
const shouldntSelectFirstBlock = firstBlock.selected !== lastBlock.selected;
for (let i = Math.min(fIndex, lIndex); i <= Math.max(fIndex, lIndex); i++) {
const block = BlockManager.blocks[i];
if (
block !== this.firstSelectedBlock &&
block !== (shouldntSelectFirstBlock ? firstBlock : lastBlock)
) {
BlockManager.blocks[i].selected = !BlockManager.blocks[i].selected;
}
}
}
}

View file

@ -22,9 +22,9 @@ export interface ListenerData {
handler: (event: Event) => void;
/**
* Should event bubbling be used or not
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
*/
useCapture: boolean;
options: boolean | AddEventListenerOptions;
}
/**
@ -56,19 +56,19 @@ export default class Listeners extends Module {
* @param {EventTarget} element - DOM element that needs to be listened
* @param {String} eventType - event type
* @param {Function} handler - method that will be fired on event
* @param {Boolean} useCapture - use event bubbling
* @param {Boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}
*/
public on(
element: EventTarget,
eventType: string,
handler: (event: Event) => void,
useCapture: boolean = false,
options: boolean | AddEventListenerOptions = false,
): void {
const assignedEventData = {
element,
eventType,
handler,
useCapture,
options,
};
const alreadyExist = this.findOne(element, eventType, handler);
@ -76,7 +76,7 @@ export default class Listeners extends Module {
if (alreadyExist) { return; }
this.allListeners.push(assignedEventData);
element.addEventListener(eventType, handler, useCapture);
element.addEventListener(eventType, handler, options);
}
/**
@ -85,13 +85,13 @@ export default class Listeners extends Module {
* @param {EventTarget} element - DOM element that we removing listener
* @param {String} eventType - event type
* @param {Function} handler - remove handler, if element listens several handlers on the same event type
* @param {Boolean} useCapture - use event bubbling
* @param {Boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}
*/
public off(
element: EventTarget,
eventType: string,
handler: (event: Event) => void,
useCapture: boolean = false,
handler?: (event: Event) => void,
options?: boolean | AddEventListenerOptions,
): void {
const existingListeners = this.findAll(element, eventType, handler);
@ -100,19 +100,20 @@ export default class Listeners extends Module {
if (index > 0) {
this.allListeners.splice(index, 1);
listener.element.removeEventListener(listener.eventType, listener.handler, listener.options);
}
});
element.removeEventListener(eventType, handler, useCapture);
}
/**
* @param {EventTarget} element
* @param {String} eventType
* @param {Function} handler
* @return {EventTarget|null}
* @return {ListenerData|null}
*/
public findOne(element: EventTarget, eventType: string, handler: (event: Event) => void): ListenerData {
public findOne(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData {
const foundListeners = this.findAll(element, eventType, handler);
return foundListeners.length > 0 ? foundListeners[0] : null;
@ -122,9 +123,9 @@ export default class Listeners extends Module {
* @param {EventTarget} element
* @param {String} eventType
* @param {Function} handler
* @return {Array}
* @return {ListenerData[]}
*/
public findAll(element: EventTarget, eventType: string, handler: (event: Event) => void): ListenerData[] {
public findAll(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData[] {
let found;
const foundByEventTargets = element ? this.findByEventTarget(element) : [];

View file

@ -32,9 +32,16 @@ export default class ModificationsObserver extends Module {
* @type {Function}
*/
private mutationDebouncer = _.debounce( () => {
this.updateNativeInputs();
this.config.onChange();
}, ModificationsObserver.DebounceTimer);
/**
* Array of native inputs in Blocks.
* Changes in native inputs are not handled by modification observer, so we need to set change event listeners on them
*/
private nativeInputs: HTMLElement[] = [];
/**
* Clear timeout and set null to mutationDebouncer property
*/
@ -42,6 +49,7 @@ export default class ModificationsObserver extends Module {
this.mutationDebouncer = null;
this.observer.disconnect();
this.observer = null;
this.nativeInputs.forEach((input) => this.Editor.Listeners.off(input, 'input', this.mutationDebouncer));
}
/**
@ -142,4 +150,19 @@ export default class ModificationsObserver extends Module {
this.mutationDebouncer();
}
}
/**
* Gets native inputs and set oninput event handler
*/
private updateNativeInputs(): void {
if (this.nativeInputs) {
this.nativeInputs.forEach((input) => {
this.Editor.Listeners.off(input, 'input');
});
}
this.nativeInputs = Array.from(this.Editor.UI.nodes.redactor.querySelectorAll('textarea, input, select'));
this.nativeInputs.forEach((input) => this.Editor.Listeners.on(input, 'input', this.mutationDebouncer));
}
}

View file

@ -120,6 +120,11 @@ export default class Paste extends Module {
[tool: string]: FilesSubstitution,
} = {};
/**
* List of tools which do not need a paste handling
*/
private exceptionList: string[] = [];
/**
* Set onPaste callback and collect tools` paste configurations
*
@ -247,7 +252,12 @@ export default class Paste extends Module {
data: {},
}) as BlockTool;
if (!toolInstance.onPaste || typeof toolInstance.onPaste !== 'function') {
if (tool.pasteConfig === false) {
this.exceptionList.push(name);
return;
}
if (typeof toolInstance.onPaste !== 'function') {
return;
}
@ -378,15 +388,23 @@ export default class Paste extends Module {
* @param {ClipboardEvent} event
*/
private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => {
const {BlockManager, Tools, 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;
}
/**
* If Tools is in list of exceptions, skip processing of paste event
*/
if (BlockManager.currentBlock && this.exceptionList.includes(BlockManager.currentBlock.name)) {
return;
}
event.preventDefault();
this.processDataTransfer(event.clipboardData);
@ -496,7 +514,7 @@ export default class Paste extends Module {
break;
}
const {tags} = Tools.blockTools[tool].pasteConfig;
const {tags} = Tools.blockTools[tool].pasteConfig as PasteConfig;
const toolTags = tags.reduce((result, tag) => {
result[tag.toLowerCase()] = {};

View file

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

View file

@ -0,0 +1,308 @@
import Module from '../../__module';
import $ from '../../dom';
import {BlockToolConstructable} from '../../../../types';
import _ from '../../utils';
import {SavedData} from '../../../types-internal/block-data';
import Block from '../../block';
/**
* Block Converter
*/
export default class ConversionToolbar extends Module {
/**
* CSS getter
*/
public static get CSS(): { [key: string]: string } {
return {
conversionToolbarWrapper: 'ce-conversion-toolbar',
conversionToolbarShowed: 'ce-conversion-toolbar--showed',
conversionToolbarTools: 'ce-conversion-toolbar__tools',
conversionTool: 'ce-conversion-tool',
conversionToolFocused : 'ce-conversion-tool--focused',
conversionToolActive : 'ce-conversion-tool--active',
};
}
/**
* HTML Elements used for UI
*/
public nodes: { [key: string]: HTMLElement } = {
wrapper: null,
tools: null,
};
/**
* Conversion Toolbar open/close state
* @type {boolean}
*/
public opened: boolean = false;
/**
* Focused button index
* -1 equals no chosen Tool
* @type {number}
*/
private focusedButtonIndex: number = -1;
/**
* Available tools
*/
private tools: { [key: string]: HTMLElement } = {};
/**
* Create UI of Conversion Toolbar
*/
public make(): void {
this.nodes.wrapper = $.make('div', ConversionToolbar.CSS.conversionToolbarWrapper);
this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);
/**
* Add Tools that has 'import' method
*/
this.addTools();
$.append(this.nodes.wrapper, this.nodes.tools);
$.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);
}
/**
* Try to show Conversion Toolbar near passed Block
* @param {Block} block - block to convert
*/
public tryToShow(block: Block): void {
const hasExportConfig = block.class.conversionConfig && block.class.conversionConfig.export;
if (!hasExportConfig) {
return;
}
const currentToolName = block.name;
/**
* Focus current tool in conversion toolbar
*/
if (this.tools[currentToolName]) {
/**
* Drop previous active button before moving
*/
if (this.focusedButton && this.focusedButton.classList.contains(ConversionToolbar.CSS.conversionToolActive)) {
this.focusedButton.classList.remove(ConversionToolbar.CSS.conversionToolActive);
}
this.tools[currentToolName].classList.add(ConversionToolbar.CSS.conversionToolActive);
}
this.move(block);
if (!this.opened) {
this.open();
}
}
/**
* Shows Conversion Toolbar
*/
public open(): void {
this.opened = true;
this.nodes.wrapper.classList.add(ConversionToolbar.CSS.conversionToolbarShowed);
}
/**
* Closes Conversion Toolbar
*/
public close(): void {
this.opened = false;
this.nodes.wrapper.classList.remove(ConversionToolbar.CSS.conversionToolbarShowed);
this.dropFocusedButton();
}
/**
* Leaf tools by Tab
* @todo use class with tool iterator
*/
public leaf(direction: string = 'right'): void {
const toolsElements = (Array.from(this.nodes.tools.childNodes) as HTMLElement[]);
this.focusedButtonIndex = $.leafNodesAndReturnIndex(
toolsElements, this.focusedButtonIndex, direction, ConversionToolbar.CSS.conversionToolFocused,
);
}
/**
* Returns focused tool as HTML element
* @return {HTMLElement}
*/
public get focusedButton(): HTMLElement {
if (this.focusedButtonIndex === -1) {
return null;
}
return (this.nodes.tools.childNodes[this.focusedButtonIndex] as HTMLElement);
}
/**
* Drops focused button
*/
public dropFocusedButton() {
Object.values(this.tools).forEach( (tool) => {
(tool as HTMLElement).classList
.remove(ConversionToolbar.CSS.conversionToolActive, ConversionToolbar.CSS.conversionToolFocused);
});
this.focusedButtonIndex = -1;
}
/**
* Replaces one Block with another
* For that Tools must provide import/export methods
*
* @param {string} replacingToolName
*/
public async replaceWithBlock(replacingToolName: string): Promise <void> {
/**
* At first, we get current Block data
* @type {BlockToolConstructable}
*/
const currentBlockClass = this.Editor.BlockManager.currentBlock.class;
const currentBlockName = this.Editor.BlockManager.currentBlock.name;
const savedBlock = await this.Editor.BlockManager.currentBlock.save() as SavedData;
const blockData = savedBlock.data;
/**
* When current Block name is equals to the replacing tool Name,
* than convert this Block back to the initial Block
*/
if (currentBlockName === replacingToolName) {
replacingToolName = this.config.initialBlock;
}
/**
* Getting a class of replacing Tool
* @type {BlockToolConstructable}
*/
const replacingTool = this.Editor.Tools.toolsClasses[replacingToolName] as BlockToolConstructable;
/**
* Export property can be:
* 1) Function Tool defines which data to return
* 2) String the name of saved property
*
* In both cases returning value must be a string
*/
let exportData: string = '';
const exportProp = currentBlockClass.conversionConfig.export;
if (typeof exportProp === 'function') {
exportData = exportProp(blockData);
} else if (typeof exportProp === 'string') {
exportData = blockData[exportProp];
} 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;
}
/**
* Clean exported data with replacing sanitizer config
*/
const cleaned: string = this.Editor.Sanitizer.clean(
exportData,
replacingTool.sanitize,
);
/**
* «import» property can be Function or String
* function accept imported string and compose tool data object
* string the name of data field to import
*/
let newBlockData = {};
const importProp = replacingTool.conversionConfig.import;
if (typeof importProp === 'function') {
newBlockData = importProp(cleaned);
} else if (typeof importProp === 'string') {
newBlockData[importProp] = cleaned;
} 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.BlockSelection.clearSelection();
this.close();
_.delay(() => {
this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock);
}, 10)();
}
/**
* Move Conversion Toolbar to the working Block
*/
private move(block: Block): void {
const blockRect = block.pluginsContent.getBoundingClientRect();
const wrapperRect = this.Editor.UI.nodes.wrapper.getBoundingClientRect();
const newCoords = {
x: blockRect.left - wrapperRect.left,
y: blockRect.top + blockRect.height - wrapperRect.top,
};
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
}
/**
* Iterates existing Tools and inserts to the ConversionToolbar
* if tools have ability to import
*/
private addTools(): void {
const tools = this.Editor.Tools.blockTools;
for (const toolName in tools) {
if (!tools.hasOwnProperty(toolName)) {
continue;
}
const api = this.Editor.Tools.apiSettings;
const toolClass = tools[toolName] as BlockToolConstructable;
const toolToolboxSettings = toolClass[api.TOOLBOX];
const conversionConfig = toolClass[api.CONVERSION_CONFIG];
/**
* Skip tools that don't pass 'toolbox' property
*/
if (_.isEmpty(toolToolboxSettings) || !toolToolboxSettings.icon) {
continue;
}
/**
* Skip tools without «import» rule specified
*/
if (!conversionConfig || !conversionConfig.import) {
continue;
}
this.addTool(toolName, toolToolboxSettings.icon);
}
}
/**
* Add tool to the Conversion Toolbar
*/
private addTool(toolName: string, toolIcon: string): void {
const tool = $.make('div', [ ConversionToolbar.CSS.conversionTool ]);
tool.dataset.tool = toolName;
tool.innerHTML = toolIcon;
$.append(this.nodes.tools, tool);
this.tools[toolName] = tool;
this.Editor.Listeners.on(tool, 'click', async () => {
await this.replaceWithBlock(toolName);
});
}
}

View file

@ -196,18 +196,27 @@ export default class Toolbar extends Module {
return;
}
/**
* Set Toolbar Min Height as Block's height
* Plus Button and Toolbox positioned at the center of the Toolbar
*/
const contentOffset = Math.floor(currentBlock.offsetHeight / 2);
const { isMobile } = this.Editor.UI;
const blockHeight = currentBlock.offsetHeight;
let toolbarY = currentBlock.offsetTop;
/**
* 1) On desktop Toolbar at the top of Block, Plus/Toolbox moved the center of Block
* 2) On mobile Toolbar at the bottom of Block
*/
if (!isMobile) {
const contentOffset = Math.floor(blockHeight / 2);
this.nodes.plusButton.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
this.Editor.Toolbox.nodes.toolbox.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
} else {
toolbarY += blockHeight;
}
this.nodes.plusButton.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
this.Editor.Toolbox.nodes.toolbox.style.transform = `translate3d(0, calc(${contentOffset}px - 50%), 0)`;
/**
* Move Toolbar to the Top coordinate of Block
*/
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(currentBlock.offsetTop)}px, 0)`;
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(toolbarY)}px, 0)`;
}
/**

View file

@ -23,6 +23,8 @@ export default class InlineToolbar extends Module {
public CSS = {
inlineToolbar: 'ce-inline-toolbar',
inlineToolbarShowed: 'ce-inline-toolbar--showed',
inlineToolbarLeftOriented: 'ce-inline-toolbar--left-oriented',
inlineToolbarRightOriented: 'ce-inline-toolbar--right-oriented',
buttonsWrapper: 'ce-inline-toolbar__buttons',
actionsWrapper: 'ce-inline-toolbar__actions',
inlineToolButton: 'ce-inline-tool',
@ -79,6 +81,12 @@ export default class InlineToolbar extends Module {
*/
private focusedButtonIndex: number = -1;
/**
* Cache for Inline Toolbar width
* @type {number}
*/
private width: number = 0;
/**
* Inline Toolbar Tools
*
@ -129,6 +137,10 @@ export default class InlineToolbar extends Module {
*/
this.addTools();
/**
* Recalculate initial width with all buttons
*/
this.recalculateWidth();
}
/**
@ -137,17 +149,21 @@ export default class InlineToolbar extends Module {
*/
/**
* Shows Inline Toolbar by keyup/mouseup
* @param {KeyboardEvent|MouseEvent} event
* 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 handleShowingEvent(event): void {
public tryToShow(needToClose: boolean = false): void {
if (!this.allowedToShow()) {
this.close();
if (needToClose) {
this.close();
}
return;
}
this.move();
this.open();
this.Editor.Toolbar.close();
/** Check Tools state for selected fragment */
this.checkToolsState();
@ -178,6 +194,26 @@ export default class InlineToolbar extends Module {
newCoords.x += Math.floor(selectionRect.width / 2);
}
/**
* Inline Toolbar has -50% translateX, so we need to check real coords to prevent overflowing
*/
const realLeftCoord = newCoords.x - this.width / 2;
const realRightCoord = newCoords.x + this.width / 2;
/**
* By default, Inline Toolbar has top-corner at the center
* We are adding a modifiers for to move corner to the left or right
*/
this.nodes.wrapper.classList.toggle(
this.CSS.inlineToolbarLeftOriented,
realLeftCoord < this.Editor.UI.contentRect.left,
);
this.nodes.wrapper.classList.toggle(
this.CSS.inlineToolbarRightOriented,
realRightCoord > this.Editor.UI.contentRect.right,
);
this.nodes.wrapper.style.left = Math.floor(newCoords.x) + 'px';
this.nodes.wrapper.style.top = Math.floor(newCoords.y) + 'px';
}
@ -246,12 +282,6 @@ export default class InlineToolbar extends Module {
* Shows Inline Toolbar
*/
public open(): void {
/**
* Check if inline toolbar is allowed to show or not
*/
if (!this.allowedToShow()) {
return;
}
/**
* Filter inline-tools and show only allowed by Block's Tool
@ -308,6 +338,7 @@ export default class InlineToolbar extends Module {
// The selection of the element only in contenteditable
const contenteditable = target.closest('[contenteditable="true"]');
if (contenteditable === null) {
return false;
}
@ -366,6 +397,18 @@ export default class InlineToolbar extends Module {
if (lastVisibleButton) {
lastVisibleButton.classList.add(this.CSS.inlineToolButtonLast);
}
/**
* Recalculate width because some buttons can be hidden
*/
this.recalculateWidth();
}
/**
* Recalculate inline toolbar width
*/
private recalculateWidth(): void {
this.width = this.nodes.wrapper.offsetWidth;
}
/**

View file

@ -123,6 +123,7 @@ export default class Tools extends Module {
/**
* Constant for available Tools Settings
* @todo separate internal and external options
* @return {object}
*/
public get apiSettings() {
@ -135,6 +136,7 @@ export default class Tools extends Module {
SHORTCUT: 'shortcut',
TOOLBOX: 'toolbox',
SANITIZE_CONFIG: 'sanitize',
CONVERSION_CONFIG: 'conversionConfig',
};
}
@ -151,7 +153,7 @@ export default class Tools extends Module {
private readonly toolsAvailable: {[name: string]: ToolConstructable} = {};
/**
* Tools` classes not availbale to use beacause of preparation failure
* Tools` classes not available to use because of preparation failure
*/
private readonly toolsUnavailable: {[name: string]: ToolConstructable} = {};

View file

@ -13,6 +13,7 @@ import $ from '../dom';
import _ from '../utils';
import Selection from '../selection';
import Block from '../block';
/**
* @class
@ -53,10 +54,37 @@ export default class UI extends Module {
}
/**
* Width of center column of Editor
* @type {number}
* Return Width of center column of Editor
* @return {DOMRect}
*/
public contentWidth: number = 650;
public get contentRect(): DOMRect {
if (this.contentRectCache) {
return this.contentRectCache;
}
const someBlock = this.nodes.wrapper.querySelector(`.${Block.CSS.content}`);
/**
* When Editor is not ready, there is no Blocks, so return the default value
*/
if (!someBlock) {
return {
width: 650,
left: 0,
right: 0,
} as DOMRect;
}
this.contentRectCache = someBlock.getBoundingClientRect() as DOMRect;
return this.contentRectCache;
}
/**
* Flag that became true on mobile viewport
* @type {boolean}
*/
public isMobile: boolean = false;
/**
* HTML Elements used for UI
@ -67,6 +95,21 @@ export default class UI extends Module {
redactor: null,
};
/**
* 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(() => {
this.windowResize();
}, 200);
/**
* Adds loader to editor while content is not ready
*/
@ -88,8 +131,19 @@ export default class UI extends Module {
* Making main interface
*/
public async prepare(): Promise<void> {
/**
* Detect mobile version
*/
this.checkIsMobile();
/**
* Make main UI elements
*/
await this.make();
/**
* Loader for rendering process
*/
this.addLoader();
/**
@ -107,6 +161,11 @@ export default class UI extends Module {
*/
await this.Editor.InlineToolbar.make();
/**
* Make the Converter tool holder
*/
await this.Editor.ConversionToolbar.make();
/**
* Load and append CSS
*/
@ -134,6 +193,13 @@ export default class UI extends Module {
this.nodes.holder.innerHTML = '';
}
/**
* Check for mobile mode and cache a result
*/
private checkIsMobile() {
this.isMobile = window.innerWidth < 650;
}
/**
* Makes Editor.js interface
* @return {Promise<void>}
@ -154,7 +220,7 @@ export default class UI extends Module {
/**
* If Editor has injected into the narrow container, enable Narrow Mode
*/
if (this.nodes.holder.offsetWidth < this.contentWidth) {
if (this.nodes.holder.offsetWidth < this.contentRect.width) {
this.nodes.wrapper.classList.add(this.CSS.editorWrapperNarrow);
}
@ -202,6 +268,36 @@ export default class UI extends Module {
);
this.Editor.Listeners.on(document, 'keydown', (event) => this.documentKeydown(event as KeyboardEvent), true);
this.Editor.Listeners.on(document, 'click', (event) => this.documentClicked(event as MouseEvent), true);
/**
* Handle selection change on mobile devices for the Inline Toolbar support
*/
if (_.isTouchSupported()) {
this.Editor.Listeners.on(document, 'selectionchange', (event) => {
this.selectionChanged(event as Event);
}, true);
}
this.Editor.Listeners.on(window, 'resize', () => {
this.resizeDebouncer();
}, {
passive: true,
});
}
/**
* Resize window handler
*/
private windowResize(): void {
/**
* Invalidate content zone size cached, because it may be changed
*/
this.contentRectCache = null;
/**
* Detect mobile version
*/
this.checkIsMobile();
}
/**
@ -257,10 +353,10 @@ export default class UI extends Module {
if (BlockSelection.anyBlockSelected) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertAtIndex(selectionPositionIndex, true), Caret.positions.START);
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection();
BlockSelection.clearSelection(event);
/**
* Stop propagations
@ -277,7 +373,7 @@ export default class UI extends Module {
* @param event
*/
private enterPressed(event: KeyboardEvent): void {
const {BlockManager, BlockSelection, Caret, BlockSettings} = this.Editor;
const { BlockManager, BlockSelection, Caret, BlockSettings, ConversionToolbar } = this.Editor;
const hasPointerToBlock = BlockManager.currentBlockIndex >= 0;
/**
@ -294,16 +390,23 @@ export default class UI extends Module {
BlockSettings.focusedButton.click();
/**
* Add animation on click
* Focused button can be deleted by click, for example with 'Remove Block' api
*/
BlockSettings.focusedButton.classList.add(BlockSettings.CSS.focusedButtonAnimated);
if (BlockSettings.focusedButton) {
/**
* Add animation on click
*/
BlockSettings.focusedButton.classList.add(BlockSettings.CSS.focusedButtonAnimated);
/**
* Remove animation class
*/
_.delay( () => {
BlockSettings.focusedButton.classList.remove(BlockSettings.CSS.focusedButtonAnimated);
}, 280)();
/**
* Remove animation class
*/
_.delay( () => {
if (BlockSettings.focusedButton) {
BlockSettings.focusedButton.classList.remove(BlockSettings.CSS.focusedButtonAnimated);
}
}, 280)();
}
/**
* Restoring focus on current Block
@ -322,12 +425,21 @@ export default class UI extends Module {
return;
}
if (ConversionToolbar.opened && ConversionToolbar.focusedButton) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
ConversionToolbar.focusedButton.click();
return;
}
if (BlockSelection.anyBlockSelected) {
const selectionPositionIndex = BlockManager.removeSelectedBlocks();
Caret.setToBlock(BlockManager.insertAtIndex(selectionPositionIndex, true), Caret.positions.START);
Caret.setToBlock(BlockManager.insertInitialBlockAtIndex(selectionPositionIndex, true), Caret.positions.START);
/** Clear selection */
BlockSelection.clearSelection();
BlockSelection.clearSelection(event);
/**
* Stop propagations
@ -367,7 +479,7 @@ export default class UI extends Module {
this.Editor.Toolbar.plusButton.show();
}
this.Editor.BlockSelection.clearSelection();
this.Editor.BlockSelection.clearSelection(event);
}
/**
@ -380,8 +492,6 @@ export default class UI extends Module {
* Do not fire check on clicks at the Inline Toolbar buttons
*/
const target = event.target as HTMLElement;
const clickedOnInlineToolbarButton = target.closest(`.${this.Editor.InlineToolbar.CSS.inlineToolbar}`);
const clickedInsideOfEditor = this.nodes.holder.contains(target) || Selection.isAtEditor;
if (!clickedInsideOfEditor) {
@ -394,13 +504,8 @@ export default class UI extends Module {
this.Editor.BlockManager.dropPointer();
this.Editor.InlineToolbar.close();
this.Editor.Toolbar.close();
this.Editor.BlockSelection.clearSelection();
} else if (!clickedOnInlineToolbarButton) {
/**
* Move inline toolbar to the focused Block
*/
this.Editor.InlineToolbar.handleShowingEvent(event);
this.Editor.BlockSelection.clearSelection(event);
this.Editor.ConversionToolbar.close();
}
if (Selection.isAtEditor) {
@ -441,7 +546,6 @@ export default class UI extends Module {
*
*/
private redactorClicked(event: MouseEvent): void {
if (!Selection.isCollapsed) {
return;
}
@ -511,13 +615,29 @@ export default class UI extends Module {
this.Editor.Toolbar.plusButton.show();
}
}
/** Clear selection */
this.Editor.BlockSelection.clearSelection();
}
/**
* Append prebuilded sprite with SVG icons
* Handle selection changes on mobile devices
* Uses for showing the Inline Toolbar
* @param {Event} event
*/
private selectionChanged(event: Event): void {
const focusedElement = Selection.anchorElement as Element;
/**
* Event can be fired on clicks at the Editor elements, for example, at the Inline Toolbar
* We need to skip such firings
*/
if (!focusedElement || !focusedElement.closest(`.${Block.CSS.content}`)) {
return;
}
this.Editor.InlineToolbar.tryToShow();
}
/**
* Append prebuilt sprite with SVG icons
*/
private appendSVGSprite(): void {
const spriteHolder = $.make('div');

View file

@ -2,6 +2,7 @@
* TextRange interface fot IE9-
*/
import _ from './utils';
import $ from './dom';
interface TextRange {
boundingTop: number;
@ -55,6 +56,30 @@ export default class SelectionUtils {
return selection ? selection.anchorNode : null;
}
/**
* Returns selected anchor element
* @return {Element|null}
*/
static get anchorElement(): Element | null {
const selection = window.getSelection();
if (!selection) {
return null;
}
const anchorNode = selection.anchorNode;
if (!anchorNode) {
return null;
}
if (!$.isElement(anchorNode)) {
return anchorNode.parentElement;
} else {
return anchorNode;
}
}
/**
* Returns selection offset according to the anchor node
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
@ -76,6 +101,24 @@ export default class SelectionUtils {
return selection ? selection.isCollapsed : null;
}
/**
* Returns true if 85% of text content is selected
* @return {boolean}
*/
public static almostAllSelected(targetText: string): boolean {
const range = SelectionUtils.range;
if (!range) {
return false;
}
const copiedFragment = range.cloneContents();
const lengthOfWholeText = targetText.length;
const lengthOfCopiedText = copiedFragment.textContent.length;
return lengthOfCopiedText / lengthOfWholeText > 0.85;
}
/**
* Check current selection if it is at Editor's zone
* @return {boolean}

@ -1 +1 @@
Subproject commit fa20d187729c72d41129d2e4e89c3a6e989eed12
Subproject commit 69f0c1a24cdfa443dcda9dca71015472709ae2fb

View file

@ -83,6 +83,33 @@ export default class Util {
};
}
/**
* Return mouse buttons codes
*/
static get mouseButtons() {
return {
LEFT: 0,
WHEEL: 1,
RIGHT: 2,
BACKWARD: 3,
FORWARD: 4,
};
}
/**
* Returns true if passed key code is printable (a-Z, 0-9, etc) character.
* @param {number} keyCode
* @return {boolean}
*/
public static isPrintableKey( keyCode: number ): boolean {
return (keyCode > 47 && keyCode < 58) || // number keys
keyCode === 32 || keyCode === 13 || // Spacebar & return key(s)
(keyCode > 64 && keyCode < 91) || // letter keys
(keyCode > 95 && keyCode < 112) || // Numpad keys
(keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)
(keyCode > 218 && keyCode < 223); // [\]' (in order)
}
/**
* Fires a promise sequence asyncronically
*
@ -316,7 +343,7 @@ export default class Util {
* @return {object}
*/
public static deepMerge(target, ...sources) {
const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item);
const isObject = (item) => item && Util.typeof(item) === 'object';
if (!sources.length) { return target; }
const source = sources.shift();
@ -338,4 +365,25 @@ export default class Util {
return Util.deepMerge(target, ...sources);
}
/**
* Return true if current device supports touch events
*
* Note! This is a simple solution, it can give false-positive results.
* To detect touch devices more carefully, use 'touchstart' event listener
* @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
*
* @return {boolean}
*/
public static isTouchSupported(): boolean {
return 'ontouchstart' in document.documentElement;
}
/**
* Return string representation of the object type
*
* @param {any} object
*/
public static typeof(object: any): string {
return Object.prototype.toString.call(object).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
}

View file

@ -100,3 +100,20 @@
transform: scale3d(1, 1, 1);
}
}
@keyframes panelShowing {
from {
opacity: 0;
transform: translateY(-8px) scale(0.9);
}
70% {
opacity: 1;
transform: translateY(2px);
}
to {
transform: translateY(0);
}
}

View file

@ -21,6 +21,14 @@
animation: selectionBounce 0.2s 1;
animation-fill-mode: forwards;
/**
* Workaround Safari case when user can select inline-fragment with cross-block-selection
*/
& [contenteditable] {
-webkit-user-select: none;
user-select: none;
}
img,
.ce-stub {
opacity: 0.55;

View file

@ -0,0 +1,54 @@
.ce-conversion-toolbar {
@apply --overlay-pane;
padding: 3px;
box-shadow: 0 6px 12px -6px rgba(131, 147, 173, 0.46),
5px -12px 34px -13px rgba(97, 105, 134, 0.6),
0 26px 52px 3px rgba(147, 165, 186, 0.24);
opacity: 0;
visibility: hidden;
will-change: transform, opacity;
transition: transform 150ms ease, opacity 250ms ease;
transform: translateY(8px) scale(0.9);
&::before {
left: 20px;
}
&--showed {
opacity: 1;
visibility: visible;
transform: none;
}
[hidden] {
display: none !important;
}
&__buttons {
display: flex;
}
}
.ce-conversion-tool {
@apply --toolbar-button;
line-height: normal;
&:not(:last-of-type) {
margin-right: 2px;
}
&--last {
margin-right: 0 !important;
}
&:hover,
&--active {
color: var(--color-active-icon) !important;
}
&--active {
animation: bounceIn 0.75s 1;
animation-fill-mode: forwards;
}
}

View file

@ -25,11 +25,11 @@
@apply --toolbar-button;
&:not(:nth-child(3n+3)) {
margin-right: 5px;
margin-right: 3px;
}
&:nth-child(n+4) {
margin-top: 5px;
margin-top: 3px;
}
&--active {

View file

@ -1,14 +1,36 @@
.ce-inline-toolbar {
@apply --overlay-pane;
padding: 6px;
transform: translateX(-50%);
display: none;
padding: 3px;
transform: translateX(-50%) translateY(8px) scale(0.9);
box-shadow: 0 6px 12px -6px rgba(131, 147, 173, 0.46),
5px -12px 34px -13px rgba(97, 105, 134, 0.6),
0 26px 52px 3px rgba(147, 165, 186, 0.24);
opacity: 0;
visibility: hidden;
transition: transform 150ms ease, opacity 250ms ease;
will-change: transform, opacity;
&--showed {
display: block;
opacity: 1;
visibility: visible;
transform: translateX(-50%)
}
&--left-oriented {
transform: translateX(-23px) translateY(8px) scale(0.9);
}
&--left-oriented&--showed {
transform: translateX(-23px);
}
&--right-oriented {
transform: translateX(-100%) translateY(8px) scale(0.9);
margin-left: 23px;
}
&--right-oriented&--showed {
transform: translateX(-100%);
}
[hidden] {
@ -25,7 +47,7 @@
line-height: normal;
&:not(:last-of-type) {
margin-right: 5px;
margin-right: 2px;
}
&--last {
@ -52,7 +74,7 @@
outline: none;
border: 0;
border-radius: 3px;
margin: 6px 0 0;
margin: 3px 0 0;
font-size: 13px;
padding: 8px;
width: 100%;

View file

@ -3,6 +3,7 @@
@import './toolbar.css';
@import './toolbox.css';
@import './inline-toolbar.css';
@import './conversion-toolbar.css';
@import './settings.css';
@import './block.css';
@import './animations.css';

View file

@ -2,7 +2,7 @@
@apply --overlay-pane;
right: 5px;
top: 35px;
min-width: 124px;
min-width: 114px;
@media (--mobile){
bottom: 50px;
@ -23,19 +23,19 @@
&--opened {
display: block;
animation-duration: 0.5s;
animation-name: bounceIn;
animation-duration: 0.1s;
animation-name: panelShowing;
}
&__plugin-zone {
&:not(:empty){
padding: 6px 6px 0;
padding: 3px 3px 0;
}
}
&__default-zone {
&:not(:empty){
padding: 6px;
padding: 3px;
}
}
@ -43,11 +43,11 @@
@apply --toolbar-button;
&:not(:nth-child(3n+3)) {
margin-right: 5px;
margin-right: 3px;
}
&:nth-child(n+4) {
margin-top: 5px;
margin-top: 3px;
}
line-height: 32px;

View file

@ -10,16 +10,9 @@
display: none;
@media (--mobile) {
position: fixed;
bottom: 0;
top: auto;
left: 0;
right: 0;
z-index: 9;
height: 50px;
background: #fff;
box-shadow: 0 -2px 12px rgba(60, 67, 81, 0.18);
transform: none !important;
@apply --overlay-pane;
padding: 3px;
margin-top: 5px;
}
&--opened {
@ -39,9 +32,7 @@
display: flex;
align-content: center;
margin: 0;
padding: 0 10px;
max-width: calc(100% - 70px);
overflow-x: auto;
max-width: calc(100% - 40px);
}
}
@ -50,13 +41,16 @@
position: absolute;
left: calc(var(--toolbox-buttons-size) * -1);
flex-shrink: 0;
&--hidden {
display: none;
}
@media (--mobile){
display: none !important;
display: inline-flex !important;
position: static;
transform: none !important;
}
}
@ -80,6 +74,7 @@
@media (--mobile){
position: static;
margin-left: auto;
padding-right: 10px;
display: flex;
align-items: center;
}

View file

@ -10,7 +10,7 @@
position: static;
transform: none !important;
align-items: center;
visibility: visible !important;
overflow-x: auto;
}
&--opened {
@ -20,6 +20,7 @@
&__button {
@apply --toolbox-button;
flex-shrink: 0;
}
&__tooltip {
@ -27,7 +28,6 @@
top: 25px;
padding: 6px 10px;
border-radius: 5px;
line-height: 21px;
opacity: 0;
background: var(--bg-light);
box-shadow: 0 10px 12px -9px rgba(26, 39, 54, 0.32), 0 3px 2px -2px rgba(33, 48, 73, 0.05);
@ -41,6 +41,10 @@
letter-spacing: 0.02em;
line-height: 1em;
@media (--mobile) {
display: none;
}
&-shortcut {
color: rgba(100, 105, 122, 0.6);
word-spacing: -2px;

View file

@ -60,6 +60,10 @@
border-radius: 4px;
z-index: 2;
@media (--mobile){
box-shadow: 0 5px 9px -5px rgba(21, 40, 54, 0.49),6px 15px 34px -6px rgba(33, 48, 73, 0.54);
}
&::before {
content: '';
width: 15px;
@ -72,6 +76,21 @@
background-color: #fff;
z-index: -1;
}
&--left-oriented {
&::before {
left: 15px;
margin-left: 0;
}
}
&--right-oriented {
&::before {
left: auto;
right: 15px;
margin-left: 0;
}
}
};
/**
@ -106,6 +125,7 @@
width: 34px;
height: 34px;
line-height: 34px;
padding: 0 !important;
text-align: center;
border-radius: 3px;
cursor: pointer;

View file

@ -31,6 +31,8 @@ import Saver from '../components/modules/saver';
import BlockSelection from '../components/modules/blockSelection';
import RectangleSelection from '../components/modules/RectangleSelection';
import InlineToolbarAPI from '../components/modules/api/inlineToolbar';
import CrossBlockSelection from '../components/modules/crossBlockSelection';
import ConversionToolbar from '../components/modules/toolbar/conversion';
export interface EditorModules {
UI: UI;
@ -42,6 +44,7 @@ export interface EditorModules {
InlineToolbar: InlineToolbar;
Toolbox: Toolbox;
BlockSettings: BlockSettings;
ConversionToolbar: ConversionToolbar;
Events: Events;
Shortcuts: Shortcuts;
Paste: Paste;
@ -65,5 +68,6 @@ export interface EditorModules {
StylesAPI: StylesAPI;
ToolbarAPI: ToolbarAPI;
InlineToolbarAPI: InlineToolbarAPI;
CrossBlockSelection: CrossBlockSelection;
NotifierAPI: NotifierAPI;
}

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

@ -1,4 +1,5 @@
import {OutputData} from '../data-formats/output-data';
import {BlockToolData, ToolConfig} from "../tools";
/**
* Describes methods to manipulate with Editor`s blocks
@ -63,6 +64,26 @@ export interface Blocks {
/**
* Insert new Initial Block after current Block
*
* @deprecated
*/
insertNewBlock(): void;
/**
* Insert new Block
*
* @param {string} type Tool name
* @param {BlockToolData} data Tool data to insert
* @param {ToolConfig} config Tool config
* @param {number?} index index where to insert new Block
* @param {boolean?} needToFocus - flag to focus inserted Block
*/
insert(
type?: string,
data?: BlockToolData,
config?: ToolConfig,
index?: number,
needToFocus?: boolean,
): void;
}

View file

@ -0,0 +1,26 @@
import {BlockToolData} from '../tools';
/**
* Config allows Tool to specify how it can be converted into/from another Tool
*/
export interface ConversionConfig {
/**
* How to import string to this Tool.
*
* Can be a String or Function:
*
* 1. String the key of Tool data object to fill it with imported string on render.
* 2. Function method that accepts importing string and composes Tool data to render.
*/
import: ((data: string) => string) | string;
/**
* How to export this Tool to make other Block.
*
* Can be a String or Function:
*
* 1. String which property of saved Tool data should be used as exported string.
* 2. Function accepts saved Tool data and create a string to export
*/
export: ((data: BlockToolData) => string) | string;
}

View file

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

View file

@ -1,4 +1,4 @@
import {PasteConfig, SanitizerConfig} from '../configs';
import {ConversionConfig, PasteConfig, SanitizerConfig} from '../configs';
import {BlockToolData} from './block-tool-data';
import {BaseTool, BaseToolConstructable} from './tool';
import {ToolConfig} from './tool-config';
@ -67,7 +67,16 @@ export interface BlockToolConstructable extends BaseToolConstructable {
/**
* Paste substitutions configuration
*/
pasteConfig?: PasteConfig;
pasteConfig?: PasteConfig | false;
/**
* Rules that specified how this Tool can be converted into/from another Tool
*/
conversionConfig?: ConversionConfig;
/**
* @constructor
* @return {BlockTool}
*/
new (config: {api: API, config: ToolConfig, data: BlockToolData}): BlockTool;
}

View file

@ -31,11 +31,6 @@ export interface ToolSettings {
*/
config?: ToolConfig;
/**
* Is user available to add line brakes in Tool (for example by Shift+Enter)
*/
enableLineBreaks?: boolean;
/**
* Is need to show Inline Toolbar.
* Can accept array of Tools for InlineToolbar or boolean.

990
yarn.lock

File diff suppressed because it is too large Load diff