[Release] Version 2.15 (#826)
|
@ -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
12
dist/sprite.svg
vendored
|
@ -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 |
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
122
docs/tools.md
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export default class InlineToolbarAPI extends Module {
|
|||
* Open Inline Toolbar
|
||||
*/
|
||||
public open(): void {
|
||||
this.Editor.InlineToolbar.open();
|
||||
this.Editor.InlineToolbar.tryToShow();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
187
src/components/modules/crossBlockSelection.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) : [];
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()] = {};
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
308
src/components/modules/toolbar/conversion.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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)`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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} = {};
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
54
src/styles/conversion-toolbar.css
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
4
src/types-internal/editor-modules.d.ts
vendored
|
@ -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
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
26
types/configs/conversion-config.ts
Normal 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;
|
||||
}
|
1
types/configs/index.d.ts
vendored
|
@ -1,3 +1,4 @@
|
|||
export * from './editor-config';
|
||||
export * from './sanitizer-config';
|
||||
export * from './paste-config';
|
||||
export * from './conversion-config';
|
||||
|
|
13
types/tools/block-tool.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
5
types/tools/tool-settings.d.ts
vendored
|
@ -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.
|
||||
|
|