Drag'n'drop support (#445)

* moving caret initial

* small improvements

* last changes, added docs

* requested changes

* implement getters instead of functions in block cursors

* last requested changes

* caret module docs and last improvements

* update docs

* upgrade request

* update docs

* upd

* todo on delays

* Sanitizer docs

* split func upd

* split blocks update

* up docs

* Listeners Module: initial

* listener module updates

* split is ready

* update

* start to make merge

* upd

* split general upd

split is ready

* ups

* keyboard module update

* BlockManager removed keyboard handler

* commit before merging rewriting2.0

* general upd split

* Documentation upd

* document + listener upd

* upd doc

* documentation upd

* doc upd

* listener upd

* update algh extract fragm

* upd extractRangeContent

* upd dom.js

* keyboard upd (shift + enter & enableLineBreaks)

* upd enter pressed

* keyboard.js upd

* enter pressed upd

* documenation added

* documentation upd

* Toolbar: settings zone added. (#252)

* Toolbar: settings zone added.

* update some comments

* Making a Toolbar

* delete block

* dom improvements and merging blocks

* merge and split improvements

* fix merging

* do not remove block if block contains media

* optimize code

* caret behaviour improved

* up

* up

* merging blocks. Now plugins handles this cases

* mergeable getter

* save

* up

* dom getdeepestnode improvements

* improve getDeepest node method one more time

* upd

* Deal with it

* improve isAtStart

* improve docs

* use smart isAtStart and isAtEnt method in navigateNext/navigatePrevious

* improve docs

* fix bug in $.isEmpty, improve keydown

* fix isAtEnd

* rollback setCaret code duplication

* improve backspace

* Debug tree walker

* fix tree walker

* small caret fix

* queue ordering

* update bundle

* improve first letter checkup

* doc upd

* update current block index setter

* TypeScript support, Webpack 4, Inline Toolbar beginning (#257)

* Create UI

* Support TypeScript Modules

* remove tmp files

* migrate to 2-spaced tabs

* Add TS Linter

* Inline Toolbar moving (#258)

* Inline Toolbar moving

* simplify code

* Check is need to show Inline Toolbar

* remove duplicate from doc

* fix doc

* open/close IT

* Close IT by clicks on Redactor

* @guryn going strange

Co-Authored-By: Taly <vitalik7tv@yandex.ru>

* default settings initial

* add move up button to default tunes area

* need to figure out with assets

* Inline Toolbar Tools base example (#260)

* Inline Toolbar Tools base example

* texts fixed

* imrpove texts

* little fixes

* save

* tunes with interface

* add tool settings

* initial api methods

* api is ready

* started writing docs

* Create svg sprite (#261)

* API

* requested changes

* fix conflicts

* add docs

* doc fixes and interface improvements

* update

* API scopes improved

* Deleting block: Initial

* Delete block with confirmation

* Event subscription&unsubscription

* deletion trigger improvements

* small improvements

* Link Inline Tool (#264)

* Link Inline Tool

* api injected

* text improved

* Clear input on the reselection

* little improvements

* Delete tune fixes

* UI: Block Settings, show Plus after Enter keydown (#265)

* Some UI improvements: icons settigns

* Show plus button after split

* decrease autoprefixer

* rename variable

* Revert "Merge branch 'delete-tune-fixes' into rewriting-version2.0"

This reverts commit 779bf5db9e, reversing
changes made to 972eb87d89.

* Delete Tune improvements

* upd

* upd comments

* actualize API docs

* Allow to connect external Inline Tools (#269)

* Allow to connect external Inline Tools

* unhardcode tool's api settings

* Italic inline tool

* update icon size

* upgrade findParentTag function

* add interface selection

* fix cs

* save marker

* bundles

* add todo

* removing wrapper

* update styles

* market -> term

* add comments

* improve code

* descrease margin

* add text block to example

* add line brakes

* remove space

* fix bugs

* fix bug

* umd as a library target

* background -> background-color

if `background` has only `color` then use `background-color`

* Clear API (#274)

* blockManager.clear

* upd

* api bez ebanoj knopki api

* fix assignment

* insert empty block with clear method

* clear and render methods improved

* open saver.save()

* add comments

* update comments

* fix data returned by editor

* rename plugin name field in data object (#276)

* Text tool refactored (#277)

Now it returns strict data format.

* do not add block if tool is not exist (#278)

* do not add block if tool is not exist

* show warning

* add todo

* update warning message

* put message into variable

* Revert "put message into variable"

This reverts commit c1f63894d7.

* update comment

* Module Keyboard rewrited to BlockEvents (#279)

* Module Keydown rewrited to BlockEvents

* move keyup and mouseup to the Block Events

* Move-up tune (#268)

Co-authored-by: khaydarov <murod.haydarov@gmail.com>

* Move up tune initial
* move up tune initial behaviour
* moving up formula, docs and code cleaned
* do not close the toolbar if moving block up
* move nagivate methods to Caret Module
* navigations returns boolean if caret is set
* code improved
* update comments
* disable tune when block index is zero
* provide API with listener module methods
* caret improvements
* add to docs
* update docs and blocks api methods
* docs changes
* small improvements
* swap instead of moves
* update
* fix swap
* remove unused method
* remove useless return value from setToBlock
* improve caret isAtEnd and isAtStart
* improve swap, fix listeners api@off method

* Eslint --fix for project files (#280)

* Header plugin (#281)

* header initial

* fix styles

* eslint fix

* add appendCallback

* add comments

* update styles

* add svgs

* highlight settings buttons

* do not show text plugin in the toolbar

* remove svg

* Fixing caret behaviour. (#282)

Plugins can change their state so that affect on Block's pluginsContent property which is in memory.

* remove useless code

* fix merge

* "MoveDown" tune (#283)

* move down initial

Swap current block with next block and scroll window screen

* check if block is last

added new method to the blocks API that returns blocks count

* fix comments

* animate tune

* add animation when tune is disabled

* requested changes

* remove unused css

* Fix merge function and rename Block's wrapper (#284)

* Fix merge function and rename wrapper

* update

* renew condition

* update

* upd

* Merging blocks: Restore caret position 🤟🤟💪 (#286)

* Merging blocks: Restore caret position 🤟🤟💪

* requested changes

* update removing shadow caret

* hide toolbar and selection on typing (#289)

closes #288

* Editor Instance config Interface (#285)

* create interface for editor config

* use IEditorConfig

* create some interfaces

* add comments

* editor interface

* updates

* update editor interface (#293)

* При перемещении по стрелочкам убирать выделение блока (#296)

* При перемещении по стрелочкам убирать выделение блока

* Add comments

* update comments

* update comment

* update toolbar design (#301)

* Set caret at the end if clicked outsite the block (#305)

* Set caret at last block or create new block at end

* update comment

* fix comments

* Insert new Block when enter pressed on editor area (#307)

* insert new block when enter pressed on editor zone

* extra conditions. Enter must be handled on editors area

* move at editor condition to the Selection method

* closes can return null

* fixing editor area

* do not create new block

* clean example

* updates due to the requested changes

* Add placeholder to contentEditable elements (#306)

* add placeholder to contentEditable elements

* store selection color in a variable

* add placeholder to header block

* Add placeholder to contenteditable only if attribute data-placeholder exists

* remove tool config

* Close toolbar after block is removed (#314)

* makeSettings -> renderSettings (#315)

* Term: new icon, new style. + margin between settings buttons (#316)

Resolves #309

* Tool's renderToolboxIcon function  (#318)

* create renderToolboxIcon

issue was not done

* remove useless span wrapper

* update linters

* update styles

* process click on svg by closest

* remove commented code

* rename function: svgIcon -> toolboxIcon

* add toolboxIcon to docs

* Paste (#259)

* Paste module

* Rewrite paste logic

* Update due comments

* Docs

* Add all block elements

* Sanitize content on paste

* Remove logs and add header handler

* Add comment to dom.js

* Add comment to tools.js

* Split block if paste not at the end

* Update docs

* Update docs

* pidr

* Take Tool name from config

* Update docs

* Label onPaste handler as private

* Resolve conflict

* Replace current block if it is empty (#320)

* Improve Header line-height (#321)

* Fix typo (#324)

* CodeX Editor 2.0

Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>
Co-authored-by: Petr Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>
Co-authored-by: Taly Guryn <vitalik7tv@yandex.ru>

* Fix typo

* Installation docs (#325)

close #310

* Module Shortcuts (#317)

* import shortcuts

* node modules works

* enable shortcut for inline tools

* check shortcut existance

* enable shortcuts to Block tools

* set caret to block if block replaced

* enable shortcuts on inline-tools

* improve code

* last changes

* update

* fix

* insert returns block so we can set caret

* use Map instead own structure

* disable shortcut if iniline-toolbar disabled

* update code styles

* remove todo

* upd

* update

* remove settings from insert function

* create interface

* code improvements

* use const instead of let

* upd

* Simple Image Tool (#326)

* Simple Image

* fix pattern

* show tunes` state

* update code

* update code

* upd

* Fix toolbox appearance, tools boxed are clickable (#331)

* Remove toolsConfig from Editor's config (#327)

* fix linters

* remove toolsConfig

* update tool's interfaces

* add comments

* bundles

* remove test headers

* restore commented code

* update tool's interface

* toolConfig -> toolSetting

* fix typos

toolSetting -> toolSettings
toolsSettings -> toolSettings
toolClasses -> toolsClasses

* update code comments

* update installation doc

* update docs

* update dev dep packages (#333)

* Check is paste handler a function only if it exists (#328)

* Toolbar with tab (#330)

* toolbar tabs initial

* leaf initial

* save state

* flip back toolbox items

* enter on toolbox item

* update

* requested changes

* new condfition

* update

* improve animation on leaf

* fix shift+tab flip

* up

* update

* updates

* Consecutive blank lines are forbidden

* Correct choosing next toolbox item

* update

* update comment

* Validate editor's config before initing (#341)

* validate editor's config before initing

* update readme

* @

* update comments

* add function _.isClass

* Styles API (#343)

* StylesAPI

* use styles api in plugins

* add inline styles

* List Tool [new] (#344)

* list tool initial

* list class with settings

* make tool reactive

* final List improvements

* reorder

* tmp update

* unhadrcode enter handler

* updates

* enableLineBreaks also checks

* skip empty items

* select LI by CMD+A, fix backspace in the last item

* improve check for emptiness

* Example page improved (#347)

* Update new example

* imrpove example.html

* updates

* improve code

* Header plugin (#348)

* isFunction function

* use header from cdn

* Improve paste behaviour (#346)

* Improve paste behaviour

* Done

* Don't pass empty items

* Update comment

* move public up

* Remove disallow paste option

* Quote Tool (#329)

* Quote Tool

* Add icon

* Upd

* fix ENTER on quote

* Remove useless code

* items -> blocks (#351)

* use SimpleImage from cdn (#355)

* use simpleimage from cdn

* add comments

* fix spaces

* fix comments

* remove comments

* update simple-image script

* Update text on the example.html (#356)

* use Paragraph Tool from CDN (#357)

* use Paragraph Tool from CDN

* add line brakes

* rename block: paragraph -> text

* Remove _callbacks.js (#358)

* Clear unused files (#359)

* TOOLBAR_ICON_CLASS -> TOOLBAR_ICON (#360)

* TOOLBAR_ICON_CLASS -> TOOLBAR_ICON

* remove defaultConfig

* Delimiter tool (#362)

* Delimiter added

* ашч

* use delimiter from cdn

* Enter on editor (#363)

* Enterpress on editor

* use additional property

* check enter on body

* update

* fix toolbar behaviour

* upd

* update bundle

* remove useless ui property

* update comment

* add Element.prepend() function (#365)

* add Element.prepend() function

* allow to pass element of array to prepend() polyfill

* use List Tool from cdn (#366)

* use List Tool from cdn

* add missing </div> in example.html

* Pass "config" from Tool's settings to Tool's constructor (#367)

* pass config from Tool's settings to Tool's constructor

* reorder elements

* add apiSettings.CONFIG property

* use string as a object's key 😔 (#368)

* update placeholder's styles (#369)

* Add shortcuts for internal tools (#370)

* Add shortcuts for internal tools

* upd doc

* remove articles

* ☹️ guryn asked

* use quote from cdn (#371)

* Add cache to the inline tools (#372)

* use Inline Code Tool from cdn (#375)

* Issue 354 inline tools filter (#376)

* Allow to filter inline tools.

* update example

* update header

* fix endless cycle (#378)

* Destructured options for Inline Tools (#379)

* add destructured options for inline-tools

* temporary disable inlineCode

* remove term sources

* Fix toolbar moving after arrow navigation (#380)

* Fix toolbar moving after arrow navigation

Resolves #361

* move before open

* fix error on Enter after block removing

* add example Tools as submodules (#381)

* add destructured options for inline-tools

* temporary disable inlineCode

* remove term sources

* add inline code Tool as a submodule

* update version of inline-code package

* add Tools as submodules

* update Tools and use destructured params object

* Add constructor() docs

* update package Paragraph Tool

* update installation docs

* Input navigation (#339)

* Quote Tool

* Add icon

* Upd

* Initial setup

* Save changes

* Add scroll and fix input focus

* Add comments

* Rebuild bundle

* fix ENTER on quote

* Fix split behaviour

* Fix

* Navigate only to contentful blocks

* add comments

* Fix backspace on last block

* Remove log

* It works

* Resolve comments

* Use constants

* New readme 🦅 (#386)

* New readme 🦅

* upd text

* Issue 374 (#385)

* Fix issue #374

* Set current block index to -1 if there is no blocks left

* Insert new block if first one was removed by default

* Paragraph as a default Tool in editor; Zero-conf (#389)

* git commit -m "Removed submodule Paragraph"

* add paragraph to core

+ zero-config

* update bundle

* update comment

* remove log

* enable minifying (#390)

* Drop current block index only if there is no selection at the Editor (#388)

* Drop current block index only if there is no selection at the Editor

* Set current node if click ended out of editor

* Small backspace behaviour improvement (#391)

* Small backspace behaviour improvement

* fix caret position

* update from base branch

* Update webpack config

* Migrate to Yarn (#393)

* Migrate to yarn

* Update scripts

* Rewrite helpers classes to TypeScript (#396)

* Add docs and isReady promise (#394)

* set default holderId value (#404)

* Destroyer (#392)

* Initial destroy method

* Add destroy method to api docs

* Export isReady promise in CodexEditor constructor

* Add drag'n'drop support

* Add docs

* Small fix

* Continue merging master :|

* Fixes after merge

* Add styles and improve behavior

* Close inline toolbar and remove dragged content

* Some improvements of paste data processing

* Delete selection content only if drag has been started at editor

* Add comments

* Support of pasting from copy-buffer

* Change header level

* Improvements

* Use isDraNDrop flag

* Update example

* Improve d'n'd behaviour

* git pull for simple-image tool
This commit is contained in:
George Berezhnoy 2018-09-27 21:11:30 +03:00 committed by GitHub
commit 577da8fa22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 775 additions and 319 deletions

View file

@ -120,6 +120,28 @@ export default class BlockEvents extends Module {
}
}
/**
* Add drop target styles
*
* @param {DragEvent} e
*/
public dragOver(e: DragEvent) {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target);
block.dropTarget = true;
}
/**
* Remove drop target style
*
* @param {DragEvent} e
*/
public dragLeave(e: DragEvent) {
const block = this.Editor.BlockManager.getBlockByChildNode(e.target);
block.dropTarget = false;
}
/**
* ENTER pressed on block
* @param {KeyboardEvent} event - keydown

View file

@ -103,6 +103,8 @@ export default class BlockManager extends Module {
this.Editor.Listeners.on(block.holder, 'keydown', (event) => this.Editor.BlockEvents.keydown(event), true);
this.Editor.Listeners.on(block.holder, 'mouseup', (event) => this.Editor.BlockEvents.mouseUp(event));
this.Editor.Listeners.on(block.holder, 'keyup', (event) => this.Editor.BlockEvents.keyup(event));
this.Editor.Listeners.on(block.holder, 'dragover', (event) => this.Editor.BlockEvents.dragOver(event));
this.Editor.Listeners.on(block.holder, 'dragleave', (event) => this.Editor.BlockEvents.dragLeave(event));
}
/**
@ -362,9 +364,10 @@ export default class BlockManager extends Module {
* 2) Mark it as current
*
* @param {Element|Text} childNode - look ahead from this node.
* @param {string} caretPosition - position where to set caret
* @throws Error - when passed Node is not included at the Block
*/
setCurrentBlockByChildNode(childNode) {
setCurrentBlockByChildNode(childNode, caretPosition = 'default') {
/**
* If node is Text TextNode
*/
@ -372,7 +375,7 @@ export default class BlockManager extends Module {
childNode = childNode.parentNode;
}
let parentFirstLevelBlock = childNode.closest(`.${Block.CSS.wrapper}`);
const parentFirstLevelBlock = childNode.closest(`.${Block.CSS.wrapper}`);
if (parentFirstLevelBlock) {
/**
@ -381,12 +384,31 @@ export default class BlockManager extends Module {
*/
this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock);
this.Editor.Caret.setToInput(childNode);
this.Editor.Caret.setToInput(childNode, caretPosition);
} else {
throw new Error('Can not find a Block from this child Node');
}
}
/**
* Return block which contents passed node
*
* @param {Node} childNode
* @return {Block}
*/
getBlockByChildNode(childNode) {
/**
* If node is Text TextNode
*/
if (!$.isElement(childNode)) {
childNode = childNode.parentNode;
}
const firstLevelBlock = childNode.closest(`.${Block.CSS.wrapper}`);
return this.blocks.find(block => block.holder === firstLevelBlock);
}
/**
* Swap Blocks Position
* @param {Number} fromIndex

View file

@ -0,0 +1,77 @@
import SelectionUtils from '../selection';
declare var Module: any;
export default class DragNDrop extends Module {
/**
* If drag has been started at editor, we save it
*
* @type Boolean
* @private
*/
private isStartedAtEditor = false;
/**
* Bind events
*
* @private
*/
public prepare(): void {
this.bindEvents();
}
/**
* Add drag events listeners to editor zone
* @private
*/
private bindEvents(): void {
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'drop', this.processDrop, true);
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragstart', (dragEvent: DragEvent) => {
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {
this.isStartedAtEditor = true;
}
this.Editor.InlineToolbar.close();
});
/* Prevent default browser behavior to allow drop on non-contenteditable elements */
this.Editor.Listeners.on(this.Editor.UI.nodes.holder, 'dragover', (e) => e.preventDefault(), true);
}
/**
* Handle drop event
*
* @param {DragEvent} dropEvent
*/
private processDrop = async (dropEvent: DragEvent): Promise<void> => {
const {
BlockManager,
Paste,
} = this.Editor;
dropEvent.preventDefault();
BlockManager.blocks.forEach((block) => block.dropTarget = false);
if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed && this.isStartedAtEditor) {
document.execCommand('delete');
}
this.isStartedAtEditor = false;
/**
* Try to set current block by drop target.
* If drop target (error will be thrown) is not part of the Block, set last Block as current.
*/
try {
BlockManager.setCurrentBlockByChildNode(dropEvent.target, 'end');
} catch (e) {
BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder, 'end');
}
Paste.processDataTransfer(dropEvent.dataTransfer, true);
}
}

View file

@ -1,6 +1,7 @@
import IBlockToolData from '../interfaces/tools/block-tool';
import IEditorConfig from '../interfaces/editor-config';
import CaretClass from './caret';
import SelectionUtils from '../selection';
declare const Module: any;
declare const $: any;
@ -32,6 +33,19 @@ interface IPatternSubstitute {
tool: string;
}
/**
* Files` types substitutions object.
*
* @param {string[]} extensions - array of extenstions Tool can handle
* @param {string[]} mimeTypes - array of MIME types Tool can handle
* @param {Function} handler - callback to handle pasted File
*/
interface IFilesSubstitution {
extensions: string[];
mimeTypes: string[];
handler: (file: File) => IBlockToolData;
}
/**
* Processed paste data object.
*
@ -46,6 +60,19 @@ interface IPasteData {
isBlock: boolean;
handler: (content: HTMLElement|string, patten?: RegExp) => IBlockToolData;
}
/**
* Tool onPaste configuration object
*/
interface IPasteConfig {
tags?: string[];
handler?: (element: HTMLElement) => IBlockToolData;
patterns?: {[key: string]: RegExp};
patternHandler?: (text: string, key: string) => IBlockToolData;
files?: {extensions?: string[], mimeTypes?: string[]};
fileHandler?: (file: File) => IBlockToolData;
}
/**
* @class Paste
* @classdesc Contains methods to handle paste on editor
@ -67,6 +94,11 @@ export default class Paste extends Module {
/** Patterns` substitutions parameters */
private toolsPatterns: IPatternSubstitute[] = [];
/** Files` substitutions parameters */
private toolsFiles: {
[tool: string]: IFilesSubstitution,
} = {};
/**
* @constructor
* @param {IEditorConfig} config
@ -75,18 +107,64 @@ export default class Paste extends Module {
super({config});
}
/**
* Set onPaste callback and collect tools` paste configurations
*
* @public
*/
public async prepare(): Promise<void> {
this.setCallback();
this.processTools();
}
/**
* Handle pasted or dropped data transfer object
*
* @param {DataTransfer} dataTransfer - pasted or dropped data transfer object
*/
public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise<void> {
const { Sanitizer } = this.Editor;
if (dataTransfer.types.includes('Files')) {
await this.processFiles(dataTransfer.items);
return;
}
const plainData = dataTransfer.getData('text/plain');
let htmlData = dataTransfer.getData('text/html');
/**
* If text was drag'n'dropped, wrap content with P tag to insert it as the new Block
*/
if (isDragNDrop && plainData.trim() && htmlData.trim()) {
htmlData = '<p>' + ( htmlData.trim() ? htmlData : plainData ) + '</p>';
}
/** Add all tags that can be substituted to sanitizer configuration */
const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {
result[tag.toLowerCase()] = true;
return result;
}, {});
const customConfig = {tags: Object.assign({}, toolsTags, Sanitizer.defaultConfig.tags)};
const cleanData = Sanitizer.clean(htmlData, customConfig);
/** If there is no HTML or HTML string is equal to plain one, process it as plain text */
if (!cleanData.trim() || cleanData.trim() === plainData || !$.isHTMLString(cleanData)) {
await this.processText(plainData);
} else {
await this.processText(cleanData, true);
}
}
/**
* Set onPaste callback handler
*/
private setCallback(): void {
const {Listeners, UI} = this.Editor;
Listeners.on(UI.nodes.redactor, 'paste', this.processPastedData);
Listeners.on(UI.nodes.redactor, 'paste', this.handlePasteEvent);
}
/**
@ -99,76 +177,157 @@ export default class Paste extends Module {
}
/**
* Process paste config for each tools
* Process paste config for each tool
*
* @param {string} tool
* @param {string} name
* @param {Tool} tool
*/
private processTool = ([name, tool]) => {
try {
const toolPasteConfig = tool.onPaste || {};
if (this.config.initialBlock === name && !toolPasteConfig.handler) {
this.getTagsConfig(name, toolPasteConfig);
this.getFilesConfig(name, toolPasteConfig);
this.getPatternsConfig(name, toolPasteConfig);
} catch (e) {
_.log(
`Paste handling for «${name}» Tool hasn't been set up because of the error`,
'warn',
e,
);
}
}
/**
* Get tags to substitute by Tool
*
* @param {string} name - Tool name
* @param {IPasteConfig} toolPasteConfig - Tool onPaste configuration
*/
private getTagsConfig(name: string, toolPasteConfig: IPasteConfig): 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) => {
if (this.toolsTags.hasOwnProperty(tag)) {
_.log(
`«${name}» Tool must provide a paste handler.`,
`Paste handler for «${name}» Tool on «${tag}» tag is skipped ` +
`because it is already used by «${this.toolsTags[tag].tool}» Tool.`,
'warn',
);
}
if (toolPasteConfig.handler && typeof toolPasteConfig.handler !== 'function') {
_.log(
`Paste handler for «${name}» Tool should be a function.`,
'warn',
);
} else {
const tags = toolPasteConfig.tags || [];
tags.forEach((tag) => {
if (this.toolsTags.hasOwnProperty(tag)) {
_.log(
`Paste handler for «${name}» Tool on «${tag}» tag is skipped ` +
`because it is already used by «${this.toolsTags[tag].tool}» Tool.`,
'warn',
);
return;
}
this.toolsTags[tag] = {
handler: toolPasteConfig.handler,
tool: name,
};
});
}
if (!toolPasteConfig.patternHandler || _.isEmpty(toolPasteConfig.patterns)) {
return;
}
if (typeof toolPasteConfig.patternHandler !== 'function') {
this.toolsTags[tag.toUpperCase()] = {
handler: toolPasteConfig.handler,
tool: name,
};
});
}
/**
* Get files` types and extensions to substitute by Tool
*
* @param {string} name - Tool name
* @param {IPasteConfig} toolPasteConfig - Tool onPaste configuration
*/
private getFilesConfig(name: string, toolPasteConfig: IPasteConfig): void {
const {fileHandler, 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.`);
return;
}
if (extensions && !Array.isArray(extensions)) {
_.log(`«extensions» property of the onDrop config for «${name}» Tool should be an array`);
extensions = [];
}
if (mimeTypes && !Array.isArray(mimeTypes)) {
_.log(`«mimeTypes» property of the onDrop config for «${name}» Tool should be an array`);
mimeTypes = [];
}
if (mimeTypes) {
mimeTypes = mimeTypes.filter((type) => {
if (!_.isValidMimeType(type)) {
_.log(`MIME type value «${type}» for the «${name}» Tool is not a valid MIME type`, 'warn');
return false;
}
return true;
});
}
this.toolsFiles[name] = {
extensions: extensions || [],
mimeTypes: mimeTypes || [],
handler: fileHandler,
};
}
/**
* Get RegExp patterns to substitute by Tool
*
* @param {string} name - Tool name
* @param {IPasteConfig} toolPasteConfig - Tool onPaste configuration
*/
private getPatternsConfig(name: string, toolPasteConfig: IPasteConfig): void {
if (!toolPasteConfig.patternHandler || _.isEmpty(toolPasteConfig.patterns)) {
return;
}
if (typeof toolPasteConfig.patternHandler !== 'function') {
_.log(
`Pattern parser for «${name}» Tool should be a function.`,
'warn',
);
return;
}
Object.entries(toolPasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => {
/** Still need to validate pattern as it provided by user */
if (!(pattern instanceof RegExp)) {
_.log(
`Pattern parser for «${name}» Tool should be a function.`,
`Pattern ${pattern} for «${name}» Tool is skipped because it should be a Regexp instance.`,
'warn',
);
} else {
Object.entries(toolPasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => {
/** Still need to validate pattern as it provided by user */
if (!(pattern instanceof RegExp)) {
_.log(
`Pattern ${pattern} for «${name}» Tool is skipped because it should be a Regexp instance.`,
'warn',
);
}
this.toolsPatterns.push({
key,
pattern,
handler: toolPasteConfig.patternHandler,
tool: name,
});
});
}
} catch (e) {
_.log(`Paste handling for «${name}» Tool is not enabled because of an error `, 'warn', e);
}
this.toolsPatterns.push({
key,
pattern,
handler: toolPasteConfig.patternHandler,
tool: name,
});
});
}
/**
@ -190,42 +349,106 @@ export default class Paste extends Module {
}
/**
* Get pasted data, process it and insert into editor
* Check if Editor should process pasted data and pass data transfer object to handler
*
* @param {ClipboardEvent} event
*/
private processPastedData = async (event: ClipboardEvent): Promise<void> => {
private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => {
const {
Editor: {Tools, Sanitizer, BlockManager, Caret},
Editor: {Sanitizer, BlockManager, Tools, Caret},
} = this;
/** If target is native input or is not Block, use browser behaviour */
if (this.isNativeBehaviour(event.target)) {
if (
this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files')
) {
return;
}
event.preventDefault();
this.processDataTransfer(event.clipboardData);
}
const htmlData = event.clipboardData.getData('text/html'),
plainData = event.clipboardData.getData('text/plain');
/**
* Get files from data transfer object and insert related Tools
*
* @param {DataTransferItemList} items - pasted or dropped items
*/
private async processFiles(items: DataTransferItemList) {
const {BlockManager} = this.Editor;
/** Add all tags that can be substituted to sanitizer configuration */
const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {
result[tag.toLowerCase()] = {};
let dataToInsert: Array<{type: string, data: IBlockToolData}>;
return result;
}, {});
dataToInsert = await Promise.all(
Array
.from(items)
.map((item) => this.processFile(item)),
);
dataToInsert = dataToInsert.filter((data) => !!data);
const customConfig = {tags: Object.assign({}, toolsTags, Sanitizer.defaultConfig.tags)};
const cleanData = Sanitizer.clean(htmlData, customConfig);
dataToInsert.forEach(
(data, i) => {
if (i === 0 && BlockManager.currentBlock && BlockManager.currentBlock.isEmpty) {
BlockManager.replace(data.type, data.data);
return;
}
let dataToInsert = [];
BlockManager.insert(data.type, data.data);
},
);
}
/** If there is no HTML or HTML string is equal to plain one, process it as plain text */
if (!cleanData.trim() || cleanData.trim() === plainData || !$.isHTMLString(cleanData)) {
dataToInsert = this.processPlain(plainData);
} else {
dataToInsert = this.processHTML(htmlData);
/**
* Get information about file and find Tool to handle it
*
* @param {DataTransferItem} item
*/
private async processFile(item: DataTransferItem) {
if (item.kind === 'string') {
return;
}
const file = item.getAsFile();
const extension = _.getFileExtension(file);
const foundConfig = Object
.entries(this.toolsFiles)
.find(([toolName, {mimeTypes, extensions}]) => {
const [fileType, fileSubtype] = file.type.split('/');
const foundExt = extensions.find((ext) => ext.toLowerCase() === extension.toLowerCase());
const foundMimeType = mimeTypes.find((mime) => {
const [type, subtype] = mime.split('/');
return type === fileType && (subtype === fileSubtype || subtype === '*');
});
return !!foundExt || !!foundMimeType;
});
if (!foundConfig) {
return;
}
const [tool, {handler}] = foundConfig;
return {
data: await handler(file),
type: tool,
};
}
/**
* Process pasted text and divide them into Blocks
*
* @param {string} data - text to process. Can be HTML or plain.
* @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 dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data);
if (!dataToInsert.length) {
return;
}
if (dataToInsert.length === 1 && !dataToInsert[0].isBlock) {
@ -233,15 +456,103 @@ export default class Paste extends Module {
return;
}
this.splitBlock();
/**
* 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();
}
await Promise.all(dataToInsert.map(
async (data, i) => await this.insertBlock(data, i === 0),
async (content, i) => await this.insertBlock(content, i === 0),
));
Caret.setToBlock(BlockManager.currentBlock, CaretClass.positions.END);
}
/**
* Split HTML string to blocks and return it as array of Block data
*
* @param {string} innerHTML
* @returns {IPasteData[]}
*/
private processHTML(innerHTML: string): IPasteData[] {
const {Tools, Sanitizer} = this.Editor,
initialTool = this.config.initialBlock,
wrapper = $.make('DIV');
wrapper.innerHTML = innerHTML;
const nodes = this.getNodes(wrapper);
return nodes
.map((node) => {
let content, tool = initialTool, isBlock = false;
switch (node.nodeType) {
/** If node is a document fragment, use temp wrapper to get innerHTML */
case Node.DOCUMENT_FRAGMENT_NODE:
content = $.make('div');
content.appendChild(node);
break;
/** If node is an element, then there might be a substitution */
case Node.ELEMENT_NODE:
content = node as HTMLElement;
isBlock = true;
if (this.toolsTags[content.tagName]) {
tool = this.toolsTags[content.tagName].tool;
}
break;
}
const {handler, tags} = Tools.blockTools[tool].onPaste;
const toolTags = tags.reduce((result, tag) => {
result[tag.toLowerCase()] = {};
return result;
}, {});
const customConfig = {tags: Object.assign({}, toolTags, Sanitizer.defaultConfig.tags)};
content.innerHTML = Sanitizer.clean(content.innerHTML, customConfig);
return {content, isBlock, handler, tool};
})
.filter((data) => !$.isNodeEmpty(data.content) || $.isSingleTag(data.content));
}
/**
* Split plain text by new line symbols and return it as array of Block data
*
* @param {string} plain
* @returns {IPasteData[]}
*/
private processPlain(plain: string): IPasteData[] {
const {initialBlock} = this.config as {initialBlock: string},
{Tools} = this.Editor;
if (!plain) {
return [];
}
const tool = initialBlock,
handler = Tools.blockTools[tool].onPaste.handler;
return plain
.split(/\r?\n/)
.filter((text) => text.trim())
.map((text) => {
const content = $.make('div');
content.innerHTML = text;
return {content, tool, isBlock: false, handler};
});
}
/**
* Process paste to single Block:
* 1. Find patterns` matches
@ -342,85 +653,6 @@ export default class Paste extends Module {
}
}
/**
* Split HTML string to blocks and return it as array of Block data
*
* @param {string} innerHTML
* @returns {IPasteData[]}
*/
private processHTML(innerHTML: string): IPasteData[] {
const {Tools, Sanitizer} = this.Editor,
initialTool = this.config.initialBlock,
wrapper = $.make('DIV');
wrapper.innerHTML = innerHTML;
const nodes = this.getNodes(wrapper);
return nodes
.map((node) => {
let content, tool = initialTool, isBlock = false;
switch (node.nodeType) {
/** If node is a document fragment, use temp wrapper to get innerHTML */
case Node.DOCUMENT_FRAGMENT_NODE:
content = $.make('div');
content.appendChild(node);
break;
/** If node is an element, then there might be a substitution */
case Node.ELEMENT_NODE:
content = node as HTMLElement;
isBlock = true;
if (this.toolsTags[content.tagName]) {
tool = this.toolsTags[content.tagName].tool;
}
break;
}
const {handler, tags} = Tools.blockTools[tool].onPaste;
const toolTags = tags.reduce((result, tag) => {
result[tag.toLowerCase()] = {};
return result;
}, {});
const customConfig = {tags: Object.assign({}, toolTags, Sanitizer.defaultConfig.tags)};
content.innerHTML = Sanitizer.clean(content.innerHTML, customConfig);
return {content, isBlock, handler, tool};
})
.filter((data) => !$.isNodeEmpty(data.content));
}
/**
* Split plain text by new line symbols and return it as array of Block data
*
* @param {string} plain
* @returns {IPasteData[]}
*/
private processPlain(plain: string): IPasteData[] {
const {initialBlock} = this.config as {initialBlock: string},
{Tools} = this.Editor;
if (!plain) {
return [];
}
const tool = initialBlock,
handler = Tools.blockTools[tool].onPaste.handler;
return plain.split('\n\n').map((text) => {
const content = $.make('div');
content.innerHTML = text;
return {content, tool, isBlock: false, handler};
});
}
/**
* Recursively divide HTML string to two types of nodes:
* 1. Block element
@ -434,7 +666,7 @@ export default class Paste extends Module {
tags = Object.keys(this.toolsTags);
const reducer = (nodes: Node[], node: Node): Node[] => {
if ($.isEmpty(node)) {
if ($.isEmpty(node) && !$.isSingleTag(node as HTMLElement)) {
return nodes;
}