editor.js/src/components/modules/paste.ts
Peter Savchenko 61242ab6a6
Release 2.0-beta (#387)
* update
* Toolbar, Toolbox, UI (#239)
* Toolbox making
* Add Toolbox buttons click handler
* Toolbar, Toolbox, UI
* Updates
* update css prefix
* trying to write docs
* append callback behaviour
* update request
* update
* empty initial data
* initial saver
* some requested changes
* update request
* requested changes
* upgrade saver
* new improvements
* update
* Caret module: initial
* improvements
* 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
* 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"
* 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
* 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"
* 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)
* 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
* 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)
* 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)
* 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
* 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
* 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
* 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
* 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

👨‍🎓

Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
2018-08-05 14:51:58 +03:00

491 lines
13 KiB
TypeScript

import IBlockToolData from '../interfaces/tools/block-tool';
import IEditorConfig from '../interfaces/editor-config';
import CaretClass from './caret';
declare const Module: any;
declare const $: any;
declare const _: any;
/**
* Tag substitute object.
*
* @param {string} tool - name of related Tool
* @param {Function} handler - callback to handle pasted element
*/
interface ITagSubstitute {
tool: string;
handler: (element: HTMLElement) => IBlockToolData;
}
/**
* Pattern substitute object.
*
* @param {string} key - pattern`s key
* @param {RegExp} pattern - pasted pattern
* @param {Function} handler - callback to handle pasted pattern
* @param {string} tool - name of related Tool
*/
interface IPatternSubstitute {
key: string;
pattern: RegExp;
handler: (text: string, key: string) => IBlockToolData;
tool: string;
}
/**
* Processed paste data object.
*
* @param {string} tool - name of related Tool
* @param {HTMLElement} content - processed pasted content
* @param {boolean} isBlock - true if content should be inserted as new Block
* @param {Function} handler - callback that returns pasted data in IBlockToolData format
*/
interface IPasteData {
tool: string;
content: HTMLElement;
isBlock: boolean;
handler: (content: HTMLElement|string, patten?: RegExp) => IBlockToolData;
}
/**
* @class Paste
* @classdesc Contains methods to handle paste on editor
*
* @module Paste
*
* @version 2.0.0
*/
export default class Paste extends Module {
/** If string`s length is greater than this number we don't check paste patterns */
public static readonly PATTERN_PROCESSING_MAX_LENGTH = 450;
/**
* Tags` substitutions parameters
*/
private toolsTags: {[tag: string]: ITagSubstitute} = {};
/** Patterns` substitutions parameters */
private toolsPatterns: IPatternSubstitute[] = [];
/**
* @constructor
* @param {IEditorConfig} config
*/
constructor({config}) {
super({config});
}
public async prepare(): Promise<void> {
this.setCallback();
this.processTools();
}
/**
* Set onPaste callback handler
*/
private setCallback(): void {
const {Listeners, UI} = this.Editor;
Listeners.on(UI.nodes.redactor, 'paste', this.processPastedData);
}
/**
* Get and process tool`s paste configs
*/
private processTools(): void {
const tools = this.Editor.Tools.blockTools;
Object.entries(tools).forEach(this.processTool);
}
/**
* Process paste config for each tools
*
* @param {string} tool
*/
private processTool = ([name, tool]) => {
const toolPasteConfig = tool.onPaste || {};
if (this.config.initialBlock === name && !toolPasteConfig.handler) {
_.log(
`«${name}» Tool must provide a paste handler.`,
'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') {
_.log(
`Pattern parser for "${name}" Tool should be a function.`,
'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 "${tool}" Tool is skipped because it should be a Regexp instance.`,
'warn',
);
}
this.toolsPatterns.push({
key,
pattern,
handler: toolPasteConfig.patternHandler,
tool: name,
});
});
}
}
/**
* Check if browser behavior suits better
*
* @param {EventTarget} element - element where content has been pasted
* @returns {boolean}
*/
private isNativeBehaviour(element: EventTarget): boolean {
const {Editor: {BlockManager}} = this;
if ( $.isNativeInput(element) ) {
return true;
}
const block = BlockManager.getBlock(element);
return !block;
}
/**
* Get pasted data, process it and insert into editor
*
* @param {ClipboardEvent} event
*/
private processPastedData = async (event: ClipboardEvent): Promise<void> => {
const {
Editor: {Tools, Sanitizer, BlockManager, Caret},
} = this;
/** If target is native input or is not Block, use browser behaviour */
if (this.isNativeBehaviour(event.target)) {
return;
}
event.preventDefault();
const htmlData = event.clipboardData.getData('text/html'),
plainData = event.clipboardData.getData('text/plain');
/** Add all tags that can be substituted to sanitizer configuration */
const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {
result[tag.toLowerCase()] = {};
return result;
}, {});
const customConfig = {tags: Object.assign({}, toolsTags, Sanitizer.defaultConfig.tags)};
const cleanData = Sanitizer.clean(htmlData, customConfig);
let dataToInsert = [];
/** 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);
}
if (dataToInsert.length === 1 && !dataToInsert[0].isBlock) {
this.processSingleBlock(dataToInsert.pop());
return;
}
this.splitBlock();
await Promise.all(dataToInsert.map(
async (data, i) => await this.insertBlock(data, i === 0),
));
Caret.setToBlock(BlockManager.currentBlock, CaretClass.positions.END);
}
/**
* Process paste to single Block:
* 1. Find patterns` matches
* 2. Insert new block if it is not the same type as current one
* 3. Just insert text if there is no substitutions
*
* @param {IPasteData} dataToInsert
*/
private async processSingleBlock(dataToInsert: IPasteData): Promise<void> {
const initialTool = this.config.initialBlock,
{BlockManager, Caret} = 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;
if (BlockManager.currentBlock && BlockManager.currentBlock.isEmpty) {
BlockManager.replace(blockData.tool, blockData.data);
} else {
insertedBlock = BlockManager.insert(blockData.tool, blockData.data);
}
Caret.setToBlock(insertedBlock, CaretClass.positions.END);
return;
}
}
/** If there is no pattern substitute - insert string as it is */
document.execCommand('insertHTML', false, content.innerHTML);
}
/**
* Get patterns` matches
*
* @param {string} text
* @returns Promise<{data: IBlockToolData, tool: string}>
*/
private async processPattern(text: string): Promise<{data: IBlockToolData, tool: string}> {
const pattern = this.toolsPatterns.find((substitute) => {
const execResult = substitute.pattern.exec(text);
if (!execResult) {
return false;
}
return text === execResult.shift();
});
const data = pattern && await pattern.handler(text, pattern.key);
return data && {
data,
tool: pattern.tool,
};
}
/**
*
* @param {IPasteData} data
* @param {Boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block
* @returns {Promise<void>}
*/
private async insertBlock(data: IPasteData, canReplaceCurrentBlock: boolean = false): Promise<void> {
const blockData = await data.handler(data.content),
{BlockManager, Caret} = this.Editor,
{currentBlock} = BlockManager;
if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) {
BlockManager.replace(data.tool, blockData);
return;
}
const Block = BlockManager.insert(data.tool, blockData);
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--;
}
}
/**
* 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
* 2. Document Fragments contained text and markup tags like a, b, i etc.
*
* @param {Node} wrapper
* @returns {Node[]}
*/
private getNodes(wrapper: Node): Node[] {
const children = Array.from(wrapper.childNodes),
tags = Object.keys(this.toolsTags);
const reducer = (nodes: Node[], node: Node): Node[] => {
if ($.isEmpty(node)) {
return nodes;
}
const lastNode = nodes[nodes.length - 1];
let destNode: Node = new DocumentFragment();
if (lastNode && $.isFragment(lastNode)) {
destNode = nodes.pop();
}
switch (node.nodeType) {
/**
* If node is HTML element:
* 1. Check if it is inline element
* 2. Check if it contains another block or substitutable elements
*/
case Node.ELEMENT_NODE:
const element = node as HTMLElement;
/** Append inline elements to previous fragment */
if (
!$.blockElements.includes(element.tagName.toLowerCase()) &&
!tags.includes(element.tagName)
) {
destNode.appendChild(element);
return [...nodes, destNode];
}
if (tags.includes(element.tagName) || (
$.blockElements.includes(element.tagName.toLowerCase()) &&
Array.from(element.children).every(
({tagName}) => !$.blockElements.includes(tagName.toLowerCase()),
)
)
) {
return [...nodes, element];
}
break;
/**
* If node is text node, wrap it with DocumentFragment
*/
case Node.TEXT_NODE:
destNode.appendChild(node);
return [...nodes, destNode];
default:
return [...nodes, destNode];
}
return [...nodes, ...Array.from(node.childNodes).reduce(reducer, [])];
};
return children.reduce(reducer, []);
}
}