@syncfusion/ej2-richtexteditor
Version:
Essential JS 2 RichTextEditor component
1,187 lines (1,186 loc) • 47.6 kB
JavaScript
/**
* 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(/</g, '<').replace(/>/g, '>').replace(/&/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, '&')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>');
}
/**
* 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, '&').replace(/</g, '<').replace(/>/g, '>');
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
};
}