@syncfusion/ej2-richtexteditor
Version:
Essential JS 2 RichTextEditor component
934 lines (932 loc) • 118 kB
JavaScript
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, '&$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'