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:
George Berezhnoy 2018-11-29 12:16:06 +03:00 committed by GitHub
parent 4c9aa0fbd5
commit 669c11eaa5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 385 additions and 10691 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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.

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}

View file

@ -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

View file

@ -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

View file

@ -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] = [];
}

View file

@ -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;
}
}

View file

@ -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
*/

View file

@ -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

View file

@ -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"]
}
}

View file

@ -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
View file

@ -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';

View file

@ -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;
}

View file

@ -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
View 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;

View file

@ -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"