UNPKG

@syncfusion/ej2-richtexteditor

Version:
934 lines (932 loc) 118 kB
import { NodeSelection } from './../../selection/index'; import { NodeCutter } from './nodecutter'; import * as CONSTANT from './../base/constant'; import { detach, Browser, isNullOrUndefined as isNOU, createElement, closest } from '@syncfusion/ej2-base'; import { InsertMethods } from './insert-methods'; import { updateTextNode, nestedListCleanUp, scrollToCursor, cleanHTMLString } from './../../common/util'; /** * This InsertHtml class contains methods to insert HTML nodes or text into a document. * * @hidden */ var InsertHtml = /** @class */ (function () { function InsertHtml() { } /** * Inserts an HTML node or text into the specified document. * * @param {Document} docElement - The document where the node should be inserted. * @param {Node | string} insertNode - The node or text to be inserted. Can be a DOM Node or a string representing HTML. * @param {Element} [editNode] - The container or editor node where the insertion will occur. * @param {boolean} [isExternal] - Flag indicating if the node is from an external source. Optional. * @param {string} [enterAction] - Represents the action taken when 'Enter' is pressed. Optional. * @param {EditorManager} [editorManager] - Represents the EditorManager instance. Optional. * @returns {void} * @hidden */ InsertHtml.Insert = function (docElement, insertNode, editNode, isExternal, enterAction, editorManager) { var insertedNode = this.prepareInsertNode(insertNode, isExternal, editNode); var scrollHeight = !isNOU(editNode) ? editNode.scrollHeight : 0; var nodeSelection = new NodeSelection(editNode); var nodeCutter = new NodeCutter(); var range = nodeSelection.getRange(docElement); //Adjusts the selection range to handle various edge cases for cursor positioning range = this.adjustSelectionRange(nodeSelection, docElement, editNode, range); var isCursor = this.isCursorAtStartPoint(range); var isCollapsed = range.collapsed; var nodes = this.getNodeCollection(range, nodeSelection, insertedNode); var isInsertedNodeTable = insertedNode.nodeName.toLowerCase() === 'table'; var closestParentNode = this.findRelevantParentNode(nodes, isInsertedNodeTable, range, editNode); // Handle BR parent case if (closestParentNode && closestParentNode.nodeName === 'BR') { closestParentNode = closestParentNode.parentNode; } // Handling the table insertion inside list items if (closestParentNode && closestParentNode.nodeName === 'LI' && isInsertedNodeTable) { this.handleTableInListItem(range, insertedNode, closestParentNode, nodes, nodeSelection, nodeCutter, editNode); return; } // Handle image insertion at empty cursor position var isImgOnlyNode = insertedNode.nodeName !== '#text' && !isNOU(insertedNode.children[0]) && !isNOU(insertedNode.children[0].tagName) && insertedNode.children[0].tagName === 'IMG' && insertedNode.children.length === 1; var isEmptyCursorPosition = isCursor && range.startContainer.textContent === '' && range.startContainer.nodeName !== 'BR' && enterAction !== 'BR'; if (isEmptyCursorPosition && isImgOnlyNode) { range.startContainer.innerHTML = ''; } var isPasteContentOrInsertHtml = isExternal || (!isNOU(insertedNode) && !isNOU(insertedNode.classList) && insertedNode.classList.contains('pasteContent')); var targetCells = docElement.querySelectorAll('td.e-cell-select, th.e-cell-select'); if (targetCells && targetCells.length > 1) { this.clearTargetCells(targetCells); } if (isPasteContentOrInsertHtml) { if (editorManager && editorManager.tableObj && editorManager.tableObj.tablePastingObj) { var tablePastingObj = editorManager.tableObj.tablePastingObj; var insertedTable = tablePastingObj.getValidTableFromPaste(insertedNode); var hasSelectedTargetCells = targetCells && targetCells.length > 0; if (hasSelectedTargetCells && insertedTable) { // Delegate to the table pasting logic tablePastingObj.handleTablePaste(insertedTable, targetCells); return; } } this.pasteInsertHTML(nodes, insertedNode, range, nodeSelection, nodeCutter, docElement, isCollapsed, closestParentNode, editNode, enterAction); return; } if (this.shouldInsertOutsideRange(editNode, range, isCollapsed, closestParentNode, insertedNode)) { this.handleContentInsertionOutsideRange(docElement, editNode, range, nodeSelection, nodeCutter, isCollapsed, closestParentNode, insertedNode, nodes, insertNode, isCursor, enterAction); } else { this.handleContentInsertionInsideRange(docElement, range, nodeSelection, closestParentNode, insertedNode, isCursor); } // Scroll to cursor if needed for the image if (this.shouldScrollToCursor(editNode, scrollHeight, insertedNode)) { scrollToCursor(docElement, editNode); } }; /* * Clears the content of all target cells by setting their innerHTML to a line break */ InsertHtml.clearTargetCells = function (cells) { for (var i = 0; i < cells.length; i++) { cells[i].innerHTML = '<br>'; } }; // Prepares the node or HTML string for insertion, attaching it to a temporary container if necessary, and ensuring valid usage. InsertHtml.prepareInsertNode = function (insertNode, isExternal, editNode) { if (typeof insertNode === 'string') { insertNode = cleanHTMLString(insertNode, editNode); var divNode = createElement('div'); divNode.innerHTML = insertNode.replace(/&(times|divide|ne)(;?)/g, '&amp;$1$2'); return isExternal ? divNode : divNode.firstChild; } else { var isValidPasteContent = !isNOU(insertNode) && !isNOU(insertNode.classList) && insertNode.classList.contains('pasteContent'); if (isExternal && !isValidPasteContent) { var divNode = createElement('div'); divNode.appendChild(insertNode); return divNode; } else { return insertNode; } } }; // Adjusts the selection range to handle various edge cases for cursor positioning. InsertHtml.adjustSelectionRange = function (nodeSelection, docElement, editNode, range) { // Check if this is a collapsed selection at the beginning (offset 0) var isCollapsedAtStart = range.startContainer === range.endContainer && range.startOffset === 0 && range.startOffset === range.endOffset; if (!isCollapsedAtStart) { return range; // Early return if not a collapsed selection at start } // Apply each adjustment in based on the cursor range. range = this.adjustEmptyEditorSelection(nodeSelection, docElement, editNode, range); range = this.adjustSelectionToFirstTextNode(nodeSelection, docElement, editNode, range); range = this.adjustBrElementSelection(nodeSelection, docElement, range); return range; }; // Adjusts selection when the editor is empty with a single block element. InsertHtml.adjustEmptyEditorSelection = function (nodeSelection, docElement, editNode, range) { if (range.startContainer === editNode && editNode.textContent.length === 0 && (editNode.children[0].tagName === 'P' || editNode.children[0].tagName === 'DIV' || editNode.children[0].tagName === 'BR')) { nodeSelection.setSelectionText(docElement, range.startContainer.children[0], range.startContainer.children[0], 0, 0); return nodeSelection.getRange(docElement); } return range; }; // Adjusts selection to the first text node when cursor is at the start of content. InsertHtml.adjustSelectionToFirstTextNode = function (nodeSelection, docElement, editNode, range) { if (range.startContainer === editNode && editNode.textContent.trim().length > 0 && editNode.childNodes[0].tagName !== 'TABLE') { var focusNode = this.findFirstTextNode(range.startContainer); if (!isNOU(focusNode)) { nodeSelection.setSelectionText(docElement, focusNode, focusNode, 0, 0); return nodeSelection.getRange(docElement); } } return range; }; // Adjusts selection when cursor is on a BR element InsertHtml.adjustBrElementSelection = function (nodeSelection, docElement, range) { if (range.startContainer.nodeName === 'BR') { var currentIndex = Array.prototype.slice.call(range.startContainer.parentElement.childNodes).indexOf(range.startContainer); nodeSelection.setSelectionText(docElement, range.startContainer.parentElement, range.startContainer.parentElement, currentIndex, currentIndex); return nodeSelection.getRange(docElement); } return range; }; // Handles the insertion of a table element within a list item context. InsertHtml.handleTableInListItem = function (range, insertedNode, closestParentNode, nodes, nodeSelection, nodeCutter, editNode) { if (nodes.length === 0) { var tableCursor = nodeSelection.processedTableImageCursor(range); if (tableCursor.startName === 'TABLE' || tableCursor.endName === 'TABLE') { var tableNode = tableCursor.start ? tableCursor.startNode : tableCursor.endNode; nodes.push(tableNode); } } var lastClosestParentNode = this.findClosestRelevantElement(nodes[nodes.length - 1].parentNode, editNode); this.insertTableInList(range, insertedNode, closestParentNode, nodes[0], nodeCutter, lastClosestParentNode, editNode); }; // Determines if the cursor is positioned at the start of the range. InsertHtml.isCursorAtStartPoint = function (range) { return range.startOffset === 0 && range.startOffset === range.endOffset && range.startContainer === range.endContainer; }; // Identifies the most contextually relevant parent node for insertion based on various criteria. InsertHtml.findRelevantParentNode = function (nodes, isInsertedNodeTable, range, editNode) { if (isInsertedNodeTable) { return (!isNOU(nodes[0]) && !isNOU(nodes[0].parentNode)) ? this.findClosestRelevantElement(nodes[0].parentNode, editNode) : range.startContainer; } else { return nodes[0]; } }; // Checks if the content should be inserted outside the existing selection range based on multiple checks. InsertHtml.shouldInsertOutsideRange = function (editNode, range, isCollapsed, closestParentNode, insertedNode) { return editNode !== range.startContainer && ((!isCollapsed && !(closestParentNode.nodeType === Node.ELEMENT_NODE && CONSTANT.TABLE_BLOCK_TAGS.indexOf(closestParentNode.tagName.toLocaleLowerCase()) !== -1)) || (insertedNode.nodeName.toLowerCase() === 'table' && closestParentNode && CONSTANT.TABLE_BLOCK_TAGS.indexOf(closestParentNode.tagName.toLocaleLowerCase()) === -1)); }; // Handles insertion of content outside the specified selection range, managing complex cases including tables. InsertHtml.handleContentInsertionOutsideRange = function (docElement, editNode, range, nodeSelection, nodeCutter, isCollapsed, closestParentNode, insertedNode, nodes, insertNode, isCursor, enterAction) { // Extract content and prepare for insertion var preNode = nodeCutter.GetSpliceNode(range, closestParentNode); var sibNode = preNode.previousSibling; var parentNode = preNode.parentNode; // Update selection based on node structure if (nodes.length === 1) { nodeSelection.setSelectionContents(docElement, preNode); range = nodeSelection.getRange(docElement); } else if (parentNode && parentNode.nodeName !== 'LI') { var lasNode = nodeCutter.GetSpliceNode(range, nodes[nodes.length - 1].parentElement); lasNode = isNOU(lasNode) ? preNode : lasNode; nodeSelection.setSelectionText(docElement, preNode, lasNode, 0, (lasNode.nodeType === 3) ? lasNode.textContent.length : lasNode.childNodes.length); range = nodeSelection.getRange(docElement); } // Extract content or clean up nested lists this.extractOrCleanupContent(range, parentNode); // Handle table insertion specially if (insertNode.tagName === 'TABLE') { this.cleanupForTableInsertion(range, editNode); } // Remove original nodes after processing this.removeOriginalNodes(nodes); // Insert node at appropriate location this.insertNodeAtLocation(docElement, sibNode, parentNode, editNode, insertedNode, preNode, insertNode, isCursor, range, enterAction); this.removeEmptyElements(editNode); this.setSelectionAfterInsertion(insertedNode, nodeSelection, docElement); }; // Extracts content or cleans nested lists as required when managing inserts in outer content ranges. InsertHtml.extractOrCleanupContent = function (range, parentNode) { if (range.startContainer.parentElement.closest('ol,ul') !== null && range.endContainer.parentElement.closest('ol,ul') !== null) { nestedListCleanUp(range, parentNode); } else { range.extractContents(); } }; // Performs cleanup operations necessary specifically for cases involving table insertions. InsertHtml.cleanupForTableInsertion = function (range, editNode) { var emptyElement = closest(range.startContainer, 'blockquote'); if (!isNOU(emptyElement) && emptyElement.childNodes.length > 0) { for (var i = emptyElement.childNodes.length - 1; i >= 0; i--) { var currentChild = emptyElement.childNodes[i]; if (!isNOU(currentChild) && currentChild.innerText.trim() === '') { detach(currentChild); } } } this.removeEmptyElements(editNode, false, emptyElement); }; // Removes the original nodes from the document tree after processing insertion operations. InsertHtml.removeOriginalNodes = function (nodes) { for (var index = 0; index < nodes.length; index++) { if (nodes[index].nodeType !== 3 && nodes[index].parentNode != null) { if (nodes[index].nodeName === 'IMG') { continue; } nodes[index].parentNode.removeChild(nodes[index]); } } }; // Directly inserts the node at a calculated location, ensuring appropriate context and order. InsertHtml.insertNodeAtLocation = function (docElement, sibNode, parentNode, editNode, insertedNode, preNode, insertNode, isCursor, range, enterAction) { if (!isNOU(sibNode) && !isNOU(sibNode.parentNode)) { if (docElement.contains(sibNode)) { InsertMethods.AppendBefore(insertedNode, sibNode, true); } else { range.insertNode(insertedNode); } } else { parentNode = this.findAppropriateParentNode(parentNode, editNode); this.insertNodeBasedOnContext(parentNode, editNode, insertedNode, insertNode, isCursor, range, preNode, enterAction); } }; // Identifies an appropriate parent node which accommodates the insertion effectively. InsertHtml.findAppropriateParentNode = function (parentNode, editNode) { var previousNode = null; while (parentNode !== editNode && parentNode.firstChild && (parentNode.textContent.trim() === '') && parentNode.nodeName !== 'LI') { var parentNode1 = parentNode.parentNode; previousNode = parentNode; parentNode = parentNode1; } return previousNode !== null ? previousNode : parentNode; }; // Inserts nodes by considering established contexts like sibling nodes and nested elements. InsertHtml.insertNodeBasedOnContext = function (parentNode, editNode, insertedNode, insertNode, isCursor, range, preNode, enterAction) { if (parentNode.firstChild && (parentNode !== editNode || (insertedNode.nodeName === 'TABLE' && isCursor && parentNode === range.startContainer && parentNode === range.endContainer))) { if (parentNode.textContent.trim() === '' && parentNode !== editNode && parentNode.nodeName === 'LI') { parentNode.appendChild(insertedNode); } else if (parentNode.textContent.trim() === '' && parentNode !== editNode) { if (parentNode.parentNode && parentNode.parentNode === editNode && !this.isBlockElement(insertedNode) && !(enterAction && enterAction.toUpperCase() === 'BR')) { var blockNode = enterAction && enterAction.toUpperCase() === 'DIV' ? createElement('div') : createElement('p'); blockNode.appendChild(insertedNode); InsertMethods.AppendBefore(blockNode, parentNode, false); } else { InsertMethods.AppendBefore(insertedNode, parentNode, false); } detach(parentNode); } else { InsertMethods.AppendBefore(insertedNode, parentNode.firstChild, false); } } else if (isNOU(preNode.previousSibling) && insertNode.tagName === 'TABLE') { parentNode.prepend(insertedNode); } else { parentNode.appendChild(insertedNode); } }; // Configures the node selection state after executing the insertion operation. InsertHtml.setSelectionAfterInsertion = function (insertedNode, nodeSelection, docElement) { if (insertedNode.nodeName === 'IMG') { this.imageFocus(insertedNode, nodeSelection, docElement); } else if (insertedNode.nodeType !== 3) { nodeSelection.setSelectionText(docElement, insertedNode, insertedNode, 0, insertedNode.childNodes.length); } else { nodeSelection.setSelectionText(docElement, insertedNode, insertedNode, 0, insertedNode.textContent.length); } }; // Manages insertion operations when nodes are intended to be placed within the current range selection. InsertHtml.handleContentInsertionInsideRange = function (docElement, range, nodeSelection, closestParentNode, insertedNode, isCursor) { var liElement = !isNOU(closestParentNode) ? closest(closestParentNode, 'li') : null; if (this.shouldInsertInTableCell(closestParentNode, liElement, isCursor)) { range.extractContents(); liElement.appendChild(insertedNode); this.removeEmptyNextLI(liElement); } else { this.insertWithRangeHandling(docElement, range, insertedNode, isCursor); } this.setCursorAfterInsertion(docElement, insertedNode, nodeSelection); }; // Determines if content should be inserted inside a table cell based on the specific conditions. InsertHtml.shouldInsertInTableCell = function (closestParentNode, liElement, isCursor) { return (!isNOU(closestParentNode) && (closestParentNode.nodeName === 'TD' || closestParentNode.nodeName === 'TH')) && !isNOU(liElement) && !isCursor; }; // Handles direct node insertions by accounting for document structure and browser compatibility factors. InsertHtml.insertWithRangeHandling = function (docElement, range, insertedNode, isCursor) { range.deleteContents(); if (isCursor && range.startContainer.textContent === '' && range.startContainer.nodeName !== 'BR') { range.startContainer.innerHTML = ''; } if (Browser.isIE) { var frag = docElement.createDocumentFragment(); frag.appendChild(insertedNode); range.insertNode(frag); } else if (this.isHrElement(range)) { this.insertAfterHrElement(range, insertedNode); } else { this.insertBasedOnStartContainer(range, insertedNode); } }; // Handles direct node insertions by accounting for document structure and browser compatibility factors. InsertHtml.isHrElement = function (range) { return range.startContainer.nodeType === 1 && range.startContainer.nodeName.toLowerCase() === 'hr' && range.endContainer.nodeName.toLowerCase() === 'hr'; }; // Handling inserting after horizontal rule elements. InsertHtml.insertAfterHrElement = function (range, insertedNode) { var paraElem = range.startContainer.nextElementSibling; if (paraElem) { if (paraElem.querySelector('br')) { detach(paraElem.querySelector('br')); } paraElem.appendChild(insertedNode); } }; // Inserts content based on the start container properties and current text structure. InsertHtml.insertBasedOnStartContainer = function (range, insertedNode) { if (range.startContainer.nodeName === 'BR') { range.startContainer.parentElement.insertBefore(insertedNode, range.startContainer); } else { range.insertNode(insertedNode); } }; // Sets the cursor position after completing the content insertion logic. InsertHtml.setCursorAfterInsertion = function (docElement, insertedNode, nodeSelection) { if (insertedNode.nodeType !== 3 && insertedNode.childNodes.length > 0) { nodeSelection.setSelectionText(docElement, insertedNode, insertedNode, 1, 1); } else if (insertedNode.nodeName === 'IMG') { this.imageFocus(insertedNode, nodeSelection, docElement); } else if (insertedNode.nodeType !== 3) { nodeSelection.setSelectionContents(docElement, insertedNode); } else { nodeSelection.setSelectionText(docElement, insertedNode, insertedNode, insertedNode.textContent.length, insertedNode.textContent.length); } }; // Checks whether the editor should scroll to the cursor position after insertion. InsertHtml.shouldScrollToCursor = function (editNode, scrollHeight, insertedNode) { return !isNOU(editNode) && scrollHeight < editNode.scrollHeight && insertedNode.nodeType === 1 && (insertedNode.nodeName === 'IMG' || !isNOU(insertedNode.querySelector('img'))); }; // Removes empty list items from the associated list after node insertions. InsertHtml.removeEmptyNextLI = function (liElement) { // Find the root-level list containing this list item var rootList = closest(liElement, 'ul,ol'); // Navigate to the topmost list if this is inside nested lists while (rootList && rootList.parentElement && rootList.parentElement.nodeName === 'LI') { rootList = closest(rootList.parentElement, 'ul,ol'); } if (!rootList) { return; } // Collect all list items in the list var listItems = rootList.querySelectorAll('li'); // Define a helper to check if a list item is empty (no text and no media elements) var isEmptyListItem = function (item) { return item.textContent.trim() === '' && !item.querySelector('audio,video,img,table,br'); }; // Remove all empty list items listItems.forEach(function (item) { if (isEmptyListItem(item)) { detach(item); } }); }; // Recursively searches for and returns the first text node within the specified node. InsertHtml.findFirstTextNode = function (node) { if (node.nodeType === Node.TEXT_NODE) { return node; } for (var i = 0; i < node.childNodes.length; i++) { var textNode = this.findFirstTextNode(node.childNodes[i]); if (!isNOU(textNode)) { return textNode; } } return null; }; // Handles HTML content pasting operations & insertHTML execCommand while ensuring context-specific adjustments. InsertHtml.pasteInsertHTML = function (nodes, insertedNode, range, nodeSelection, nodeCutter, docElement, isCollapsed, closestParentNode, editNode, enterAction) { var blockElement = this.getImmediateBlockNode(nodes[nodes.length - 1], editNode); if (blockElement && blockElement.textContent.length === 0) { var brElement = blockElement.querySelector('br:last-of-type'); if (brElement) { brElement.classList.add('rte-temp-br'); } } // Initialize key variables and adjust range if needed var isCursor = range.startOffset === range.endOffset && range.startContainer === range.endContainer; range = this.adjustRangeForEmptyEditor(nodes, range, nodeSelection, docElement, editNode, isCursor); // Setup variables for range manipulation var rangeInfo = this.setupRangeForPaste(nodes, insertedNode, range, nodeSelection, nodeCutter, docElement, isCollapsed, closestParentNode, editNode); range = rangeInfo.range; // Process based on content structure var containsBlockNode = this.containsBlockElements(insertedNode); var lastSelectionNode = containsBlockNode ? this.handleBlockNodeContent(nodes, insertedNode, range, nodeCutter, editNode, enterAction, isCollapsed) : this.handleInlineContent(nodes, insertedNode, range, nodeSelection, docElement, editNode, isCursor, rangeInfo.sibNode, rangeInfo.lasNode, rangeInfo.isSingleNode); // Process special cases var processedNode = this.processSpecialNodes(lastSelectionNode, insertedNode, enterAction); // Position cursor appropriately this.positionCursorAfterPaste(processedNode, insertedNode, nodeSelection, docElement, editNode, enterAction); // Final cleanup this.alignCheck(editNode); this.listCleanUp(nodeSelection, docElement); this.removeEmptyBrFromParagraph(editNode); }; // Clean up unnecessary line breaks after paste actions. InsertHtml.removeEmptyBrFromParagraph = function (editNode) { var tempBr = editNode.querySelector('br.rte-temp-br'); if (tempBr) { tempBr.remove(); } }; // Adjusts range settings when the editor is empty, covering cursor initialization aspects. InsertHtml.adjustRangeForEmptyEditor = function (nodes, range, nodeSelection, docElement, editNode, isCursor) { if (isCursor && range.startContainer === editNode && editNode.textContent === '' && range.startOffset === 0 && range.endOffset === 0 && editNode.childNodes[0].tagName !== 'TABLE') { var currentBlockNode = this.getImmediateBlockNode(nodes[nodes.length - 1], editNode); nodeSelection.setSelectionText(docElement, currentBlockNode, currentBlockNode, 0, 0); return nodeSelection.getRange(docElement); } return range; }; // Sets up parameters involving range, sibling nodes, and relevant options for pasting operations. InsertHtml.setupRangeForPaste = function (nodes, insertedNode, range, nodeSelection, nodeCutter, docElement, isCollapsed, closestParentNode, editNode) { var preNode; var sibNode; var lasNode; var isSingleNode = false; if (editNode !== range.startContainer && ((!isCollapsed && !(closestParentNode.nodeType === Node.ELEMENT_NODE && CONSTANT.TABLE_BLOCK_TAGS.indexOf(closestParentNode.tagName.toLocaleLowerCase()) !== -1)) || (insertedNode.nodeName.toLowerCase() === 'table' && closestParentNode && CONSTANT.TABLE_BLOCK_TAGS.indexOf(closestParentNode.tagName.toLocaleLowerCase()) === -1)) && insertedNode.firstChild.nodeName !== 'HR') { preNode = nodeCutter.GetSpliceNode(range, closestParentNode); if (!isNOU(preNode)) { sibNode = isNOU(preNode.previousSibling) ? preNode.parentNode.previousSibling : preNode.previousSibling; if (nodes.length === 1) { nodeSelection.setSelectionContents(docElement, preNode); range = nodeSelection.getRange(docElement); isSingleNode = true; } else { var textContent = nodes[nodes.length - 1].textContent ? nodes[nodes.length - 1].textContent : ''; lasNode = nodeCutter.GetSpliceNode(range, nodes[nodes.length - 1].parentElement); if (lasNode && lasNode.nodeName === 'LI' && lasNode.nextSibling && lasNode.nextSibling.nodeName === 'LI') { this.isAnotherLiFromEndLi = textContent === lasNode.textContent ? false : true; } lasNode = isNOU(lasNode) ? preNode : lasNode; nodeSelection.setSelectionText(docElement, preNode, lasNode, 0, (lasNode.nodeType === 3) ? lasNode.textContent.length : lasNode.childNodes.length); range = nodeSelection.getRange(docElement); isSingleNode = false; } } } // Clean node content this.removingComments(insertedNode); return { preNode: preNode, sibNode: sibNode, lasNode: lasNode, isSingleNode: isSingleNode, range: range }; }; // Examines whether the inserted node contains block element. InsertHtml.containsBlockElements = function (insertedNode) { var allChildNodes = insertedNode.childNodes; for (var i = 0; i < allChildNodes.length; i++) { if (CONSTANT.BLOCK_TAGS.indexOf(allChildNodes[i].nodeName.toLowerCase()) >= 0) { return true; } } return false; }; // Processes inline-only content during paste operations for correct insertion. InsertHtml.handleInlineContent = function (nodes, insertedNode, range, nodeSelection, docElement, editNode, isCursor, sibNode, lasNode, isSingleNode) { var fragment = document.createDocumentFragment(); if (!isCursor) { return this.handleRegularInlineContent(insertedNode, range, fragment, editNode, sibNode, lasNode, isSingleNode); } else { return this.handleCursorInlineContent(nodes, insertedNode, range, nodeSelection, docElement, editNode, fragment); } }; // Handles paste operations when dealing with non-collapsed inline selections. InsertHtml.handleRegularInlineContent = function (insertedNode, range, fragment, editNode, sibNode, lasNode, isSingleNode) { var lastSelectionNode; while (insertedNode.firstChild) { lastSelectionNode = insertedNode.firstChild; fragment.appendChild(insertedNode.firstChild); } if (isSingleNode) { range.deleteContents(); this.removeEmptyElements(editNode, true); range.insertNode(fragment); } else { var startContainerParent = editNode === range.startContainer ? range.startContainer : range.startContainer.parentNode; var startIndex = Array.prototype.indexOf.call(startContainerParent.childNodes, (Browser.userAgent.indexOf('Firefox') !== -1 && editNode === range.startContainer) ? range.startContainer.firstChild : range.startContainer); range.deleteContents(); if (startIndex !== -1) { range.setStart(startContainerParent, startIndex); range.setEnd(startContainerParent, startIndex); } if (!isNOU(lasNode) && lasNode !== editNode) { detach(lasNode); this.removeEmptyElements(editNode, true); } if (!isNOU(sibNode)) { if (sibNode.parentNode === editNode) { sibNode.appendChild(fragment); } else { sibNode.parentNode.appendChild(fragment); } } else { range.insertNode(fragment); } } return lastSelectionNode; }; // Handles content insertion when the cursor is placed in an inline context without initial selection. InsertHtml.handleCursorInlineContent = function (nodes, insertedNode, range, nodeSelection, docElement, editNode, fragment) { var lastSelectionNode; var immediateBlockNode = this.getImmediateBlockNode(range.startContainer, editNode); var tempSpan = createElement('span', { className: 'tempSpan' }); if (this.shouldInsertInAnchor(range, nodes)) { this.insertInAnchor(range, tempSpan, editNode); } else if (this.isMentionChip(nodes)) { range.startContainer.parentElement.insertAdjacentElement('afterend', tempSpan); } else if (range.startOffset !== 0 && range.endOffset !== 0 && range.startOffset === range.endOffset && !insertedNode.querySelector('a') && range.endOffset === range.startContainer.textContent.length) { immediateBlockNode.appendChild(tempSpan); } else { range.insertNode(tempSpan); } while (insertedNode.firstChild) { lastSelectionNode = insertedNode.firstChild; fragment.appendChild(insertedNode.firstChild); } return this.insertFragmentOrReplaceNode(tempSpan, fragment, range, nodeSelection, document, lastSelectionNode); }; //Determines if content should be inserted within an anchor element based on specified conditions. InsertHtml.shouldInsertInAnchor = function (range, nodes) { var nearestAnchor = closest(range.startContainer.parentElement, 'a'); return range.startContainer.nodeType === 3 && !isNOU(nearestAnchor) && !isNOU(closest(nearestAnchor, 'span')); }; // Specifically inserts nodes inside an anchor tag if conditions are met during paste. InsertHtml.insertInAnchor = function (range, tempSpan, editNode) { var immediateBlockNode = this.getImmediateBlockNode(range.startContainer, editNode); if (immediateBlockNode.querySelectorAll('br').length > 0) { detach(immediateBlockNode.querySelector('br')); } var rangeElement = closest(closest(range.startContainer.parentElement, 'a'), 'span'); rangeElement.appendChild(tempSpan); }; // Checks if the node includes a mentions chip for handling special paste scenarios. InsertHtml.isMentionChip = function (nodes) { return nodes[0] && nodes[0].nodeName === '#text' && nodes[0].nodeValue.includes('\u200B') && !isNOU(nodes[0].parentElement) && !isNOU(nodes[0].parentElement.previousElementSibling) && nodes[0].parentElement.previousElementSibling.classList.contains('e-mention-chip'); }; // Inserts a document fragment at a temporary span position or replaces a specific node. InsertHtml.insertFragmentOrReplaceNode = function (tempSpan, fragment, range, nodeSelection, docElement, lastSelectionNode) { var matchedElement = this.getClosestMatchingElement(tempSpan.parentNode, fragment); if (fragment.childNodes.length === 1 && fragment.firstChild && matchedElement) { return this.replaceWithMatchedContent(tempSpan, matchedElement, fragment, range, nodeSelection, docElement, lastSelectionNode); } else { tempSpan.parentNode.replaceChild(fragment, tempSpan); return lastSelectionNode; } }; // Replaces the temporary node with matched content, adjusting text nodes if required. InsertHtml.replaceWithMatchedContent = function (tempSpan, matchedElement, fragment, range, nodeSelection, docElement, lastSelectionNode) { var wrapperDiv = document.createElement('div'); var text = fragment.firstChild.textContent || ''; wrapperDiv.innerHTML = fragment.firstChild.innerHTML || ''; var replacementNode = wrapperDiv.firstChild; var result = lastSelectionNode; if (replacementNode) { matchedElement.replaceChild(replacementNode, tempSpan); if (matchedElement.parentNode && replacementNode.nodeType === Node.TEXT_NODE && this.shouldNormalizeTextNodes(replacementNode)) { matchedElement.parentNode.normalize(); var startOffset = range.startOffset + text.length; nodeSelection.setCursorPoint(docElement, matchedElement.firstChild, startOffset); result = null; } } wrapperDiv.remove(); return result; }; // Determines if text node normalization is necessary after a paste operation. InsertHtml.shouldNormalizeTextNodes = function (node) { return (node.previousSibling && node.previousSibling.nodeType === Node.TEXT_NODE) || (node.nextSibling && node.nextSibling.nodeType === Node.TEXT_NODE); }; // Manages block node insertion during paste operations to align with document structure. InsertHtml.handleBlockNodeContent = function (nodes, insertedNode, range, nodeCutter, editNode, enterAction, isCollapsed) { var parentElem = this.findParentPreElement(range, editNode); if (!isNOU(insertedNode) && !isNOU(parentElem) && parentElem.nodeName === 'PRE') { range.insertNode(insertedNode); return insertedNode.lastChild; } else { return this.processBlockContent(nodes, insertedNode, range, nodeCutter, editNode, enterAction, isCollapsed); } }; // Finds the nearest parent PRE element starting from the current range container. InsertHtml.findParentPreElement = function (range, editNode) { var parentElem = range.startContainer; while (!isNOU(parentElem) && parentElem.nodeName !== 'PRE' && parentElem !== editNode) { parentElem = parentElem.parentElement; } return parentElem; }; /* Processes the inserted nodes, preserving initial nodes until first block element, then wrapping inline nodes between blocks with appropriate container elements */ InsertHtml.processInlineNodesBetweenBlocks = function (insertedNode, enterAction) { var fragment = document.createDocumentFragment(); var foundFirstBlock = false; var currentGroup = null; var lastNode = null; var tempElement = createElement('div', { id: 'pasteContent_rte' }); while (insertedNode.firstChild) { var currentNode = insertedNode.firstChild; // Skip empty text nodes if (currentNode.nodeName === '#text' && currentNode.textContent.trim() === '') { detach(currentNode); continue; } // Keep track of last processed node lastNode = currentNode; // Check if this is a block element var isBlockNode = this.isBlockElement(currentNode); if (!foundFirstBlock) { // Before first block is encountered, preserve original structure if (isBlockNode) { // First block found, change mode foundFirstBlock = true; fragment.appendChild(currentNode); } else { tempElement.appendChild(currentNode); fragment.appendChild(tempElement); } } else { // After first block, apply wrapping logic if (isBlockNode) { // Add block elements directly, close any open group currentGroup = null; fragment.appendChild(currentNode); } else { // Wrap inline/text nodes if (!currentGroup) { // Create new wrapper if needed currentGroup = enterAction === 'DIV' ? createElement('div') : createElement('p'); fragment.appendChild(currentGroup); } // Add to current group currentGroup.appendChild(currentNode); } } } return { fragment: fragment, lastNode: lastNode }; }; // Checks whether the given node is a block element. InsertHtml.isBlockElement = function (node) { if (node.nodeType !== Node.ELEMENT_NODE) { return false; } var blockTags = CONSTANT.BLOCK_TAGS; var nodeName = node.nodeName.toLowerCase(); for (var i = 0; i < blockTags.length; i++) { if (blockTags[i] === nodeName) { return true; } } return false; }; // Processes block elements during insertion, wrapping and positioning elements as needed. InsertHtml.processBlockContent = function (nodes, insertedNode, range, nodeCutter, editNode, enterAction, isCollapsed) { var lastSelectionNode = null; var insertedFragment = this.processInlineNodesBetweenBlocks(insertedNode, enterAction); // Insert a temporary node and get ready to process content lastSelectionNode = this.insertTempNode(range, insertedFragment.fragment, nodes, nodeCutter, editNode); // Delete existing contents if needed if (!this.contentsDeleted) { this.cleanupBeforeBlockInsertion(range, editNode, isCollapsed); } var inlineNodeWrapper = editNode.querySelector('#pasteContent_rte'); if (!isNOU(inlineNodeWrapper)) { this.processFirstInlineNodeSet(inlineNodeWrapper, enterAction); } return lastSelectionNode; }; // Performs necessary cleanup actions prior to block element insertion, like removing empties. InsertHtml.cleanupBeforeBlockInsertion = function (range, editNode, isCollapsed) { if (!isCollapsed && range.startContainer.parentElement.textContent.length === 0 && range.startContainer.nodeName === 'BR' && range.startContainer.parentElement.nodeName === 'P') { editNode.removeChild(range.startContainer.parentElement); } range.deleteContents(); this.removeEmptyElements(editNode); }; // Processes and adjusts the first set of inline nodes before any block. InsertHtml.processFirstInlineNodeSet = function (insertedNode, enterAction) { var lastSelectionNode; while (insertedNode.firstChild) { lastSelectionNode = insertedNode.firstChild; if (this.isInlineElement(lastSelectionNode)) { lastSelectionNode = this.handleFirstBlockChild(insertedNode, enterAction); } else { break; // Prevent infinite loop } } detach(insertedNode); return lastSelectionNode; }; // Moves the first set of inline nodes to the previous block element a block. InsertHtml.handleFirstBlockChild = function (insertedNode, enterAction) { var firstChild = insertedNode.firstChild; // Ensure there's a previous element sibling if (isNOU(insertedNode.previousElementSibling)) { var firstParaElm = enterAction === 'DIV' ? createElement('div') : createElement('p'); insertedNode.parentElement.insertBefore(firstParaElm, insertedNode); } // Insert based on previous sibling type if (insertedNode.previousElementSibling.nodeName === 'BR') { insertedNode.parentElement.insertBefore(insertedNode.firstChild, insertedNode); } else { insertedNode.previousElementSibling.appendChild(insertedNode.firstChild); } return firstChild; }; // Checks if a given node is an inline node. InsertHtml.isInlineElement = function (node) { return node.nodeName === '#text' || (this.inlineNode.indexOf(node.nodeName.toLowerCase()) >= 0); }; // Handles special cases in node structures that require custom processing post-insertion. InsertHtml.processSpecialNodes = function (lastSelectionNode, insertedNode, enterAction) { if (!lastSelectionNode) { return null; } // Handle Google Sheets HTML if (lastSelectionNode instanceof Element && lastSelectionNode.nodeName === 'GOOGLE-SHEETS-HTML-ORIGIN') { return this.processGoogleSheetsTable(lastSelectionNode); } // Handle table nodes to insert paragraphs after tables if there is no content after table. if (lastSelectionNode.nodeName === 'TABLE') { return this.addParagraphAfterTable(lastSelectionNode, enterAction); } return lastSelectionNode; }; // Processes table nodes that originate from Google Sheets for alignment adjustments. InsertHtml.processGoogleSheetsTable = function (node) { var tableEle = node.querySelector('table'); var colGroup = tableEle.querySelector('colgroup'); if (colGroup) { for (var i = 0; i < tableEle.rows.length; i++) { for (var k = 0; k < tableEle.rows[i].cells.length; k++) { var col = colGroup.querySelectorAll('col')[k]; if (col && col.hasAttribute('width')) { var width = col.getAttribute('width'); tableEle.rows[i].cells[k].style.width = width + 'px'; } } } } return node; }; // Inserts a paragraph after a table node to ensure continuity in the document. InsertHtml.addParagraphAfterTable = function (tableNode, enterAction) { var pTag = createElement(enterAction === 'DIV' ? 'div' : 'p'); pTag.appendChild(createElement('br')); tableNode.parentElement.insertBefore(pTag, tableNode.nextSibling); return pTag; }; // Positions the editor cursor appropriately after completing a paste operation. InsertHtml.positionCursorAfterPaste = function (lastSelectionNode, insertedNode, nodeSelection, docElement, editNode, enterAction) { if (!lastSelectionNode) { return; } if (lastSelectionNode.nodeName === '#text') { this.placeCursorEnd(lastSelectionNode, insertedNode, nodeSelection, docElement, editNode); } else if (lastSelectionNode.nodeName === 'HR') { this.handleHRElementCursor(lastSelectionNode, nodeSelection, docElement, enterAction); } else if (editNode.contains(lastSelectionNode) && isNOU(editNode.querySelector('.paste-cursor'))) { this.cursorPos(lastSelectionNode, insertedNode, nodeSelection, docElement, editNode); } else { this.handleListElementCursor(insertedNode, editNode, nodeSelection, docElement); } }; InsertHtml.handleListElementCursor = function (insertedNode, editNode, nodeSelection, docElement) { var cursorElm = editNode.querySelector('.paste-cursor'); if (!isNOU(cursorElm)) { nodeSelection.setCursorPoint(docElement, cursorElm, 0); cursorElm.remove(); } else { var nodeList = editNode.querySelectorAll('.pasteContent_RTE'); if (nodeList.length > 0) { var lastElement = nodeList[nodeList.length - 1]; this.cursorPos(lastElement, insertedNode, nodeSelection, docElement, editNode); } } }; // Handles cursor placement after inserting horizontal rule elements in the document. InsertHtml.handleHRElementCursor = function (lastSelectionNode, nodeSelection, docElement, enterAction) { var nextSiblingNode = lastSelectionNode.nextSibling; while (nextSiblingNode && nextSiblingNode.nodeName === '#text' && nextSiblingNode.textContent.trim() === '') { nextSiblingNode = nextSiblingNode.nextSibling; } var siblingTag = createElement(enterAction === 'DIV' ? 'div' : 'p'); siblingTag.appendChild(createElement('br')); var parentNode = lastSelectionNode.parentNode; if (nextSiblingNode && (nextSiblingNode.nodeName === 'HR' || nextSiblingNode.nodeName === 'TABLE')) { parentNode.insertBefore(siblingTag, nextSiblingNode); lastSelectionNode = siblingTag; } else if (parentNode && parentNode.nodeName === 'LI') { var currentNode = lastSelectionNode.nextSibling; // Traverse through siblings of the <hr> to find a valid non-empty node while (currentNode && (currentNode.nodeType === Node.TEXT_NODE && currentNode.textContent.trim() === '')) { currentNode = currentNode.nextSibling; } // If no valid sibling is found, move up to the parent and check for the parent'