mirror of
https://github.com/codex-team/editor.js
synced 2024-06-01 05:22:35 +02:00
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 commit779bf5db9e
, reversing changes made to972eb87d89
. * 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 commitc1f63894d7
. * 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:
parent
cdf6d973c1
commit
577da8fa22
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -4,14 +4,14 @@ CodeX Editor is a block-oriented editor. It means that entry composed with the l
|
|||
|
||||
`Tool` — is a class that provide custom `Block` type. All Tools represented by `Plugins`.
|
||||
|
||||
Each Tool should have an installation guide.
|
||||
Each Tool should have an installation guide.
|
||||
|
||||
## Tool class structure
|
||||
|
||||
### constructor()
|
||||
|
||||
Each Tool's instance called with an params object.
|
||||
|
||||
|
||||
| Param | Type | Description |
|
||||
| ------ | ------------------- | ----------------------------------------------- |
|
||||
| api | [`IAPI`][iapi-link] | CodeX Editor's API methods |
|
||||
|
@ -62,7 +62,7 @@ Options that Tool can specify. All settings should be passed as static propertie
|
|||
|
||||
### User configuration
|
||||
|
||||
All Tools can be configured by users. You can set up some of available settings along with Tool's class
|
||||
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.
|
||||
|
||||
```javascript
|
||||
|
@ -103,7 +103,7 @@ To provide paste handling for your Tool you need to define static getter `onPast
|
|||
|
||||
##### HTML tags handling
|
||||
|
||||
To handle pasted HTML elements object returned from `onPaste` getter should contain following fields:
|
||||
To handle pasted HTML elements object returned from `onPaste` getter should contain following fields:
|
||||
|
||||
| Name | Type | Description |
|
||||
| -- | -- | -- |
|
||||
|
@ -140,7 +140,7 @@ Your Tool can analyze text by RegExp patterns to substitute pasted string with d
|
|||
| `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.
|
||||
**Note** Editor will check pattern's full match, so don't forget to handle all available chars in there.
|
||||
|
||||
Pattern will be processed only if paste was on `initialBlock` Tool and pasted string length is less than 450 characters.
|
||||
|
||||
|
@ -156,7 +156,7 @@ static get onPaste() {
|
|||
},
|
||||
patternHandler: (text, key) => {
|
||||
const urlData = Youtube.onPaste.patterns[key].exec(text);
|
||||
|
||||
|
||||
return {
|
||||
iframe: Youtube.makeEmbededFromURL(urlData)
|
||||
};
|
||||
|
@ -167,4 +167,41 @@ static get onPaste() {
|
|||
|
||||
> Both `onPaste.handler` and `onPaste.patternHandler` can be `async` or return a `Promise`.
|
||||
|
||||
##### Files
|
||||
|
||||
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.
|
||||
|
||||
`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:
|
||||
|
||||
| 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() {
|
||||
return {
|
||||
files: {
|
||||
mimeTypes: ['image/png'],
|
||||
extensions: ['json']
|
||||
},
|
||||
fileHandler: (file) => {
|
||||
/* do smth with the file */
|
||||
|
||||
return {
|
||||
data // Some extracted content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sanitize
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit b3ba0e5de15bed3d4354b4bf23f63158bd4167dd
|
||||
Subproject commit 94389715bd0b33d2ae1a1aa715176e460ef3216f
|
|
@ -32,12 +32,13 @@ export default class Block {
|
|||
* CSS classes for the Block
|
||||
* @return {{wrapper: string, content: string}}
|
||||
*/
|
||||
static get CSS(): {wrapper: string, wrapperStretched: string, content: string, selected: string} {
|
||||
static get CSS(): {wrapper: string, wrapperStretched: string, content: string, selected: string, dropTarget: string} {
|
||||
return {
|
||||
wrapper: 'ce-block',
|
||||
wrapperStretched: 'ce-block--stretched',
|
||||
content: 'ce-block__content',
|
||||
selected: 'ce-block--selected',
|
||||
dropTarget: 'ce-block--drop-target',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -363,6 +364,14 @@ export default class Block {
|
|||
return tunesElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle drop target state
|
||||
* @param {boolean} state
|
||||
*/
|
||||
public set dropTarget(state) {
|
||||
this.holder.classList.toggle(Block.CSS.dropTarget, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make default Block wrappers and put Tool`s content there
|
||||
* @returns {HTMLDivElement}
|
||||
|
|
|
@ -262,7 +262,7 @@ export default class Core {
|
|||
* @return {Promise}
|
||||
*/
|
||||
async start() {
|
||||
const modulesToPrepare = ['Tools', 'UI', 'BlockManager', 'Paste', 'ModificationsObserver'];
|
||||
const modulesToPrepare = ['Tools', 'UI', 'BlockManager', 'Paste', 'DragNDrop', 'ModificationsObserver'];
|
||||
|
||||
await modulesToPrepare.reduce(
|
||||
(promise, module) => promise.then(async () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
77
src/components/modules/dragNDrop.ts
Normal file
77
src/components/modules/dragNDrop.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -182,6 +182,26 @@ export default class Util {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension
|
||||
*
|
||||
* @param {File} file
|
||||
* @return string
|
||||
*/
|
||||
public static getFileExtension(file: File): string {
|
||||
return file.name.split('.').pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if string is MIME type
|
||||
*
|
||||
* @param {string} type
|
||||
* @return boolean
|
||||
*/
|
||||
public static isValidMimeType(type: string): boolean {
|
||||
return /^[-\w]+\/([-+\w]+|\*)$/.test(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debouncing method
|
||||
* Call method after passed time
|
||||
|
@ -202,7 +222,7 @@ export default class Util {
|
|||
|
||||
const later = () => {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -13,7 +13,40 @@
|
|||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&--drop-target &__content {
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: -20px;
|
||||
margin-top: -1px;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border: solid var(--color-active-icon);
|
||||
border-width: 1px 1px 0 0;
|
||||
transform-origin: right;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
color: var(--color-active-icon);
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
var(--color-active-icon),
|
||||
var(--color-active-icon) 1px,
|
||||
#fff 1px,
|
||||
#fff 6px
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue