UNPKG

@syncfusion/ej2-richtexteditor

Version:
1,187 lines (1,186 loc) 47.6 kB
/** * Defines common util methods used by Rich Text Editor. */ import { isNullOrUndefined, Browser, removeClass, closest, createElement, detach } from '@syncfusion/ej2-base'; import { CLS_AUD_FOCUS, CLS_IMG_FOCUS, CLS_RESIZE, CLS_RTE_DRAG_IMAGE, CLS_TABLE_MULTI_CELL, CLS_TABLE_SEL, CLS_TABLE_SEL_END, CLS_VID_FOCUS } from './constant'; import { IsFormatted } from '../editor-manager/plugin/isformatted'; /** * @returns {boolean} - returns boolean value * @hidden */ export function isIDevice() { var result = false; if (Browser.isDevice && Browser.isIos) { result = true; } return result; } /** * @param {Element} editableElement - specifies the editable element. * @param {string} selector - specifies the string values. * @returns {void} * @hidden */ export function setEditFrameFocus(editableElement, selector) { if (editableElement.nodeName === 'BODY' && !isNullOrUndefined(selector)) { var iframe = top.window.document.querySelector(selector); if (!isNullOrUndefined(iframe)) { iframe.contentWindow.focus(); } } } /** * @param {string} value - specifies the string value * @returns {void} * @hidden */ export function updateTextNode(value) { var resultElm = document.createElement('div'); resultElm.innerHTML = value; var tableElm = resultElm.querySelectorAll('table'); for (var i = 0; i < tableElm.length; i++) { if (tableElm[i].classList.length > 0 && !tableElm[i].classList.contains('e-rte-table') && !tableElm[i].classList.contains('e-rte-custom-table')) { tableElm[i].classList.add('e-rte-paste-table'); if (tableElm[i].classList.contains('e-rte-paste-word-table')) { tableElm[i].classList.remove('e-rte-paste-word-table'); continue; // Skiping the removal of the border if the source is from word. } else if (tableElm[i].classList.contains('e-rte-paste-excel-table')) { tableElm[i].classList.remove('e-rte-paste-excel-table'); if (tableElm[i].getAttribute('border') === '0') { tableElm[i].removeAttribute('border'); } var tdElm = tableElm[i].querySelectorAll('td'); for (var j = 0; j < tdElm.length; j++) { if (tdElm[j].style.borderLeft === 'none') { tdElm[j].style.removeProperty('border-left'); } if (tdElm[j].style.borderRight === 'none') { tdElm[j].style.removeProperty('border-right'); } if (tdElm[j].style.borderBottom === 'none') { tdElm[j].style.removeProperty('border-bottom'); } if (tdElm[j].style.borderTop === 'none') { tdElm[j].style.removeProperty('border-top'); } if (tdElm[j].style.border === 'none') { tdElm[j].style.removeProperty('border'); } } } else if (tableElm[i].classList.contains('e-rte-paste-onenote-table')) { tableElm[i].classList.remove('e-rte-paste-onenote-table'); continue; } else if (tableElm[i].classList.contains('e-rte-paste-html-table')) { var currentTable = tableElm[i]; currentTable.classList.remove('e-rte-paste-html-table'); if (!currentTable.classList.contains('e-rte-table-border')) { var cells = currentTable.querySelectorAll('td, th'); var tableStyle = currentTable.getAttribute('style'); var hasBorder = tableStyle && tableStyle.includes('border'); if (!hasBorder) { for (var _i = 0, _a = Array.from(cells); _i < _a.length; _i++) { var cell = _a[_i]; var cellStyle = cell.getAttribute('style'); if (cellStyle && cellStyle.includes('border')) { hasBorder = true; break; } } } if (!hasBorder) { currentTable.classList.add('e-rte-table'); currentTable.classList.remove('e-rte-paste-table'); } } continue; } } } var imageElm = resultElm.querySelectorAll('img'); for (var i = 0; i < imageElm.length; i++) { if (imageElm[i].classList.contains('e-rte-image-unsupported')) { continue; // Should not add the class if the image is Broken. } if (!imageElm[i].classList.contains('e-rte-image')) { imageElm[i].classList.add('e-rte-image'); } if (!(imageElm[i].classList.contains('e-imginline') || imageElm[i].classList.contains('e-imgbreak'))) { imageElm[i].classList.add('e-imginline'); } } return resultElm.innerHTML; } /** * @param {Node} startChildNodes - specifies the node * @returns {void} * @hidden */ export function getLastTextNode(startChildNodes) { var finalNode = startChildNodes; do { if (finalNode.childNodes.length > 0) { finalNode = finalNode.childNodes[0]; } } while (finalNode.childNodes.length > 0); return finalNode; } /** * @returns {void} * @hidden */ export function getDefaultHtmlTbStatus() { return { bold: false, italic: false, subscript: false, superscript: false, strikethrough: false, orderedlist: false, unorderedlist: false, numberFormatList: false, bulletFormatList: false, underline: false, alignments: null, lineHeight: null, backgroundcolor: null, fontcolor: null, fontname: null, fontsize: null, formats: null, createlink: false, insertcode: false, blockquote: false, inlinecode: false, isCodeBlock: false, isCheckList: false }; } /** * @returns {void} * @hidden */ export function getDefaultMDTbStatus() { return { bold: false, italic: false, subscript: false, superscript: false, strikethrough: false, orderedlist: false, uppercase: false, lowercase: false, inlinecode: false, unorderedlist: false, formats: null }; } /** * Checks if the node has any formatting * * @param {Node} node - specifies the node. * @param {IsFormatted} isFormatted - specifies the IsFormatted instance. * @returns {boolean} - returns whether the node has any formatting */ export function hasAnyFormatting(node, isFormatted) { if (isFormatted === void 0) { isFormatted = null; } if (!node) { return false; } var nodeName = node.nodeName.toUpperCase(); if (['TABLE', 'IMG', 'VIDEO', 'AUDIO'].indexOf(nodeName) !== -1) { return false; } if (node.nodeType === Node.ELEMENT_NODE && node.hasAttribute('style')) { return true; } if (!isFormatted) { isFormatted = new IsFormatted(); } var semanticFormats = [ 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'fontcolor', 'fontname', 'fontsize', 'backgroundcolor', 'inlinecode' ]; for (var _i = 0, semanticFormats_1 = semanticFormats; _i < semanticFormats_1.length; _i++) { var format = semanticFormats_1[_i]; if (isFormatted.isFormattedNode(node, format)) { return true; } } for (var i = 0; i < node.childNodes.length; i++) { if (hasAnyFormatting(node.childNodes[i], isFormatted)) { return true; } } return false; } /** * @param {Range} range - specifies the range * @param {Node} parentNode - specifies the parent node * @returns {void} * @hidden */ export function nestedListCleanUp(range, parentNode) { if (range.startContainer.parentElement.closest('ol,ul') !== null && range.endContainer.parentElement.closest('ol,ul') !== null) { range.extractContents(); var liElem = (range.startContainer.nodeName === '#text' ? range.startContainer.parentElement : range.startContainer).querySelectorAll('li'); if (liElem.length > 0) { liElem.forEach(function (item) { if (!isNullOrUndefined(item.firstChild) && (item.firstChild.nodeName === 'OL' || item.firstChild.nodeName === 'UL')) { item.style.listStyleType = 'none'; } if (item.innerHTML.trim() === '' && item !== parentNode) { item.remove(); } var parentLi = parentNode.nodeName === 'LI' ? parentNode : closest(parentNode, 'li'); // Only remove if the list item is empty and not the parent's list item if (item.textContent.trim() === '' && item !== parentLi) { item.remove(); } }); } } } /** * Method to scroll the content to the cursor position * * @param {Document} document - specifies the document. * @param {HTMLElement | HTMLBodyElement} inputElement - specifies the input element. * @returns {void} */ export function scrollToCursor(document, inputElement) { var rootElement = inputElement.nodeName === 'BODY' ? inputElement.ownerDocument.defaultView.frameElement.closest('.e-richtexteditor') : inputElement.closest('.e-richtexteditor'); var height = rootElement.style.height; if (document.getSelection().rangeCount === 0) { return; } var range = document.getSelection().getRangeAt(0); var finalFocusElement = range.startContainer.nodeName === '#text' ? range.startContainer.parentElement : range.startContainer; var rect = finalFocusElement.getBoundingClientRect(); var cursorTop = rect.top; var cursorBottom = rect.bottom; var rootRect = rootElement.getBoundingClientRect(); var hasMargin = rootElement.querySelectorAll('.e-count-enabled, .e-resize-enabled').length > 0; if (inputElement.nodeName === 'BODY') { if (height === 'auto') { if (window.innerHeight < cursorTop) { finalFocusElement.scrollIntoView({ block: 'end', inline: 'nearest' }); } else if (cursorBottom > finalFocusElement.getBoundingClientRect().top) { finalFocusElement.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } } else { if (cursorTop > inputElement.getBoundingClientRect().height || cursorBottom > rootRect.bottom) { finalFocusElement.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } if (cursorBottom > finalFocusElement.getBoundingClientRect().top) { finalFocusElement.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } } } else { if (height === 'auto') { if (window.innerHeight < cursorTop) { finalFocusElement.scrollIntoView({ block: 'end', inline: 'nearest' }); } if (cursorTop > finalFocusElement.getBoundingClientRect().top) { finalFocusElement.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } } else { if (cursorBottom > finalFocusElement.getBoundingClientRect().top) { finalFocusElement.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } if (cursorBottom > rootRect.bottom) { rootElement.querySelector('.e-rte-content').scrollTop += (cursorBottom - rootRect.bottom) + (hasMargin ? 20 : 0); } } } var scrollVal = inputElement.closest('div[style*="overflow-y: scroll"]'); if (!isNullOrUndefined(scrollVal)) { var parentRect = scrollVal.getBoundingClientRect(); if (cursorBottom > parentRect.bottom) { scrollVal.scrollTop += (cursorBottom - parentRect.bottom); } } } /** * Inserts items at a specific index in an array. * * @template T * @param {Array<T>} oldArray - Specifies the old array. * @param {Array<T>} newArray - Specifies the elements to insert. * @param {number} indexToInsert - Specifies the index to insert. * @returns {Array<T>} - Returns the array after inserting the elements. */ export function insertItemsAtIndex(oldArray, newArray, indexToInsert) { // This is a work around for ES6 ...spread operator usage. // Usecase: When a new array is inserted into an existing array at a specific index. for (var i = 0; i < newArray.length; i++) { if (i === 0) { oldArray.splice(indexToInsert + i, 1, newArray[i]); } else { oldArray.splice(indexToInsert + i, 0, newArray[i]); } } return oldArray; } /** * Wrapper function to remove a class from the element and remove the attribute if the class is empty. * * @param {Element[]|NodeList} elements - An array of elements that need to remove a list of classes * @param {string|string[]} classes - String or array of string that need to add an individual element as a class * * @returns {Element[]|NodeList} - Returns the array of elements after removing the class. * @private */ export function removeClassWithAttr(elements, classes) { removeClass(elements, classes); for (var i = 0; i < elements.length; i++) { if (elements[i].classList.length === 0 && elements[i].hasAttribute('class')) { elements[i].removeAttribute('class'); } } return elements; } /** * Creates a two-dimensional array mapping the logical structure of a table. * * @private * @param {HTMLTableElement} table - The HTMLTableElement to process. * @returns {Array.<Array.<HTMLElement>>} A 2D matrix of table cells accounting for colspan and rowspan. * @hidden */ export function getCorrespondingColumns(table) { var elementArray = []; var allRows = table.rows; for (var i = 0; i <= allRows.length - 1; i++) { var currentRow = allRows[i]; var columnIndex = 0; for (var j = 0; j <= currentRow.children.length - 1; j++) { var currentCell = currentRow.children[j]; var cellColspan = parseInt(currentCell.getAttribute('colspan'), 10) || 1; var cellRowspan = parseInt(currentCell.getAttribute('rowspan'), 10) || 1; columnIndex = mapCellToMatrixPositions(elementArray, currentCell, i, columnIndex, cellColspan, cellRowspan); columnIndex += cellColspan; } } return elementArray; } /** * Maps a cell to all its positions in the logical table matrix. * * @param {Array.<Array.<HTMLElement>>} matrix - The 2D matrix being constructed. * @param {HTMLElement} cell - The current cell being placed. * @param {number} startRow - The row index where the cell starts. * @param {number} startCol - The column index where the cell starts. * @param {number} colspan - The number of columns the cell spans. * @param {number} rowspan - The number of rows the cell spans. * @returns {number} - The adjusted starting column index for the next cell in the row. * @hidden */ export function mapCellToMatrixPositions(matrix, cell, startRow, startCol, colspan, rowspan) { for (var rowIndex = startRow; rowIndex < startRow + rowspan; rowIndex++) { if (!matrix[rowIndex]) { matrix[rowIndex] = []; } for (var colIndex = startCol; colIndex < startCol + colspan; colIndex++) { if (matrix[rowIndex][colIndex]) { startCol++; } else { matrix[rowIndex][colIndex] = cell; } } } return startCol; } /** * Finds the position of a specific cell element in the table matrix. * * @param {HTMLElement} cell - The HTML element to find in the table * @param {Array.<Array.<HTMLElement>>} allCells - The 2D array representing the table structure * @returns {number[]} An array containing the row and column indices [rowIndex, columnIndex], or empty array if not found * @hidden */ export function getCorrespondingIndex(cell, allCells) { for (var i = 0; i < allCells.length; i++) { for (var j = 0; j < allCells[i].length; j++) { if (allCells[i][j] === cell) { return [i, j]; } } } return []; } /** * Inserts a <colgroup> with calculated sizes to the table. * This function analyzes the table structure and adds appropriate column definitions * with width values based on the current table layout. * * @param {HTMLTableElement} curTable - The table element to add colgroup to table. * @param {boolean} hasUpdate - Flag indicating whether to update existing colgroup (default: false) * @returns {void} * @hidden */ export function insertColGroupWithSizes(curTable, hasUpdate) { if (hasUpdate === void 0) { hasUpdate = false; } if (!curTable) { return; } var colGroup = getColGroup(curTable); if (!colGroup || hasUpdate) { var cellCount = getMaxCellCount(curTable); var sizes = new Array(cellCount); var colGroupEle = createElement('colgroup'); var rowSpanCells = new Map(); for (var i = 0; i < curTable.rows.length; i++) { var currentColIndex = 0; for (var k = 0; k < curTable.rows[i].cells.length; k++) { for (var l = 1; l < curTable.rows[i].cells[k].rowSpan; l++) { var key = '' + (i + l) + currentColIndex; rowSpanCells.set(key, curTable.rows[i].cells[k]); } var cellIndex = getCellIndex(rowSpanCells, i, k); if (cellIndex > currentColIndex) { currentColIndex = cellIndex; } var width = curTable.rows[i].cells[k].offsetWidth; if (!sizes[currentColIndex] || width < sizes[currentColIndex]) { sizes[currentColIndex] = width; } currentColIndex += 1 + curTable.rows[i].cells[k].colSpan - 1; } } for (var size = 0; size < sizes.length; size++) { var cell = createElement('col'); cell.appendChild(createElement('br')); cell.style.width = convertPixelToPercentage(sizes[size], parseInt(getComputedStyle(curTable).width, 10)) + '%'; colGroupEle.appendChild(cell); } if (hasUpdate) { var colGroup_1 = getColGroup(curTable); if (colGroup_1) { detach(colGroup_1); } } curTable.insertBefore(colGroupEle, curTable.firstChild); for (var rowIndex = 0; rowIndex < curTable.rows.length; rowIndex++) { var row = curTable.rows[rowIndex]; for (var cellIndex = 0; cellIndex < row.cells.length; cellIndex++) { var cell = row.cells[cellIndex]; cell.style.width = ''; } } if (isNullOrUndefined(curTable.style.width) || curTable.style.width === '') { curTable.style.width = curTable.offsetWidth + 'px'; } } } /** * Gets the colgroup element from a table * * @param {HTMLTableElement} table - The table element to search in * @returns {HTMLTableColElement | null} The colgroup element or null if not found * @hidden */ export function getColGroup(table) { if (!table || !table.children) { return null; } var colGroup = Array.from(table.children).find(function (child) { return child.tagName === 'COLGROUP'; }); return colGroup || null; } /** * Gets the maximum cell count in a table, accounting for colspan attributes. * This function calculates the effective number of columns by examining all rows * and considering the colspan attribute of each cell. * * @param {HTMLTableElement} table - The table element to analyze * @returns {number} - The maximum number of cells/columns in the table * @hidden */ export function getMaxCellCount(table) { if (!table || !table.rows || table.rows.length === 0) { return 0; } var cellColl = table.rows[0].cells; var cellCount = 0; for (var cell = 0; cell < cellColl.length; cell++) { cellCount += cellColl[cell].colSpan; } return cellCount; } /** * Recursively finds the correct column index for a cell, accounting for rowspan cells. * This function adjusts the column index by checking if there are any rowspan cells * from previous rows that occupy the current position. * * @param {Map<string, HTMLTableDataCellElement>} rowSpanCells - Map of rowspan cells with their positions * @param {number} rowIndex - Current row index * @param {number} colIndex - Initial column index to check * @returns {number} - The adjusted column index accounting for rowspan cells * @hidden */ export function getCellIndex(rowSpanCells, rowIndex, colIndex) { var cellKey = "" + rowIndex + colIndex; var spannedCell = rowSpanCells.get(cellKey); if (spannedCell) { return getCellIndex(rowSpanCells, rowIndex, colIndex + spannedCell.colSpan); } else { return colIndex; } } /** * Converts a pixel measurement to a percentage relative to a container's width. * Used to maintain proper proportions when splitting cells. * * @param {number} value - The pixel value to convert * @param {number} offsetValue - The container width in pixels * @returns {number} The equivalent percentage value * @hidden */ export function convertPixelToPercentage(value, offsetValue) { // Avoid division by zero if (offsetValue === 0) { return 0; } return (value / offsetValue) * 100; } /** * @param {string} value - specifies the string value * @param {string} editorMode - specifies the string value * @returns {string} - returns the string value * @hidden */ export function resetContentEditableElements(value, editorMode) { if (editorMode && editorMode === 'HTML' && value) { var valueElementWrapper = document.createElement('div'); valueElementWrapper.innerHTML = value.trim(); valueElementWrapper.querySelectorAll('.e-img-inner').forEach(function (el) { el.setAttribute('contenteditable', 'true'); }); value = valueElementWrapper.innerHTML; valueElementWrapper.remove(); } return value; } /** * @param {string} value - specifies the string value * @param {string} editorMode - specifies the string value * @returns {string} - returns the string value * @hidden */ export function cleanupInternalElements(value, editorMode) { if (value && editorMode) { var valueElementWrapper = document.createElement('div'); if (editorMode === 'HTML') { valueElementWrapper.innerHTML = value; valueElementWrapper.querySelectorAll('.e-img-inner').forEach(function (el) { el.setAttribute('contenteditable', 'false'); }); var item = valueElementWrapper.querySelectorAll('.e-column-resize, .e-row-resize, .e-table-box, .e-table-rhelper, .e-img-resize, .e-vid-resize, .e-tb-row-insert, .e-tb-col-insert'); if (item.length > 0) { for (var i = 0; i < item.length; i++) { detach(item[i]); } } removeSelectionClassStates(valueElementWrapper); } else { valueElementWrapper.textContent = value; } return (editorMode === 'Markdown') ? valueElementWrapper.innerHTML.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&') : valueElementWrapper.innerHTML; } return value; } /** * @param {HTMLElement} element - specifies the element * @returns {void} * @hidden */ export function removeSelectionClassStates(element) { var classNames = [CLS_IMG_FOCUS, CLS_TABLE_SEL, CLS_TABLE_MULTI_CELL, CLS_TABLE_SEL_END, CLS_VID_FOCUS, CLS_AUD_FOCUS, CLS_RESIZE, CLS_RTE_DRAG_IMAGE]; for (var i = 0; i < classNames.length; i++) { var item = element.querySelectorAll('.' + classNames[i]); removeClass(item, classNames[i]); if (item.length === 0) { continue; } for (var j = 0; j < item.length; j++) { if (item[j].classList.length === 0) { item[j].removeAttribute('class'); } if ((item[j].nodeName === 'IMG' || item[j].nodeName === 'VIDEO') && item[j].style.outline !== '') { item[j].style.outline = ''; } } } element.querySelectorAll('[class=""]').forEach(function (el) { el.removeAttribute('class'); }); } /** * Processes the given inner HTML value and returns a structured HTML string. * * @param {string} innerValue - The inner HTML content to be processed. * @param {string} enterKey - The key used for inserting line breaks. * @param {boolean} enableHtmlEncode - A flag indicating whether HTML encoding should be enabled. * @returns {string} - The structured HTML string. */ export function getStructuredHtml(innerValue, enterKey, enableHtmlEncode) { // Early return for special cases if (enableHtmlEncode || enterKey.toLowerCase() === 'br' || isNullOrUndefined(innerValue)) { return innerValue; } // Create a safe wrapper element for HTML manipulation var tempDiv = document.createElement('div'); tempDiv.innerHTML = innerValue; // Get parent element tag from configuration - whitelist for safety var allowedTags = ['div', 'p']; var parentElementLower = enterKey.toLowerCase(); var parentElement = allowedTags.indexOf(parentElementLower) >= 0 ? parentElementLower : 'div'; // Apply processing to the temporary div wrapTextAndInlineNodes(tempDiv, parentElement); // Extract and return processed HTML var value = tempDiv.innerHTML; tempDiv.remove(); return value; } /** * * checks if tag is in set * * @param {Set<string>} set - The set to check for the tag. * @param {string} value - The tag to check for. * * @returns {boolean} - True if the tag is in the set, false otherwise. */ export function isInSet(set, value) { var iterator = set.values(); var current = iterator.next(); while (!current.done) { if (current.value === value) { return true; } current = iterator.next(); } return false; } /** * * Wraps text and inline nodes within a given node to ensure proper HTML structure. * * @param {Node} node - The DOM node whose child nodes are to be wrapped. * @param {string} parentElement - The parent element tag to use for wrapping. * @returns {void} - This function does not return anything. */ export function wrapTextAndInlineNodes(node, parentElement) { // Define HTML tag categories var recursiveBlockTags = new Set([ 'DIV', 'TH', 'TD', 'LI', 'BLOCKQUOTE', 'OL', 'UL', 'TABLE', 'TBODY', 'TR', 'THEAD', 'TFOOT' ]); var blockTags = new Set([ 'DIV', 'P', 'SECTION', 'ARTICLE', 'HEADER', 'FOOTER', 'ASIDE', 'NAV', 'MAIN', 'FIGURE', 'FIGCAPTION', 'BLOCKQUOTE', 'OL', 'UL', 'LI', 'TABLE', 'TBODY', 'TR', 'TD', 'TH', 'THEAD', 'TFOOT', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SVG', 'PRE', 'COLGROUP' ]); var inlineBlockTags = new Set([ 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'IMG', 'LABEL', 'IFRAME', 'VIDEO', 'AUDIO', 'OBJECT', 'EMBED', 'CANVAS', 'METER', 'PROGRESS', 'OBJECT' ]); var nonWrappableTags = new Set(['BASE', 'AREA', 'LINK']); var nodes = Array.from(node.childNodes); var currentWrapper = null; var _loop_1 = function (child) { var needTowrap = true; if (child.parentElement && child.parentElement.nodeName === 'LI') { needTowrap = needToWrapLiChild(child.parentElement, blockTags); } // Process text nodes if (child.nodeType === Node.TEXT_NODE) { if (child.nodeValue && child.nodeValue.trim() && needTowrap) { if (!currentWrapper) { currentWrapper = document.createElement(parentElement); node.insertBefore(currentWrapper, child); } currentWrapper.appendChild(child); } } // Process element nodes else if (child.nodeType === Node.ELEMENT_NODE) { var childElement = child; var tagName = childElement.tagName.toUpperCase(); // Handle block elements if (isInSet(blockTags, tagName) && !isInSet(inlineBlockTags, tagName)) { currentWrapper = null; var childElements = Array.from(childElement.childNodes); // Check if has block children (safe alternative to Array.some()) var hasBlock_1 = false; childElements.forEach(function (node) { var nodeName = node.tagName; if (node.nodeType === Node.ELEMENT_NODE && isInSet(blockTags, nodeName)) { hasBlock_1 = true; // Can't break from forEach, but we can use other patterns } }); if (isInSet(recursiveBlockTags, tagName) && childElements.length > 0 && hasBlock_1) { wrapTextAndInlineNodes(childElement, parentElement); } } // Handle inline elements else if (!isInSet(blockTags, tagName) && !isInSet(inlineBlockTags, tagName) && !nonWrappableTags.has(tagName) && tagName !== 'HR') { if (child.parentNode && needTowrap && child.parentNode.childNodes.length > 1) { if (!currentWrapper) { currentWrapper = document.createElement(parentElement); node.insertBefore(currentWrapper, child); } currentWrapper.appendChild(child); } } } // Flatten nested structures if (child.nodeType === Node.ELEMENT_NODE) { var childElement = child; var tagName = childElement.tagName.toUpperCase(); // Check if tag is in blockTags var isBlockTag = false; var blockIterator = blockTags.values(); var current = blockIterator.next(); while (!current.done) { if (current.value === tagName) { isBlockTag = true; break; } current = blockIterator.next(); } if (isBlockTag) { if (childElement.childNodes.length === 1 && childElement.firstChild && childElement.firstChild.nodeType === Node.ELEMENT_NODE && (childElement.firstChild.nodeName === 'P' && childElement.nodeName !== 'DIV') && childElement.firstChild.childNodes.length === 1 && childElement.firstChild.firstChild && childElement.firstChild.firstChild.nodeType !== Node.ELEMENT_NODE && childElement.firstChild.attributes.length === 0) { childElement.replaceChild(childElement.firstChild.firstChild, childElement.firstChild); } else if (childElement.nodeName === 'P' && childElement.parentElement && childElement.parentElement.nodeName === 'LI' && !needTowrap && childElement.attributes.length === 0) { var isEmptyText = void 0; var next = getNextMeaningfulSibling(childElement.nextSibling); var prev = getPreviousMeaningfulSibling(childElement.previousSibling); if (!next && !prev) { isEmptyText = true; } if (isEmptyText) { while (childElement.firstChild) { childElement.parentElement.insertBefore(childElement.firstChild, childElement); // Move each child before the <p> } childElement.parentElement.removeChild(childElement); // Remove the empty <p> } } } } }; for (var _i = 0, nodes_1 = nodes; _i < nodes_1.length; _i++) { var child = nodes_1[_i]; _loop_1(child); } } /** * * Returns the next meaningful sibling of the given node. * * @param {Node} node - The DOM node whose child nodes are to be wrapped. * @returns {Node | null} - Returns a node or null. */ export function getNextMeaningfulSibling(node) { while (node) { if ((node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '') || (node.nodeName === 'OL' || node.nodeName === 'UL')) { node = node.nextSibling; } else { return node; } } return null; } /** * * Returns the previous meaningful sibling of the given node. * * @param {Node} node - The DOM node whose child nodes are to be wrapped. * @returns {Node | null} - Returns a node or null. */ export function getPreviousMeaningfulSibling(node) { while (node) { if ((node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '') || (node.nodeName === 'OL' || node.nodeName === 'UL')) { node = node.previousSibling; } else { return node; } } return null; } /** * * Checks if the given node is need to be wrapped. * * @param {Node} node - The DOM node whose child nodes are to be wrapped. * @param {Set<string>} blockTags - The set of block tags. * @returns {boolean} - Returns a boolean value. */ export function needToWrapLiChild(node, blockTags) { var hasBlockElement = false; var hasNonBlockContent = false; var liElement = node; liElement.childNodes.forEach(function (child) { if (child.nodeType === Node.ELEMENT_NODE) { var tag = child.nodeName; if (blockTags.has(tag) && tag !== 'OL' && tag !== 'UL') { var next = child.nextSibling; var isFollowedByList = next && ['UL', 'OL'].indexOf(next.nodeName) !== -1; if (!isFollowedByList) { hasBlockElement = true; } } else if (['OL', 'UL'].indexOf(tag) !== -1) { var prev = child.previousSibling; var next = child.nextSibling; var isSurroundedByContent = prev && blockTags.has(prev.nodeName) && next && next.nodeType === Node.TEXT_NODE && next.textContent.trim().length > 0; if (isSurroundedByContent) { hasBlockElement = true; } } else if (!blockTags.has(tag) && tag !== 'LI') { var next = child.nextSibling; var isFollowedByList = next && ['UL', 'OL'].indexOf(next.nodeName) !== -1; if (!isFollowedByList) { hasNonBlockContent = true; } } } else if (child.nodeType === Node.TEXT_NODE && child.textContent.trim().length > 0) { hasNonBlockContent = true; } }); return hasBlockElement && hasNonBlockContent; } /** * Removes all newlines from a string and replaces consecutive spaces between tags with a single space. * * @param {string} htmlString - The string value from which newlines will be removed. * @param {Element} editNode - The editable element. * @returns {string} - Returns the modified string without newline characters. * @hidden */ export function cleanHTMLString(htmlString, editNode) { var isPreLine = false; if (getComputedStyle(editNode).whiteSpace === 'pre-wrap' || getComputedStyle(editNode).whiteSpace === 'pre') { return htmlString; } else if (getComputedStyle(editNode).whiteSpace === 'pre-line') { isPreLine = true; } /** * Checks if the given HTML element has the 'pre-line' style. * * @param {HTMLElement} node - The HTML element to check. * @returns {boolean} - True if the element has the 'pre-line' style, false otherwise. */ function hasPreLineStyle(node) { if (node.style.whiteSpace === 'pre-line') { return true; } return false; } /** * Checks if the given HTML element is a preformatted text element ('<pre>'). * * @param {HTMLElement} node - The HTML element to check. * @returns {boolean} - True if the element is a '<pre>' tag, false otherwise. */ function hasPre(node) { if (node.tagName === 'PRE') { return true; } if (node.style.whiteSpace === 'pre' || node.style.whiteSpace === 'pre-wrap') { return true; } return false; } /** * Cleans the text content of a given node by processing its child nodes. * * @param {Node} node - The DOM node whose text content needs to be cleaned. * @param {boolean} hasPreLine - Indicates whether the node has the 'pre-line' style. * @returns {void} */ function cleanTextContent(node, hasPreLine) { if (hasPreLine === void 0) { hasPreLine = false; } if (node == null) { return; } var child = node.firstChild; while (child != null) { if (child.nodeType === 3) { if (hasPreLine) { child.nodeValue = child.nodeValue.replace(/[\t]/g, ' '); child.nodeValue = child.nodeValue.replace(/[ ]{2,}/g, ' '); } else { child.nodeValue = child.nodeValue.replace(/[\n\r\t]/g, ' '); child.nodeValue = child.nodeValue.replace(/[ ]{2,}/g, ' '); } } else if (child.nodeType === 1) { if (!hasPre(child) && !hasPreLineStyle(child)) { cleanTextContent(child, hasPreLine); } if (hasPreLineStyle(child)) { cleanTextContent(child, true); } } child = child.nextSibling; } } var container = document.createElement('div'); container.innerHTML = htmlString; cleanTextContent(container, isPreLine); return container.innerHTML; } /** * Converting the base64 url to blob * * @param {string} dataUrl - specifies the string value * @returns {Blob} - returns the blob * @hidden */ export function convertToBlob(dataUrl) { var arr = dataUrl.split(','); var mime = arr[0].match(/:(.*?);/)[1]; var bstr = atob(arr[1]); var n = bstr.length; var u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], { type: mime }); } /** * Escapes HTML characters in a string. * * @param {string} html - The HTML string to be escaped. * @returns {string} The escaped HTML string. */ export function escaseHtml(html) { return html .replace(/&/g, '&amp;') .replace(/"/g, '&quot;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); } /** * Aligns HTML content by parsing it through the DOM parser and returning the structured HTML. * * @param {string} htmlString - The HTML string to be aligned. * @returns {string} The aligned HTML string. */ export function alignmentHtml(htmlString) { var parser = new DOMParser(); var doc = parser.parseFromString(htmlString, 'text/html'); var formatted = formatNode(doc.body, 0).trim(); return formatted; } /** * Formats a DOM node with proper indentation for improved readability. * * @param {Node} node - The DOM node to format. * @param {number} indentLevel - The current indentation level. * @returns {string} The formatted node as a string with proper indentation. */ export function formatNode(node, indentLevel) { // Block-level HTML tags var blockTags = new Set([ 'address', 'article', 'aside', 'blockquote', 'canvas', 'dd', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'li', 'main', 'nav', 'noscript', 'ol', 'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'thead', 'tbody', 'tr', 'th', 'td', 'colgroup' ]); // Self-closing HTML tags var selfClosingTags = new Set([ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr' ]); var indent = ' '.repeat(indentLevel); var result = ''; // Recursively process child nodes node.childNodes.forEach(function (child) { if (child.nodeType === Node.TEXT_NODE) { var text = child.textContent; if (text.trim().length === 0) { text = text.trim(); } if (text) { text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); result += text; } } else if (child.nodeType === Node.ELEMENT_NODE) { var element = child; var tagName = element.tagName.toLowerCase(); var attrs = Array.from(element.attributes) .map(function (attr) { return attr.name + "=\"" + escaseHtml(attr.value) + "\""; }) .join(' '); var attrString = attrs ? " " + attrs : ''; var openTag = attrs ? "<" + tagName + " " + attrs + ">" : "<" + tagName + ">"; var closeTag = "</" + tagName + ">"; var isBlock = blockTags.has(tagName); var isSelfClosing = selfClosingTags.has(tagName); if (isSelfClosing) { if (tagName === 'col') { if (result[result.length - 1] === '\n') { result += indent + "<" + tagName + attrString + ">\n"; } else { result += "\n" + indent + "<" + tagName + attrString + ">\n"; } } else { result += "<" + tagName + attrString + "/>"; } } else if (isBlock) { if (result[result.length - 1] === '\n') { result += "" + indent + openTag; } else { result += "\n" + indent + openTag; } var inner = formatNode(child, indentLevel + 1); if (inner) { result += "" + inner; } if (result[result.length - 1] === '\n') { result += "" + indent + closeTag + "\n"; } else { result += closeTag + "\n"; } } else { result += "" + openTag + formatNode(child, 0) + closeTag; } } }); return result; } /** * Opens a new window, injects the given element and styles, and triggers print. * * @param {Element} element - The element to clone and print. * @param {Window} [printWindow] - Optional existing window, otherwise a new one is created. * @returns {Window} - The print window instance. */ export function openPrintWindow(element, printWindow) { var div = element.ownerDocument.createElement('div'); var links = [].slice.call(element.ownerDocument.getElementsByTagName('head')[0].querySelectorAll('base, link, style')); var blinks = [].slice.call(element.ownerDocument.getElementsByTagName('body')[0].querySelectorAll('link, style')); if (blinks.length) { for (var l = 0, len = blinks.length; l < len; l++) { links.push(blinks[parseInt(l.toString(), 10)]); } } var reference = ''; if (isNullOrUndefined(printWindow)) { printWindow = window.open('', 'print', 'height=452,width=1024,tabbar=no'); } div.appendChild(element.cloneNode(true)); for (var i = 0, len = links.length; i < len; i++) { reference += links[parseInt(i.toString(), 10)].outerHTML; } printWindow.document.write('<!DOCTYPE html> <html><head>' + reference + '</head><body>' + div.innerHTML + '<script> (function() { window.ready = true; })(); </script>' + '</body></html>'); printWindow.document.close(); printWindow.focus(); var interval = setInterval(function () { if (printWindow.ready) { printWindow.print(); printWindow.close(); clearInterval(interval); } }, 500); return printWindow; } /** * Determines the effective root offset parent of a given image or Video element, * * @private * @param {HTMLElement} mediaElement - The image or Video element whose offset parent is to be found. * @param {string} parentID - The ID of the parent element. * @returns {HTMLElement} - The resolved root offset parent element. */ export function getRootOffsetParent(mediaElement, parentID) { var ignoreOffset = ['TD', 'TH', 'TABLE', 'A']; var doc = mediaElement.ownerDocument; var offsetParent; var rootEle = closest(mediaElement, '#' + parentID + '_rte-edit-view'); if (mediaElement.closest('.e-rte-checklist')) { offsetParent = rootEle ? rootEle : doc.documentElement; } else { offsetParent = ((mediaElement.offsetParent && ((mediaElement.offsetParent.classList.contains('e-img-caption') || mediaElement.offsetParent.classList.contains('e-video-clickelem')) || ignoreOffset.indexOf(mediaElement.offsetParent.tagName) > -1)) ? rootEle : mediaElement.offsetParent) || doc.documentElement; } while (offsetParent && (offsetParent === doc.body || offsetParent === doc.documentElement) && offsetParent.style.position === 'static') { offsetParent = offsetParent.parentNode; } return offsetParent; } /** * Determines the image or video element top and left position, * * @private * @param {HTMLElement} mediaElement - The image or Video element whose offset parent is to be found. * @param {HTMLTextAreaElement} rootEle - RichTextEditor root div element. * @returns {OffsetPosition} - The resolved media element top and left position value. */ export function getMediaResizeBarValue(mediaElement, rootEle) { // Client rects in the same document coordinate space var elemRect = mediaElement.getBoundingClientRect(); var rootRect = rootEle.getBoundingClientRect(); // Determine scroll context var documentEle = rootEle.ownerDocument; // Page scroll (used if you need page-relative, but cancels out when subtracting rects) var pageX = documentEle.documentElement.scrollLeft || 0; var pageY = documentEle.documentElement.scrollTop || 0; // Content-root internal scroll (if the editor area is scrollable) var rootScrollX = rootEle.scrollLeft || 0; var rootScrollY = rootEle.scrollTop || 0; return { top: (elemRect.top + pageY) - (rootRect.top + pageY) + rootScrollY, left: (elemRect.left + pageX) - (rootRect.left + pageX) + rootScrollX }; }