mirror of
https://github.com/codex-team/editor.js
synced 2024-06-10 09:52:36 +02:00
Paste handling improvements (#534)
* Make on paste callback non-static method * Add docs * change tools.md header levels * some docs improvements * upd docs * Types improvements * add image tool for testing * Fix file drag'n'drop * improve log on paste * Update submodules * Update bundle * Update paragraph submodule * Fix some bugs with blocks replacement Remove tag from HTMLPasteEvent * Use production webpack mode * minimize: true * Update docs * Update submodules * Update bundle
This commit is contained in:
parent
4c9aa0fbd5
commit
669c11eaa5
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
133
docs/tools.md
133
docs/tools.md
|
@ -47,7 +47,7 @@ Method that specifies how to merge two `Blocks` of the same type, for example on
|
|||
Method does accept data object in same format as the `Render` and it should provide logic how to combine new
|
||||
data with the currently stored value.
|
||||
|
||||
### Internal Tool Settings
|
||||
## Internal Tool Settings
|
||||
|
||||
Options that Tool can specify. All settings should be passed as static properties of Tool's class.
|
||||
|
||||
|
@ -58,7 +58,7 @@ Options that Tool can specify. All settings should be passed as static propertie
|
|||
| `enableLineBreaks` | _Boolean_ | `false` | With this option, CodeX Editor 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) |
|
||||
|
||||
### User configuration
|
||||
## User configuration
|
||||
|
||||
All Tools can be configured by users. You can set up some of available settings along with Tool's class
|
||||
to the `tools` property of Editor Config.
|
||||
|
@ -85,58 +85,58 @@ There are few options available by CodeX Editor.
|
|||
| `inlineToolbar` | _Boolean/Array_ | `false` | Pass `true` to enable the Inline Toolbar with all Tools, or pass an array with specified Tools list |
|
||||
| `config` | _Object_ | `null` | User's configuration for Plugin.
|
||||
|
||||
### Paste handling
|
||||
## Paste handling
|
||||
|
||||
CodeX Editor handles paste on Blocks and provides API for Tools to process the pasted data.
|
||||
|
||||
When user pastes content into Editor, pasted content is splitted into blocks.
|
||||
When user pastes content into Editor, pasted content will be splitted into blocks.
|
||||
|
||||
1. If plain text has been pasted, it is split by new line characters
|
||||
2. If HTML string has been pasted, it is split by block tags
|
||||
1. If plain text will be pasted, it will be splitted by new line characters
|
||||
2. If HTML string will be pasted, it will be splitted by block tags
|
||||
|
||||
Also Editor API allows you to define RegExp patterns to substitute them by your data.
|
||||
Also Editor API allows you to define your own pasting scenario. You can either:
|
||||
|
||||
To provide paste handling for your Tool you need to define static getter `onPaste` in Tool class.
|
||||
`onPaste` getter should return object with fields described below.
|
||||
1. Specify **HTML tags**, that can be represented by your Tool. For example, Image Tool can handle `<img>` tags.
|
||||
If tags you specified will be found on content pasting, your Tool will be rendered.
|
||||
2. Specify **RegExp** for pasted strings. If pattern has been matched, your Tool will be rendered.
|
||||
3. Specify **MIME type** or **extensions** of files that can be handled by your Tool on pasting by drag-n-drop or from clipboard.
|
||||
|
||||
For each scenario, you should do 2 next things:
|
||||
|
||||
##### HTML tags handling
|
||||
1. Define static getter `pasteConfig` in Tool class. Specify handled patterns there.
|
||||
2. Define public method `onPaste` that will handle PasteEvent to process pasted data.
|
||||
|
||||
To handle pasted HTML elements object returned from `onPaste` getter should contain following fields:
|
||||
### HTML tags handling
|
||||
|
||||
To handle pasted HTML elements object returned from `pasteConfig` getter should contain following field:
|
||||
|
||||
| Name | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| `handler(content: HTMLElement)` | `Function` | _Optional_. Pasted HTML elements handler. Gets one argument `content`. `content` is HTML element extracted from pasted data. Handler should return the same object as Tool's `save` method |
|
||||
| `tags` | `String[]` | _Optional_. Should contain all tag names you want to be extracted from pasted data and be passed to your `handler` method |
|
||||
| `tags` | `String[]` | _Optional_. Should contain all tag names you want to be extracted from pasted data and processed by your `onPaste` method |
|
||||
|
||||
|
||||
For correct work you MUST provide `onPaste.handler` at least for `initialBlock` Tool.
|
||||
For correct work you MUST provide `onPaste` handler at least for `initialBlock` Tool.
|
||||
|
||||
> Example
|
||||
|
||||
Header tool can handle `H1`-`H6` tags using paste handling API
|
||||
Header Tool can handle `H1`-`H6` tags using paste handling API
|
||||
|
||||
```javascript
|
||||
static get onPaste() {
|
||||
static get pasteConfig() {
|
||||
return {
|
||||
tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'],
|
||||
handler: (element) => ({
|
||||
type: element.tagName,
|
||||
text: element.innerHTML
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> One tag can be handled by one Tool only.
|
||||
> Same tag can be handled by one (first specified) Tool only.
|
||||
|
||||
##### Patterns handling
|
||||
### RegExp patterns handling
|
||||
|
||||
Your Tool can analyze text by RegExp patterns to substitute pasted string with data you want. Object returned from `onPaste` getter should contain following fields to use patterns:
|
||||
Your Tool can analyze text by RegExp patterns to substitute pasted string with data you want. Object returned from `pasteConfig` getter should contain following field to use patterns:
|
||||
|
||||
| Name | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| `patterns` | `Object` | _Optional_. `patterns` object contains RegExp patterns with their names as object's keys |
|
||||
| `patternHandler(text: string, key: string)` | `Function` | _Optional_. Gets pasted string and pattern name. Should return the same object as Tool `save` method |
|
||||
|
||||
**Note** Editor will check pattern's full match, so don't forget to handle all available chars in there.
|
||||
|
||||
|
@ -144,70 +144,91 @@ Pattern will be processed only if paste was on `initialBlock` Tool and pasted st
|
|||
|
||||
> Example
|
||||
|
||||
You can handle youtube links and insert embeded video instead:
|
||||
You can handle YouTube links and insert embeded video instead:
|
||||
|
||||
```javascript
|
||||
static get onPaste() {
|
||||
static get pasteConfig() {
|
||||
return {
|
||||
patterns: {
|
||||
youtube: /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?/
|
||||
},
|
||||
patternHandler: (text, key) => {
|
||||
const urlData = Youtube.onPaste.patterns[key].exec(text);
|
||||
|
||||
return {
|
||||
iframe: Youtube.makeEmbededFromURL(urlData)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Both `onPaste.handler` and `onPaste.patternHandler` can be `async` or return a `Promise`.
|
||||
|
||||
##### Files
|
||||
### Files pasting
|
||||
|
||||
Your Tool can handle files pasted or dropped into the Editor.
|
||||
|
||||
To handle file you should provide `files` and `fileHandler` properties in your `onPaste` configuration object.
|
||||
To handle file you should provide `files` property in your `pasteConfig` configuration object.
|
||||
|
||||
`fileHandler` property should be a function which takes File object as an argument and returns the same object as Tool\`s `save` method.
|
||||
|
||||
`file` property is an object with the following fields:
|
||||
`files` property is an object with the following fields:
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| `extensions` | `string[]` | _Optional_ Array of extensions your Tool can handle |
|
||||
| `mimeTypes` | `sring[]` | _Optional_ Array of MIME types your Tool can handle |
|
||||
|
||||
|
||||
|
||||
Example
|
||||
|
||||
```javascript
|
||||
static get onPaste() {
|
||||
static get pasteConfig() {
|
||||
return {
|
||||
files: {
|
||||
mimeTypes: ['image/png'],
|
||||
extensions: ['json']
|
||||
},
|
||||
fileHandler: (file) => {
|
||||
/* do smth with the file */
|
||||
|
||||
return {
|
||||
data // Some extracted content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sanitize
|
||||
### Pasted data handling
|
||||
|
||||
If you registered some paste substitutions in `pasteConfig` property, you **should** provide `onPaste` callback in your Tool class.
|
||||
`onPaste` should be public non-static method. It accepts custom _PasteEvent_ object as argument.
|
||||
|
||||
PasteEvent is an alias for three types of events - `tag`, `pattern` and `file`. You can get the type from _PasteEvent_ object's `type` property.
|
||||
Each of these events provide `detail` property with info about pasted content.
|
||||
|
||||
| Type | Detail |
|
||||
| ----- | ------ |
|
||||
| `tag` | `data` - pasted HTML element |
|
||||
| `pattern` | `key` - matched pattern key you specified in `pasteConfig` object <br /> `data` - pasted string |
|
||||
| `file` | `file` - pasted file |
|
||||
|
||||
Example
|
||||
|
||||
```javascript
|
||||
onPaste (event) {
|
||||
switch (event.type) {
|
||||
case 'tag':
|
||||
const element = event.detail.data;
|
||||
|
||||
this.handleHTMLPaste(element);
|
||||
break;
|
||||
|
||||
case 'pattern':
|
||||
const text = event.detail.data;
|
||||
const key = event.detail.key;
|
||||
|
||||
this.handlePatternPaste(key, text);
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
const file = event.detail.file;
|
||||
|
||||
this.handleFilePaste(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sanitize
|
||||
|
||||
CodeX Editor provides [API](sanitizer.md) to clean taint strings.
|
||||
Use it manually at the `save()` method or or pass `sanitizer` config to do it automatically.
|
||||
|
||||
#### Sanitizer Configuration
|
||||
### Sanitizer Configuration
|
||||
|
||||
The example of sanitizer configuration
|
||||
|
||||
|
@ -220,7 +241,7 @@ let sanitizerConfig = {
|
|||
|
||||
Keys of config object is tags and the values is a rules.
|
||||
|
||||
##### Rule
|
||||
#### Rule
|
||||
|
||||
Rule can be boolean, object or function. Object is a dictionary of rules for tag's attributes.
|
||||
|
||||
|
@ -262,7 +283,7 @@ a: function(el) {
|
|||
}
|
||||
```
|
||||
|
||||
#### Manual sanitize
|
||||
### Manual sanitize
|
||||
|
||||
Call API method `sanitizer.clean()` at the save method for each field in returned data.
|
||||
|
||||
|
@ -274,7 +295,7 @@ save() {
|
|||
}
|
||||
```
|
||||
|
||||
#### Automatic sanitize
|
||||
### Automatic sanitize
|
||||
|
||||
If you pass the sanitizer config as static getter, CodeX Editor will automatically sanitize your saved data.
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<pre class="ce-example__output-content" id="output"></pre>
|
||||
|
||||
<div class="ce-example__output-footer">
|
||||
<a href="https://ifmo.su" style="font-weight: bold">Made by CodeX</a>
|
||||
<a href="https://ifmo.su" style="font-weight: bold;">Made by CodeX</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,6 +42,7 @@
|
|||
https://github.com/codex-editor/header#installation
|
||||
-->
|
||||
<script src="./tools/header/dist/bundle.js"></script><!-- Header -->
|
||||
<script src="./tools/simple-image/dist/bundle.js"></script><!-- Image -->
|
||||
<script src="./tools/image/dist/bundle.js"></script><!-- Image -->
|
||||
<script src="./tools/delimiter/dist/bundle.js"></script><!-- Delimiter -->
|
||||
<script src="./tools/list/dist/bundle.js"></script><!-- List -->
|
||||
|
@ -94,7 +95,10 @@
|
|||
image: {
|
||||
class: ImageTool,
|
||||
config: {
|
||||
url: 'http://localhost:8008',
|
||||
endpoints: {
|
||||
byFile: 'http://localhost:8008/uploadFile',
|
||||
byUrl: 'http://localhost:8008/fetchUrl',
|
||||
},
|
||||
},
|
||||
inlineToolbar: ['link'],
|
||||
},
|
||||
|
@ -232,7 +236,7 @@
|
|||
type: 'image',
|
||||
data: {
|
||||
file: {
|
||||
url : 'https://ifmo.su/upload/redactor_images/o_e48549d1855c7fc1807308dd14990126.jpg',
|
||||
url: 'https://ifmo.su/upload/redactor_images/o_e48549d1855c7fc1807308dd14990126.jpg',
|
||||
},
|
||||
caption: '',
|
||||
stretched: false,
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit d0874d51fabb8f8881926306038079457f0db114
|
||||
Subproject commit 860d3046d0483046b9389d27e3a9c1ab51cf6b87
|
|
@ -1 +1 @@
|
|||
Subproject commit f64378a2f18ee69c66860a3e45d1e392417a4ca7
|
||||
Subproject commit af3d6545056ef07498363c9b160ad3e0df15bb0f
|
|
@ -1 +1 @@
|
|||
Subproject commit da319d4757f1909d049f0f205be62ac08ef377f5
|
||||
Subproject commit a25681245f0fdbee1b4a01108e8384bff363a80f
|
|
@ -1 +1 @@
|
|||
Subproject commit 24a5fe205d55ab481233e60a8263f1433c316852
|
||||
Subproject commit e45da06890c453cb79c8aa88cf657ad988fcc1ca
|
|
@ -1 +1 @@
|
|||
Subproject commit f14f258b3d993e3b58db76f668d956134fcd813e
|
||||
Subproject commit c6b832e5e4801f531011923a6c7340fb9a0067fa
|
|
@ -1 +1 @@
|
|||
Subproject commit 7642bb2b541e417307281849d24bc0cce6ff25e2
|
||||
Subproject commit 99c37eb07f9ed93551dd8ca2678f2ff740c6a15f
|
|
@ -1 +1 @@
|
|||
Subproject commit e970963af843ac1b6131503545f0581325b33f37
|
||||
Subproject commit d026d7d36f1b20e24ea7990b4f629b5b3abb8791
|
|
@ -1 +1 @@
|
|||
Subproject commit cfde1bc77e32ca884756f11832da282ba73b16b2
|
||||
Subproject commit 169bff33ddec03396f9b193de11b2adf03df7511
|
10378
package-lock.json
generated
10378
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "codex.editor",
|
||||
"version": "2.5.6",
|
||||
"version": "2.6.0",
|
||||
"description": "Codex Editor. Native JS, based on API and Open Source",
|
||||
"main": "build/codex-editor.js",
|
||||
"types": "./types/index.d.ts",
|
||||
|
@ -57,7 +57,7 @@
|
|||
"ts-loader": "^5.3.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-loader": "^3.6.0",
|
||||
"typescript": "^2.9.2",
|
||||
"typescript": "^3.1.6",
|
||||
"webpack": "4.20.2",
|
||||
"webpack-cli": "^3.1.0"
|
||||
}
|
||||
|
|
|
@ -191,7 +191,7 @@ export default class Dom {
|
|||
sibling = atLast ? 'previousSibling' : 'nextSibling';
|
||||
|
||||
if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) {
|
||||
let nodeChild = node[child];
|
||||
let nodeChild = node[child] as Node;
|
||||
|
||||
/**
|
||||
* special case when child is single tag that can't contain any content
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
import Block from '../block';
|
||||
import Module from '../__module';
|
||||
import $ from '../dom';
|
||||
import _ from '../utils';
|
||||
import Blocks from '../blocks';
|
||||
import {BlockTool, BlockToolConstructable, BlockToolData, ToolConfig} from '../../../types';
|
||||
import {BlockTool, BlockToolConstructable, BlockToolData, PasteEvent, ToolConfig} from '../../../types';
|
||||
import Caret from './caret';
|
||||
|
||||
/**
|
||||
|
@ -149,7 +150,7 @@ export default class BlockManager extends Module {
|
|||
*
|
||||
* @return {Block}
|
||||
*/
|
||||
public composeBlock(toolName: string, data: BlockToolData, settings?: ToolConfig): Block {
|
||||
public composeBlock(toolName: string, data: BlockToolData = {}, settings: ToolConfig = {}): Block {
|
||||
const toolInstance = this.Editor.Tools.construct(toolName, data) as BlockTool;
|
||||
const toolClass = this.Editor.Tools.available[toolName] as BlockToolConstructable;
|
||||
const block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API.methods);
|
||||
|
@ -182,6 +183,34 @@ export default class BlockManager extends Module {
|
|||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert pasted content. Call onPaste callback after insert.
|
||||
*
|
||||
* @param {string} toolName
|
||||
* @param {PasteEvent} pasteEvent - pasted data
|
||||
* @param {boolean} replace - should replace current block
|
||||
*/
|
||||
public paste(
|
||||
toolName: string,
|
||||
pasteEvent: PasteEvent,
|
||||
replace: boolean = false,
|
||||
): Block {
|
||||
let block;
|
||||
|
||||
if (replace) {
|
||||
block = this.replace(toolName);
|
||||
} else {
|
||||
block = this.insert(toolName);
|
||||
}
|
||||
|
||||
try {
|
||||
block.call('onPaste', pasteEvent);
|
||||
} catch (e) {
|
||||
_.log(`${toolName}: onPaste callback call is failed`, 'error', e);
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always inserts at the end
|
||||
* @return {Block}
|
||||
|
@ -266,7 +295,7 @@ export default class BlockManager extends Module {
|
|||
const extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition();
|
||||
const wrapper = $.make('div');
|
||||
|
||||
wrapper.append(extractedFragment);
|
||||
wrapper.append(extractedFragment as DocumentFragment);
|
||||
|
||||
/**
|
||||
* @todo make object in accordance with Tool
|
||||
|
|
|
@ -19,7 +19,7 @@ export default class Events extends Module {
|
|||
* Object with events` names as key and array of callback functions as value
|
||||
* @type {{}}
|
||||
*/
|
||||
private subscribers: {[name: string]: Array<(data?: any) => void>} = {};
|
||||
private subscribers: {[name: string]: Array<(data?: any) => any>} = {};
|
||||
|
||||
/**
|
||||
* Subscribe any event on callback
|
||||
|
@ -27,7 +27,7 @@ export default class Events extends Module {
|
|||
* @param {String} eventName - event name
|
||||
* @param {Function} callback - subscriber
|
||||
*/
|
||||
public on(eventName: string, callback: (data: any) => void) {
|
||||
public on(eventName: string, callback: (data: any) => any) {
|
||||
if (!(eventName in this.subscribers)) {
|
||||
this.subscribers[eventName] = [];
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import SelectionUtils from '../selection';
|
|||
import Module from '../__module';
|
||||
import $ from '../dom';
|
||||
import _ from '../utils';
|
||||
import {BlockToolData, PasteConfig} from '../../../types';
|
||||
import {BlockTool, BlockToolConstructable, PasteConfig, PasteEvent, PasteEventDetail} from '../../../types';
|
||||
|
||||
/**
|
||||
* Tag substitute object.
|
||||
|
@ -15,14 +15,6 @@ interface TagSubstitute {
|
|||
* @type {string}
|
||||
*/
|
||||
tool: string;
|
||||
|
||||
/**
|
||||
* Callback to handle pasted element
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @return {BlockToolData}
|
||||
*/
|
||||
handler: (element: HTMLElement) => BlockToolData;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,15 +33,6 @@ interface PatternSubstitute {
|
|||
*/
|
||||
pattern: RegExp;
|
||||
|
||||
/**
|
||||
* Callback to handle pasted pattern
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {string} key
|
||||
* @return {BlockToolData}
|
||||
*/
|
||||
handler: (text: string, key: string) => BlockToolData;
|
||||
|
||||
/**
|
||||
* Name of related Tool
|
||||
* @type {string}
|
||||
|
@ -72,14 +55,6 @@ interface FilesSubstitution {
|
|||
* @type {string[]}
|
||||
*/
|
||||
mimeTypes: string[];
|
||||
|
||||
/**
|
||||
* Callback to handle pasted File
|
||||
*
|
||||
* @param {File} file
|
||||
* @return {BlockToolData}
|
||||
*/
|
||||
handler: (file: File) => BlockToolData;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,20 +73,16 @@ interface PasteData {
|
|||
*/
|
||||
content: HTMLElement;
|
||||
|
||||
/**
|
||||
* Pasted data
|
||||
*/
|
||||
event: PasteEvent;
|
||||
|
||||
/**
|
||||
* True if content should be inserted as new Block
|
||||
* @type {boolean}
|
||||
*/
|
||||
isBlock: boolean;
|
||||
|
||||
/**
|
||||
* Callback that returns pasted data in BlockToolData format
|
||||
*
|
||||
* @param {HTMLElement | string} content
|
||||
* @param {RegExp} patten
|
||||
* @return {BlockToolData}
|
||||
*/
|
||||
handler: (content: HTMLElement|string, patten?: RegExp) => BlockToolData;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -165,7 +136,7 @@ export default class Paste extends Module {
|
|||
const { Sanitizer } = this.Editor;
|
||||
|
||||
if (dataTransfer.types.includes('Files')) {
|
||||
await this.processFiles(dataTransfer.items);
|
||||
await this.processFiles(dataTransfer.files);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -222,9 +193,19 @@ export default class Paste extends Module {
|
|||
* @param {string} name
|
||||
* @param {Tool} tool
|
||||
*/
|
||||
private processTool = ([name, tool]) => {
|
||||
private processTool = ([name, tool]: [string, BlockToolConstructable]): void => {
|
||||
try {
|
||||
const toolPasteConfig = tool.onPaste || {};
|
||||
const toolInstance = new this.Editor.Tools.blockTools[name]({
|
||||
api: this.Editor.API.methods,
|
||||
config: {},
|
||||
data: {},
|
||||
}) as BlockTool;
|
||||
|
||||
if (!toolInstance.onPaste || typeof toolInstance.onPaste !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolPasteConfig = tool.pasteConfig || {};
|
||||
|
||||
this.getTagsConfig(name, toolPasteConfig);
|
||||
this.getFilesConfig(name, toolPasteConfig);
|
||||
|
@ -245,26 +226,6 @@ export default class Paste extends Module {
|
|||
* @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
|
||||
*/
|
||||
private getTagsConfig(name: string, toolPasteConfig: PasteConfig): void {
|
||||
if (this.config.initialBlock === name && !toolPasteConfig.handler) {
|
||||
_.log(
|
||||
`«${name}» Tool must provide a paste handler.`,
|
||||
'warn',
|
||||
);
|
||||
}
|
||||
|
||||
if (!toolPasteConfig.handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof toolPasteConfig.handler !== 'function') {
|
||||
_.log(
|
||||
`Paste handler for «${name}» Tool should be a function.`,
|
||||
'warn',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = toolPasteConfig.tags || [];
|
||||
|
||||
tags.forEach((tag) => {
|
||||
|
@ -278,7 +239,6 @@ export default class Paste extends Module {
|
|||
}
|
||||
|
||||
this.toolsTags[tag.toUpperCase()] = {
|
||||
handler: toolPasteConfig.handler,
|
||||
tool: name,
|
||||
};
|
||||
});
|
||||
|
@ -294,15 +254,10 @@ export default class Paste extends Module {
|
|||
*/
|
||||
private getFilesConfig(name: string, toolPasteConfig: PasteConfig): void {
|
||||
|
||||
const {fileHandler, files = {}} = toolPasteConfig;
|
||||
const {files = {}} = toolPasteConfig;
|
||||
let {extensions, mimeTypes} = files;
|
||||
|
||||
if (!fileHandler || (!extensions && !mimeTypes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof fileHandler !== 'function') {
|
||||
_.log(`Drop handler for «${name}» Tool should be a function.`);
|
||||
if (!extensions && !mimeTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -330,7 +285,6 @@ export default class Paste extends Module {
|
|||
this.toolsFiles[name] = {
|
||||
extensions: extensions || [],
|
||||
mimeTypes: mimeTypes || [],
|
||||
handler: fileHandler,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -341,16 +295,7 @@ export default class Paste extends Module {
|
|||
* @param {PasteConfig} toolPasteConfig - Tool onPaste configuration
|
||||
*/
|
||||
private getPatternsConfig(name: string, toolPasteConfig: PasteConfig): void {
|
||||
if (!toolPasteConfig.patternHandler || _.isEmpty(toolPasteConfig.patterns)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof toolPasteConfig.patternHandler !== 'function') {
|
||||
_.log(
|
||||
`Pattern parser for «${name}» Tool should be a function.`,
|
||||
'warn',
|
||||
);
|
||||
|
||||
if (!toolPasteConfig.patterns || _.isEmpty(toolPasteConfig.patterns)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -366,7 +311,6 @@ export default class Paste extends Module {
|
|||
this.toolsPatterns.push({
|
||||
key,
|
||||
pattern,
|
||||
handler: toolPasteConfig.patternHandler,
|
||||
tool: name,
|
||||
});
|
||||
});
|
||||
|
@ -410,12 +354,12 @@ export default class Paste extends Module {
|
|||
/**
|
||||
* Get files from data transfer object and insert related Tools
|
||||
*
|
||||
* @param {DataTransferItemList} items - pasted or dropped items
|
||||
* @param {FileList} items - pasted or dropped items
|
||||
*/
|
||||
private async processFiles(items: DataTransferItemList) {
|
||||
private async processFiles(items: FileList) {
|
||||
const {BlockManager} = this.Editor;
|
||||
|
||||
let dataToInsert: Array<{type: string, data: BlockToolData}>;
|
||||
let dataToInsert: Array<{type: string, event: PasteEvent}>;
|
||||
|
||||
dataToInsert = await Promise.all(
|
||||
Array
|
||||
|
@ -427,11 +371,11 @@ export default class Paste extends Module {
|
|||
dataToInsert.forEach(
|
||||
(data, i) => {
|
||||
if (i === 0 && BlockManager.currentBlock && BlockManager.currentBlock.isEmpty) {
|
||||
BlockManager.replace(data.type, data.data);
|
||||
BlockManager.paste(data.type, data.event, true);
|
||||
return;
|
||||
}
|
||||
|
||||
BlockManager.insert(data.type, data.data);
|
||||
BlockManager.paste(data.type, data.event);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -439,14 +383,9 @@ export default class Paste extends Module {
|
|||
/**
|
||||
* Get information about file and find Tool to handle it
|
||||
*
|
||||
* @param {DataTransferItem} item
|
||||
* @param {File} file
|
||||
*/
|
||||
private async processFile(item: DataTransferItem) {
|
||||
if (item.kind === 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = item.getAsFile();
|
||||
private async processFile(file: File) {
|
||||
const extension = _.getFileExtension(file);
|
||||
|
||||
const foundConfig = Object
|
||||
|
@ -468,9 +407,13 @@ export default class Paste extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
const [tool, {handler}] = foundConfig;
|
||||
const [tool] = foundConfig;
|
||||
const pasteEvent = this.composePasteEvent('file', {
|
||||
file,
|
||||
});
|
||||
|
||||
return {
|
||||
data: await handler(file),
|
||||
event: pasteEvent,
|
||||
type: tool,
|
||||
};
|
||||
}
|
||||
|
@ -482,7 +425,7 @@ export default class Paste extends Module {
|
|||
* @param {boolean} isHTML - if passed string is HTML, this parameter should be true
|
||||
*/
|
||||
private async processText(data: string, isHTML: boolean = false) {
|
||||
const {Caret, BlockManager} = this.Editor;
|
||||
const {Caret, BlockManager, Tools} = this.Editor;
|
||||
const dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data);
|
||||
|
||||
if (!dataToInsert.length) {
|
||||
|
@ -494,16 +437,11 @@ export default class Paste extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If caret not at the end of of the Block and there is no selection,
|
||||
* we split the Block and insert content at the middle.
|
||||
*/
|
||||
if (SelectionUtils.isAtEditor && !Caret.isAtEnd && SelectionUtils.isCollapsed) {
|
||||
this.splitBlock();
|
||||
}
|
||||
const isCurrentBlockInitial = Tools.isInitial(BlockManager.currentBlock.tool);
|
||||
const needToReplaceCurrentBlock = isCurrentBlockInitial && BlockManager.currentBlock.isEmpty;
|
||||
|
||||
await Promise.all(dataToInsert.map(
|
||||
async (content, i) => await this.insertBlock(content, i === 0),
|
||||
async (content, i) => await this.insertBlock(content, i === 0 && needToReplaceCurrentBlock),
|
||||
));
|
||||
|
||||
Caret.setToBlock(BlockManager.currentBlock, CaretClass.positions.END);
|
||||
|
@ -516,9 +454,9 @@ export default class Paste extends Module {
|
|||
* @returns {PasteData[]}
|
||||
*/
|
||||
private processHTML(innerHTML: string): PasteData[] {
|
||||
const {Tools, Sanitizer} = this.Editor,
|
||||
initialTool = this.config.initialBlock,
|
||||
wrapper = $.make('DIV');
|
||||
const {Tools, Sanitizer} = this.Editor;
|
||||
const initialTool = this.config.initialBlock;
|
||||
const wrapper = $.make('DIV');
|
||||
|
||||
wrapper.innerHTML = innerHTML;
|
||||
|
||||
|
@ -546,7 +484,7 @@ export default class Paste extends Module {
|
|||
break;
|
||||
}
|
||||
|
||||
const {handler, tags} = Tools.blockTools[tool].onPaste;
|
||||
const {tags} = Tools.blockTools[tool].pasteConfig;
|
||||
|
||||
const toolTags = tags.reduce((result, tag) => {
|
||||
result[tag.toLowerCase()] = {};
|
||||
|
@ -557,7 +495,11 @@ export default class Paste extends Module {
|
|||
|
||||
content.innerHTML = Sanitizer.clean(content.innerHTML, customConfig);
|
||||
|
||||
return {content, isBlock, handler, tool};
|
||||
const event = this.composePasteEvent('tag', {
|
||||
data: content,
|
||||
});
|
||||
|
||||
return {content, isBlock, tool, event};
|
||||
})
|
||||
.filter((data) => !$.isNodeEmpty(data.content) || $.isSingleTag(data.content));
|
||||
}
|
||||
|
@ -576,8 +518,7 @@ export default class Paste extends Module {
|
|||
return [];
|
||||
}
|
||||
|
||||
const tool = initialBlock,
|
||||
handler = Tools.blockTools[tool].onPaste.handler;
|
||||
const tool = initialBlock;
|
||||
|
||||
return plain
|
||||
.split(/\r?\n/)
|
||||
|
@ -587,7 +528,11 @@ export default class Paste extends Module {
|
|||
|
||||
content.innerHTML = text;
|
||||
|
||||
return {content, tool, isBlock: false, handler};
|
||||
const event = this.composePasteEvent('tag', {
|
||||
data: content,
|
||||
});
|
||||
|
||||
return {content, tool, isBlock: false, event};
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -601,27 +546,21 @@ export default class Paste extends Module {
|
|||
*/
|
||||
private async processSingleBlock(dataToInsert: PasteData): Promise<void> {
|
||||
const initialTool = this.config.initialBlock,
|
||||
{BlockManager, Caret, Sanitizer} = this.Editor,
|
||||
{BlockManager, Caret, Sanitizer, Tools} = this.Editor,
|
||||
{content, tool} = dataToInsert;
|
||||
|
||||
if (tool === initialTool && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {
|
||||
const blockData = await this.processPattern(content.textContent);
|
||||
|
||||
if (blockData) {
|
||||
this.splitBlock();
|
||||
let insertedBlock;
|
||||
|
||||
const sanitizeConfig = Sanitizer.composeToolConfig(tool);
|
||||
const needToReplaceCurrentBlock = BlockManager.currentBlock
|
||||
&& Tools.isInitial(BlockManager.currentBlock.tool)
|
||||
&& BlockManager.currentBlock.isEmpty;
|
||||
|
||||
if (!_.isEmpty(sanitizeConfig)) {
|
||||
blockData.data = Sanitizer.deepSanitize(blockData.data, sanitizeConfig);
|
||||
}
|
||||
insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock);
|
||||
|
||||
if (BlockManager.currentBlock && BlockManager.currentBlock.isEmpty) {
|
||||
insertedBlock = BlockManager.replace(blockData.tool, blockData.data);
|
||||
} else {
|
||||
insertedBlock = BlockManager.insert(blockData.tool, blockData.data);
|
||||
}
|
||||
Caret.setToBlock(insertedBlock, CaretClass.positions.END);
|
||||
return;
|
||||
}
|
||||
|
@ -639,7 +578,7 @@ export default class Paste extends Module {
|
|||
* @param {string} text
|
||||
* @returns Promise<{data: BlockToolData, tool: string}>
|
||||
*/
|
||||
private async processPattern(text: string): Promise<{data: BlockToolData, tool: string}> {
|
||||
private async processPattern(text: string): Promise<{event: PasteEvent, tool: string}> {
|
||||
const pattern = this.toolsPatterns.find((substitute) => {
|
||||
const execResult = substitute.pattern.exec(text);
|
||||
|
||||
|
@ -650,10 +589,17 @@ export default class Paste extends Module {
|
|||
return text === execResult.shift();
|
||||
});
|
||||
|
||||
const data = pattern && await pattern.handler(text, pattern.key);
|
||||
if (!pattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
return data && {
|
||||
data,
|
||||
const event = this.composePasteEvent('pattern', {
|
||||
key: pattern.key,
|
||||
data: text,
|
||||
});
|
||||
|
||||
return {
|
||||
event,
|
||||
tool: pattern.tool,
|
||||
};
|
||||
}
|
||||
|
@ -665,40 +611,19 @@ export default class Paste extends Module {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async insertBlock(data: PasteData, canReplaceCurrentBlock: boolean = false): Promise<void> {
|
||||
const blockData = await data.handler(data.content),
|
||||
{BlockManager, Caret} = this.Editor,
|
||||
{currentBlock} = BlockManager;
|
||||
const {BlockManager, Caret} = this.Editor;
|
||||
const {currentBlock} = BlockManager;
|
||||
|
||||
if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) {
|
||||
BlockManager.replace(data.tool, blockData);
|
||||
BlockManager.paste(data.tool, data.event, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const block = BlockManager.insert(data.tool, blockData);
|
||||
const block = BlockManager.paste(data.tool, data.event);
|
||||
|
||||
Caret.setToBlock(block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split current block if paste isn't in the end of the block
|
||||
*/
|
||||
private splitBlock() {
|
||||
const {BlockManager, Caret} = this.Editor;
|
||||
|
||||
if (!BlockManager.currentBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** If we paste into middle of the current block:
|
||||
* 1. Split
|
||||
* 2. Navigate to the first part
|
||||
*/
|
||||
if (!BlockManager.currentBlock.isEmpty && !Caret.isAtEnd) {
|
||||
BlockManager.split();
|
||||
BlockManager.currentBlockIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively divide HTML string to two types of nodes:
|
||||
* 1. Block element
|
||||
|
@ -778,4 +703,16 @@ export default class Paste extends Module {
|
|||
|
||||
return children.reduce(reducer, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose paste event with passed type and detail
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {PasteEventDetail} detail
|
||||
*/
|
||||
private composePasteEvent(type: string, detail: PasteEventDetail): PasteEvent {
|
||||
return new CustomEvent(type, {
|
||||
detail,
|
||||
}) as PasteEvent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -298,7 +298,7 @@ export default class Sanitizer extends Module {
|
|||
* At least, if there is no config overrides, that API uses Default configuration
|
||||
*
|
||||
* @uses https://www.npmjs.com/package/html-janitor
|
||||
* @licence https://github.com/guardian/html-janitor/blob/master/LICENSE
|
||||
* @license https://github.com/guardian/html-janitor/blob/master/LICENSE
|
||||
*
|
||||
* @param {SanitizerConfig} config - sanitizer extension
|
||||
*/
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
interface Element {
|
||||
matchesSelector: (selector: string) => boolean;
|
||||
mozMatchesSelector: (selector: string) => boolean;
|
||||
msMatchesSelector: (selector: string) => boolean;
|
||||
oMatchesSelector: (selector: string) => boolean;
|
||||
|
||||
prepend: (nodes: Node|Node[]|any) => void;
|
||||
append: (nodes: Node|Node[]|DocumentFragment|void) => void;
|
||||
prepend: (nodes: Node|Node[]|DocumentFragment) => void;
|
||||
append: (nodes: Node|Node[]|DocumentFragment) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 053e9a4885bca063ee1c571e1dacfc5bbe8fea76
|
||||
Subproject commit bf229afc88e682530c82c8fa12aadc85c8a41c8b
|
|
@ -4,6 +4,6 @@
|
|||
"target": "es2017",
|
||||
"declaration": false,
|
||||
"moduleResolution": "node", // This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime
|
||||
"lib": ["es2017", "dom"]
|
||||
"lib": ["dom", "es2017", "es2018"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"import-sources-order": "any",
|
||||
"named-imports-order": "case-insensitive"
|
||||
}],
|
||||
"no-string-literal": false,
|
||||
"no-empty": false,
|
||||
"no-namespace": false,
|
||||
"variable-name": [true, "allow-leading-underscore", "allow-pascal-case"]
|
||||
|
|
8
types/index.d.ts
vendored
8
types/index.d.ts
vendored
|
@ -20,6 +20,14 @@ export {
|
|||
BlockToolData,
|
||||
ToolSettings,
|
||||
ToolConfig,
|
||||
PasteEvent,
|
||||
PasteEventDetail,
|
||||
PatternPasteEvent,
|
||||
PatternPasteEventDetail,
|
||||
HTMLPasteEvent,
|
||||
HTMLPasteEventDetail,
|
||||
FilePasteEvent,
|
||||
FilePasteEventDetail,
|
||||
} from './tools';
|
||||
export {BlockTune, BlockTuneConstructable} from './block-tunes';
|
||||
export {EditorConfig, SanitizerConfig, PasteConfig} from './configs';
|
||||
|
|
8
types/tools/block-tool.d.ts
vendored
8
types/tools/block-tool.d.ts
vendored
|
@ -3,6 +3,7 @@ import {BlockToolData} from './block-tool-data';
|
|||
import {Tool, ToolConstructable} from './tool';
|
||||
import {ToolConfig} from './tool-config';
|
||||
import {API} from '../index';
|
||||
import {PasteEvent} from './paste-events';
|
||||
/**
|
||||
* Describe Block Tool object
|
||||
* @see {@link docs/tools.md}
|
||||
|
@ -50,6 +51,8 @@ export interface BlockTool extends Tool {
|
|||
* @param {BlockToolData} blockData
|
||||
*/
|
||||
merge?(blockData: BlockToolData): void;
|
||||
|
||||
onPaste?(event: PasteEvent);
|
||||
}
|
||||
|
||||
export interface BlockToolConstructable extends ToolConstructable {
|
||||
|
@ -68,5 +71,10 @@ export interface BlockToolConstructable extends ToolConstructable {
|
|||
*/
|
||||
onPaste?: PasteConfig;
|
||||
|
||||
/**
|
||||
* Paste substitutions configuration
|
||||
*/
|
||||
pasteConfig: PasteConfig;
|
||||
|
||||
new (config: {api: API, config: ToolConfig, data: BlockToolData}): BlockTool;
|
||||
}
|
||||
|
|
1
types/tools/index.d.ts
vendored
1
types/tools/index.d.ts
vendored
|
@ -4,3 +4,4 @@ export * from './inline-tool';
|
|||
export * from './tool';
|
||||
export * from './tool-config';
|
||||
export * from './tool-settings';
|
||||
export * from './paste-events';
|
||||
|
|
52
types/tools/paste-events.d.ts
vendored
Normal file
52
types/tools/paste-events.d.ts
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Event detail for tag substitution on paste
|
||||
*/
|
||||
export interface HTMLPasteEventDetail {
|
||||
/**
|
||||
* Pasted element
|
||||
*/
|
||||
data: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste event for tag substitution
|
||||
*/
|
||||
export interface HTMLPasteEvent extends CustomEvent {
|
||||
readonly detail: HTMLPasteEventDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event detail for file substitution on paste
|
||||
*/
|
||||
export interface FilePasteEventDetail {
|
||||
/**
|
||||
* Pasted file
|
||||
*/
|
||||
file: File;
|
||||
}
|
||||
|
||||
export interface FilePasteEvent extends CustomEvent {
|
||||
readonly detail: FilePasteEventDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event detail for pattern substitution on paste
|
||||
*/
|
||||
export interface PatternPasteEventDetail {
|
||||
/**
|
||||
* Pattern key
|
||||
*/
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* Pasted string
|
||||
*/
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface PatternPasteEvent extends CustomEvent {
|
||||
readonly detail: PatternPasteEventDetail;
|
||||
}
|
||||
|
||||
export type PasteEvent = HTMLPasteEvent | FilePasteEvent | PatternPasteEvent;
|
||||
export type PasteEventDetail = HTMLPasteEventDetail | FilePasteEventDetail | PatternPasteEventDetail;
|
10
yarn.lock
10
yarn.lock
|
@ -3199,7 +3199,7 @@ html-comment-regex@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
|
||||
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
|
||||
|
||||
html-janitor@^2.0.2:
|
||||
html-janitor@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/html-janitor/-/html-janitor-2.0.4.tgz#ae5a115cdf3331cd5501edd7b5471b18ea44cdbb"
|
||||
integrity sha512-92J5h9jNZRk30PMHapjHEJfkrBWKCOy0bq3oW2pBungky6lzYSoboBGPMvxl1XRKB2q+kniQmsLsPbdpY7RM2g==
|
||||
|
@ -6873,10 +6873,10 @@ typedarray@^0.0.6:
|
|||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
typescript@^2.9.2:
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
|
||||
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
|
||||
typescript@^3.1.6:
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68"
|
||||
integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA==
|
||||
|
||||
uglify-es@^3.3.4:
|
||||
version "3.3.9"
|
||||
|
|
Loading…
Reference in a new issue