UNPKG

@syncfusion/ej2-richtexteditor

Version:
884 lines (882 loc) 93.4 kB
import { addClass, attributes, Browser, closest, detach, isNullOrUndefined as isNOU, isNullOrUndefined, removeClass, createElement } from '@syncfusion/ej2-base'; import { hasAnyFormatting, isIDevice, removeClassWithAttr, scrollToCursor, convertFontSize, isBlockNode } from '../../common/util'; import { InsertHtml } from '../../editor-manager/plugin/inserthtml'; import { NodeSelection } from '../../selection/selection'; import * as classes from '../base/classes'; import { CLS_TABLE_MULTI_CELL, CLS_TABLE_SEL, CLS_TABLE_SEL_END } from '../../common/constant'; import * as events from '../base/constant'; import { RenderType } from '../base/enum'; import { getDefaultValue, getTextNodesUnder, sanitizeHelper } from '../base/util'; import { HTMLFormatter } from '../formatter/html-formatter'; import { ContentRender } from '../renderer/content-renderer'; import { IframeContentRender } from '../renderer/iframe-content-renderer'; import { ON_BEGIN } from './../../common/constant'; import { HtmlToolbarStatus } from './html-toolbar-status'; import { XhtmlValidation } from './xhtml-validation'; /** * `HtmlEditor` module is used to HTML editor */ var HtmlEditor = /** @class */ (function () { function HtmlEditor(parent, serviceLocator) { this.rangeCollection = []; this.isImageDelete = false; this.isMention = false; this.parent = parent; this.locator = serviceLocator; this.renderFactory = this.locator.getService('rendererFactory'); this.xhtmlValidation = new XhtmlValidation(parent); this.addEventListener(); this.isDestroyed = false; this.isCopyAll = false; this.isSlashMenuOpen = false; this.isPreviousNodeBrAfterBackSpace = false; this.isContainsEmptySpace = false; } /** * Destroys the Markdown. * * @function destroy * @returns {void} * @hidden */ HtmlEditor.prototype.destroy = function () { if (this.isDestroyed) { return; } if (this.clickTimeout) { clearTimeout(this.clickTimeout); this.clickTimeout = null; } this.removeEventListener(); this.locator = null; this.contentRenderer = null; this.renderFactory = null; this.toolbarUpdate = null; this.nodeSelectionObj = null; this.isCopyAll = null; this.isSlashMenuOpen = null; this.isContainsEmptySpace = false; if (this.rangeCollection.length > 0) { this.rangeCollection = []; } if (this.rangeElement) { this.rangeElement = null; } if (this.oldRangeElement) { this.oldRangeElement = null; } if (this.deleteRangeElement) { this.deleteRangeElement = null; } if (this.deleteOldRangeElement) { this.deleteOldRangeElement = null; } if (this.saveSelection) { this.saveSelection = null; } if (this.xhtmlValidation) { this.xhtmlValidation = null; } this.isDestroyed = true; }; /** * @param {string} value - specifies the string value * @returns {void} * @hidden */ HtmlEditor.prototype.sanitizeHelper = function (value) { value = sanitizeHelper(value, this.parent); return value; }; HtmlEditor.prototype.addEventListener = function () { if (this.parent.isDestroyed) { return; } this.nodeSelectionObj = new NodeSelection(this.parent.inputElement); this.parent.on(events.initialLoad, this.instantiateRenderer, this); this.parent.on(events.htmlToolbarClick, this.onToolbarClick, this); this.parent.on(events.slashMenuOpening, this.onSlashMenuOpen, this); this.parent.on(events.keyDown, this.onKeyDown, this); this.parent.on(events.keyUp, this.onKeyUp, this); this.parent.on(events.initialEnd, this.render, this); this.parent.on(events.modelChanged, this.onPropertyChanged, this); this.parent.on(events.destroy, this.destroy, this); this.parent.on(events.selectAll, this.selectAll, this); this.parent.on(events.selectRange, this.selectRange, this); this.parent.on(events.getSelectedHtml, this.getSelectedHtml, this); this.parent.on(events.selectionSave, this.onSelectionSave, this); this.parent.on(events.selectionRestore, this.onSelectionRestore, this); this.parent.on(events.readOnlyMode, this.updateReadOnly, this); this.parent.on(events.paste, this.onPaste, this); this.parent.on(events.tableclass, this.isTableClassAdded, this); this.parent.on(events.onHandleFontsizeChange, this.onHandleFontsizeChange, this); this.parent.on(events.afterKeyDown, this.afterKeyDown, this); }; HtmlEditor.prototype.onSlashMenuOpen = function () { this.isSlashMenuOpen = true; }; HtmlEditor.prototype.updateReadOnly = function () { if (this.parent.readonly) { attributes(this.parent.contentModule.getEditPanel(), { contenteditable: 'false' }); addClass([this.parent.element], classes.CLS_RTE_READONLY); } else { attributes(this.parent.contentModule.getEditPanel(), { contenteditable: 'true' }); removeClass([this.parent.element], classes.CLS_RTE_READONLY); } }; HtmlEditor.prototype.onSelectionSave = function () { var currentDocument = this.contentRenderer.getDocument(); var range = this.nodeSelectionObj.getRange(currentDocument); this.saveSelection = this.nodeSelectionObj.save(range, currentDocument); }; HtmlEditor.prototype.onSelectionRestore = function (e) { this.parent.isBlur = false; this.contentRenderer.getEditPanel().focus({ preventScroll: true }); if ((isNullOrUndefined(e.items) || e.items) && (!isNullOrUndefined(this.saveSelection))) { this.saveSelection.restore(); } }; HtmlEditor.prototype.isTableClassAdded = function () { var tableElement = this.parent.inputElement.querySelectorAll('table'); for (var i = 0; i < tableElement.length; i++) { // e-rte-table class is added to the table element for styling. // e-rte-paste-table class is added for pasted table element from MS Word and other sources such as Web will not have any styles. // e-rte-custom-table class is added for custom table element will not have any styles. if (!tableElement[i].classList.contains('e-rte-table') && !tableElement[i].classList.contains('e-rte-paste-table') && !tableElement[i].classList.contains('e-rte-custom-table')) { tableElement[i].classList.add('e-rte-table'); } } }; HtmlEditor.prototype.onHandleFontsizeChange = function (e) { var keyboardArgs = e.args; var args = { name: 'dropDownSelect' }; args.item = { command: 'Font', subCommand: 'FontSize' }; var items = this.parent.fontSize.items; var activeElem; if (this.parent.toolbarModule && this.parent.toolbarModule.dropDownModule && this.parent.toolbarModule.dropDownModule.fontSizeDropDown && !isNOU(this.parent.toolbarModule.dropDownModule.fontSizeDropDown.activeElem[0].textContent) && this.parent.toolbarModule.dropDownModule.fontSizeDropDown.activeElem[0].textContent !== '') { activeElem = this.parent.toolbarModule.dropDownModule.fontSizeDropDown.activeElem[0].textContent; } else { var fontSizeValue = void 0; var selection = this.parent.contentModule.getDocument().getSelection(); if (selection && selection.focusNode && selection.focusNode.parentElement) { fontSizeValue = document.defaultView.getComputedStyle(selection.focusNode.parentElement, null).getPropertyValue('font-size'); } else { fontSizeValue = this.parent.fontSize.width; } fontSizeValue = isNOU(fontSizeValue) ? this.parent.fontSize.width : fontSizeValue; var actualTxtFontValues = fontSizeValue.match(/^([\d.]+)(\D+)$/); var size_1 = parseInt(actualTxtFontValues[1], 10); var unit = actualTxtFontValues[2]; var defaultFontValues = items[1].value.match(/^([\d.]+)(\D+)$/); if (defaultFontValues[2] === unit) { var index = items.findIndex(function (_a) { var value = _a.value; return parseInt(value, 10) >= size_1; }); activeElem = items[index].text; } else { var convertedSize_1 = convertFontSize(size_1, unit, defaultFontValues[2]); var index = items.findIndex(function (_a) { var value = _a.value; return parseInt(value, 10) >= convertedSize_1; }); activeElem = items[index].text; } } var fontIndex = items.findIndex(function (size) { return size.text === (activeElem === 'Font Size' ? 'Default' : activeElem); }); if (keyboardArgs.action === 'increase-fontsize' && fontIndex !== -1) { if (fontIndex >= items.length - 1) { var fontValues = items[fontIndex].value.match(/^([\d.]+)(\D+)$/); if (fontValues) { var size = parseInt(fontValues[1], 10); var unit = fontValues[2]; var roundedSize = size % 10 === 0 ? Math.ceil((size + 1) / 10) * 10 : Math.ceil(size / 10) * 10; args.item.value = roundedSize.toLocaleString() + unit; args.item.text = roundedSize.toLocaleString() + ' ' + unit; } this.parent.fontSize.items.push(args.item); } else { args.item.value = items[fontIndex + 1].value; args.item.text = items[fontIndex + 1].text; } } else if (keyboardArgs.action === 'decrease-fontsize' && fontIndex !== -1 && fontIndex > 0) { args.item.value = items[fontIndex - 1].value; args.item.text = items[fontIndex - 1].text; } else { if (fontIndex >= 0 && fontIndex < items.length && items[fontIndex]) { args.item.value = items[fontIndex].value; args.item.text = items[fontIndex].text; } } this.parent.formatter.process(this.parent, args, keyboardArgs); }; /* Handles deletion of an entire table when all cells are selected. Prevents default deletion and triggers the table removal action. */ HtmlEditor.prototype.handleEntireTableBackspace = function (e, args) { var range = this.parent.formatter.editorManager.nodeSelection.getRange(this.parent.contentModule.getDocument()); if (this.isEntireTableSelected(range) && (args.keyCode === 8) && !isNOU(this.parent.tableModule)) { args.preventDefault(); var save = this.parent.formatter.editorManager.nodeSelection.save(range, this.parent.contentModule.getDocument()); var selectParentEle = this.parent.formatter.editorManager.nodeSelection .getParentNodeCollection(range); this.parent.notify(events.tableToolbarAction, { member: 'table', args: { item: { command: 'Table', subCommand: 'TableRemove' }, originalEvent: e.args }, selection: save, selectParent: selectParentEle }); this.parent.autoResize(); return true; } return false; }; HtmlEditor.prototype.onKeyUp = function (e) { var args = e.args; var restrictKeys = [8, 9, 13, 17, 18, 20, 27, 36, 37, 38, 39, 40, 44, 45, 46, 91, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123]; var range = this.parent.getRange(); var regEx = new RegExp('\u200B', 'g'); var isEmptyNode = range.startContainer === range.endContainer && range.startOffset === range.endOffset && range.startOffset === 1 && range.startContainer.textContent.length === 1 && range.startContainer.textContent.charCodeAt(0) === 8203 && range.startContainer.textContent.replace(regEx, '').length === 0; var isMention = false; if (range.startContainer === range.endContainer && range.startOffset === range.endOffset && (range.startContainer !== this.parent.inputElement && range.startOffset !== 0)) { var mentionStartNode = range.startContainer.nodeType === 3 ? range.startContainer : range.startContainer.childNodes[range.startOffset - 1]; isMention = args.keyCode === 16 && mentionStartNode.textContent.charCodeAt(0) === 8203 && !isNOU(mentionStartNode.previousSibling) && mentionStartNode.previousSibling.contentEditable === 'false'; } if (this.isCopyAll) { return; } var pointer; var isRootParent = false; if (!this.isSlashMenuOpen && restrictKeys.indexOf(args.keyCode) < 0 && !args.shiftKey && !args.ctrlKey && !args.altKey && !isEmptyNode && !isMention) { pointer = range.startOffset; // eslint-disable-next-line @typescript-eslint/no-unused-expressions range.startContainer.nodeName === '#text' ? range.startContainer.parentElement !== this.parent.inputElement ? range.startContainer.parentElement.classList.add('currentStartMark') : isRootParent = true : range.startContainer.classList.add('currentStartMark'); if (range.startContainer.textContent.charCodeAt(0) === 8203) { var previousLength_1 = range.startContainer.textContent.length; var previousRange = range.startOffset; this.removeZeroWidthSpaces(range.startContainer, regEx); pointer = previousRange === 0 ? previousRange : previousRange - (previousLength_1 - range.startContainer.textContent.length); this.parent.formatter.editorManager.nodeSelection.setCursorPoint(this.parent.contentModule.getDocument(), range.startContainer, pointer); } var previousLength = this.parent.inputElement.innerHTML.length; var currentLength = this.parent.inputElement.innerHTML.replace(regEx, '').length; var focusNode = range.startContainer; if (previousLength > currentLength && !isRootParent) { if (focusNode.textContent.trim().length !== 0 && focusNode.previousSibling) { var tempSpan = document.createElement('span'); tempSpan.className = 'tempSpan'; range.insertNode(tempSpan); } var currentChild = this.parent.inputElement.firstChild; while (!isNOU(currentChild)) { if (currentChild.nodeName === '#text') { currentChild = currentChild.nextElementSibling; continue; } if (currentChild.textContent.replace(regEx, '').trim().length > 0 && currentChild.textContent.includes('\u200B')) { this.removeZeroWidthSpaces(currentChild, regEx); } currentChild = currentChild.nextElementSibling; } var tempSpanToRemove = this.parent.inputElement.querySelector('.tempSpan'); if (tempSpanToRemove && tempSpanToRemove.previousSibling && focusNode.textContent.trim().length !== 0) { focusNode = tempSpanToRemove.previousSibling; pointer = tempSpanToRemove.previousSibling.textContent.length; var parentElement = tempSpanToRemove.parentNode; parentElement.removeChild(tempSpanToRemove); tempSpanToRemove = null; } var currentElement = this.parent.inputElement.querySelector('.currentStartMark'); var currentChildNode = currentElement ? currentElement.childNodes : []; if (currentChildNode.length > 1) { for (var i = 0; i < currentChildNode.length; i++) { if (currentChildNode[i].nodeName === '#text' && currentChildNode[i].textContent.length === 0) { detach(currentChildNode[i]); i--; } if (!isNOU(currentChildNode[i]) && focusNode.textContent.replace(regEx, '') === currentChildNode[i].textContent) { var iscursorLeft = pointer <= focusNode.textContent.indexOf('\u200B'); pointer = focusNode.textContent.length > 1 ? ((focusNode.textContent === currentChildNode[i].textContent || iscursorLeft) ? pointer : pointer - (focusNode.textContent.length - focusNode.textContent.replace(regEx, '').length)) : focusNode.textContent.length; focusNode = currentChildNode[i]; } } } else if (currentChildNode.length === 1) { if (focusNode.textContent.replace(regEx, '') === currentChildNode[0].textContent) { focusNode = currentChildNode[0]; } } this.parent.formatter.editorManager.nodeSelection.setCursorPoint(this.parent.contentModule.getDocument(), focusNode, pointer); } var currentElem = this.parent.inputElement.querySelector('.currentStartMark'); if (!isNOU(currentElem)) { currentElem.classList.remove('currentStartMark'); if (currentElem.getAttribute('class').trim() === '') { currentElem.removeAttribute('class'); } } if (!isNOU(range.startContainer.previousSibling) && !isNOU(range.startContainer.previousSibling.parentElement) && range.startContainer.parentElement === range.startContainer.previousSibling.parentElement && range.startContainer.previousSibling.textContent.charCodeAt(0) === 8203 && range.startContainer.previousSibling.textContent.length <= 1) { range.startContainer.previousSibling.textContent = range.startContainer.previousSibling.textContent.replace(regEx, ''); } if (range.endContainer.textContent.charCodeAt(range.endOffset) === 8203) { pointer = range.startOffset; range.endContainer.textContent = range.endContainer.textContent.replace(regEx, ''); this.parent.formatter.editorManager.nodeSelection.setCursorPoint(this.parent.contentModule.getDocument(), range.startContainer, pointer); } } this.isSlashMenuOpen = false; }; HtmlEditor.prototype.removeZeroWidthSpaces = function (node, regex) { var _this = this; if (node.nodeType === Node.TEXT_NODE) { if (node.textContent !== null) { node.textContent = node.textContent.replace(regex, ''); } return; } node.childNodes.forEach(function (child) { _this.removeZeroWidthSpaces(child, regex); }); }; HtmlEditor.prototype.enterWithSpace = function () { var range = this.parent.getRange(); var isCursor = range.startContainer === range.endContainer && range.startOffset === range.endOffset; var cursorpointer = range.startOffset; var currentText = this.parent.formatter.editorManager.nodeSelection .findFirstTextNode(range.startContainer); var startNode = range.startContainer.nodeName === '#text' ? (range.startContainer.parentElement !== this.parent.inputElement) ? range.startContainer.parentElement : range.startContainer : range.startContainer; var preventedSelectors = 'table, tbody, td, th, .e-img-caption-text, pre, pre code, blockquote'; var isAllowed; if (startNode.nodeType !== Node.TEXT_NODE) { isAllowed = startNode && isNOU(startNode.querySelector(preventedSelectors)) && isNOU(closest(startNode, preventedSelectors)); if (startNode.closest('table, tbody, td, th')) { var closestBlockParent = this.parent.formatter.editorManager.domTree.getParentBlockNode(startNode); var notAllowedTableElemTags = ['td', 'th', 'tbody']; if (notAllowedTableElemTags.indexOf(closestBlockParent.nodeName.toLowerCase()) > -1) { isAllowed = false; } else { isAllowed = true; } } // Edge case: Prevent space replacement when cursor is positioned at a text node within startNode's children if (isCursor && startNode.nodeType !== Node.TEXT_NODE) { if (isCursor && currentText && currentText.previousSibling && currentText.previousSibling.nodeName !== 'BR') { isAllowed = false; } } } else { if (isCursor && startNode.textContent[range.startOffset] === ' ') { isAllowed = true; } } if (currentText && isCursor && currentText.textContent[range.startOffset] === ' ' && isAllowed && !this.isContainsEmptySpace) { var textContentArray = Array.from(currentText.textContent); textContentArray[range.startOffset] = textContentArray[range.startOffset].replace(/^\s/, '\u00A0'); currentText.textContent = textContentArray.join(''); this.parent.formatter.editorManager.nodeSelection.setCursorPoint(this.parent.contentModule.getDocument(), currentText, cursorpointer); } this.isContainsEmptySpace = false; }; HtmlEditor.prototype.afterKeyDown = function (e) { if (e.args.which === 13) { this.enterWithSpace(); } }; HtmlEditor.prototype.onKeyDown = function (e) { var _this = this; if (e.args.ctrlKey && e.args.keyCode === 65) { this.isCopyAll = true; } else if (!((e.args.key === 'Backspace' || e.args.key === 'Delete') && this.isCopyAll)) { this.isCopyAll = false; } var currentRange = this.parent.getRange(); var isCursor = currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset; if (isCursor && currentRange.startContainer.nodeName === '#text' && e.args.which === 13) { //Condition validation for nbsp adding scenario in enter key press var textContentArray = Array.from(currentRange.startContainer.textContent); var isPrevTextEmpty = !isNOU(textContentArray[currentRange.startOffset - 1]) && textContentArray[currentRange.startOffset - 1] === ' '; var isDoubleEmptySpace = !isNOU(textContentArray[currentRange.startOffset]) && !isNOU(textContentArray[currentRange.startOffset + 1]) && textContentArray[currentRange.startOffset] === ' ' && textContentArray[currentRange.startOffset + 1] === ' '; this.isContainsEmptySpace = isPrevTextEmpty && isDoubleEmptySpace; } var args = e.args; if (this.parent.inputElement.querySelectorAll('.e-cell-select:not(table)').length > 1 && (args.keyCode === 8 || args.keyCode === 32 || args.keyCode === 13 || args.keyCode === 46)) { this.tableSelectionKeyAction(e); this.parent.autoResize(); return; } if (Browser.info.name === 'chrome') { currentRange = this.parent.getRange(); this.backSpaceCleanup(e, currentRange); this.deleteCleanup(e, currentRange); } var isCodeBlock = false; if (!isNOU(this.parent.codeBlockModule)) { currentRange = this.parent.getRange(); isCodeBlock = this.parent.formatter.editorManager.codeBlockObj. isSelectionWithinCodeBlock(currentRange, currentRange.startContainer, currentRange.endContainer); } var range = this.parent.getRange(); // Handle empty inline element protection on delete/backspace var isDeleteOrBackspace = (args.key === 'Backspace' && args.keyCode === 8) || (args.key === 'Delete' && args.keyCode === 46); if (this.isInlineProtectionNeeded(range, isDeleteOrBackspace)) { var rootInlineContainer = this.parent.formatter.editorManager.domTree.getTopMostNode(range.startContainer); var rootBlockEle = this.parent.formatter.editorManager.domNode.blockParentNode(range.startContainer); // Check if this is a single-character inline element that needs protection var isSingleCharInBlock = rootBlockEle.textContent.length === 1 && rootInlineContainer.previousSibling === null && rootInlineContainer.nextSibling === null; var isIsolatedSingleChar = this.isInlineSurroundedByBR(rootInlineContainer) && rootInlineContainer.textContent.length === 1; if (isSingleCharInBlock || isIsolatedSingleChar) { this.processInlineElementDeletion(range, args, rootInlineContainer, isIsolatedSingleChar); } } if (args.keyCode === 9 && this.parent.enableTabKey && !isCodeBlock) { this.parent.formatter.saveData(e); if (!this.indentTab()) { return; } if (!isNOU(args.target) && isNullOrUndefined(closest(args.target, '.e-rte-toolbar'))) { var range_1 = this.nodeSelectionObj.getRange(this.contentRenderer.getDocument()); var parentNode = this.nodeSelectionObj.getParentNodeCollection(range_1); if (!((parentNode[0].nodeName === 'LI' || closest(parentNode[0], 'li') || closest(parentNode[0], 'table')))) { args.preventDefault(); var selection = this.contentRenderer.getDocument().getSelection().getRangeAt(0); var alignmentNodes = this.parent.formatter.editorManager.domNode.blockNodes(); if (this.parent.enterKey === 'BR') { if (selection.startOffset !== selection.endOffset && selection.startOffset === 0) { var save = this.nodeSelectionObj.save(range_1, this.contentRenderer.getDocument()); this.parent.formatter.editorManager.domNode.setMarker(save); alignmentNodes = this.parent.formatter.editorManager.domNode.blockNodes(); this.parent.formatter.editorManager.domNode.convertToBlockNodes(alignmentNodes, false); this.marginTabAdd(args.shiftKey, alignmentNodes); save = this.parent.formatter.editorManager.domNode.saveMarker(save); save.restore(); } else { InsertHtml.Insert(this.contentRenderer.getDocument(), '&nbsp;&nbsp;&nbsp;&nbsp;', this.parent.element); this.rangeCollection.push(this.nodeSelectionObj.getRange(this.contentRenderer.getDocument())); } } else { var isSelStartZeroNotCollapsed = selection.startOffset === 0 && selection.endOffset !== selection.startOffset; var startContainer = selection.startContainer; var isImageNodeAtOffset = (selection.startOffset !== selection.endOffset && startContainer && startContainer.hasChildNodes() && startContainer.childNodes && startContainer.childNodes.length > selection.startOffset && startContainer.childNodes[selection.startOffset].nodeName === 'IMG'); var shouldAddMarginTab = isSelStartZeroNotCollapsed || isImageNodeAtOffset; if (shouldAddMarginTab) { this.marginTabAdd(args.shiftKey, alignmentNodes); } else { InsertHtml.Insert(this.contentRenderer.getDocument(), '&nbsp;&nbsp;&nbsp;&nbsp;', this.parent.inputElement); this.rangeCollection.push(this.nodeSelectionObj.getRange(this.contentRenderer.getDocument())); } } } } this.parent.formatter.saveData(e); } // Prevents the link from being added when a space, enter, or parenthesis key is pressed. // This ensures that parentheses are not mistakenly included as part of the URL. var regex = /[^\w\s\\/\\.\\:@-]/g; if (e.args.action === 'space' || e.args.action === 'enter' || e.args.keyCode === 13 || regex.test(e.args.key)) { this.spaceLink(e.args); if (this.parent.editorMode === 'HTML' && !this.parent.readonly) { var currentLength = this.parent.getText().trim().replace(/(\r\n|\n|\r|\t)/gm, '').replace(/\u200B/g, '').length; var selectionLength = this.parent.getSelection().length; var totalLength = (currentLength - selectionLength) + 1; if (!(this.parent.maxLength === -1 || totalLength <= this.parent.maxLength) && e.args.keyCode === 13) { e.args.preventDefault(); return; } else { this.parent.notify(events.enterHandler, { args: e.args }); scrollToCursor(this.parent.contentModule.getDocument(), this.parent.inputElement); } } } if (e.args.action === 'space') { var currentRange_1 = this.parent.getRange(); var editorValue = currentRange_1.startContainer.textContent.slice(0, currentRange_1.startOffset); var orderedList_1 = this.isOrderedList(editorValue); var unOrderedList = this.isUnOrderedList(editorValue); var checkList = this.isCheckList(editorValue); var hasSplitedText = false; if (orderedList_1 || unOrderedList || checkList) { hasSplitedText = this.hasMultipleTextNode(currentRange_1); if (hasSplitedText && !this.isMention) { var element = currentRange_1.startContainer; element = this.parent.formatter.editorManager.domNode.getImmediateBlockNode(element); if (element.childNodes.length > 0 && !element.innerHTML.includes('<br>')) { hasSplitedText = false; } } } if (!hasSplitedText && ((orderedList_1 && !unOrderedList && !checkList) || (unOrderedList && !orderedList_1 && !checkList) || (checkList && !orderedList_1 && !unOrderedList))) { var eventArgs_1 = { callBack: null, event: e.args, name: 'keydown-handler', enterKey: this.parent.enterKey, shiftEnterKey: this.parent.shiftEnterKey, maxLength: this.parent.maxLength }; var actionBeginArgs = { cancel: false, item: { command: 'Lists', subCommand: orderedList_1 ? 'OL' : 'UL' }, name: 'actionBegin', originalEvent: e.args, requestType: orderedList_1 ? 'OL' : 'UL' }; this.parent.trigger(events.actionBegin, actionBeginArgs, function (actionBeginArgs) { if (!actionBeginArgs.cancel) { _this.parent.formatter.editorManager.observer.notify(ON_BEGIN, eventArgs_1); _this.parent.trigger(events.actionComplete, { editorMode: _this.parent.editorMode, elements: _this.parent.formatter.editorManager.domNode.blockNodes(), event: e.args, name: events.actionComplete, range: _this.parent.getRange(), requestType: orderedList_1 ? 'OL' : 'UL' }); } }); } } if (Browser.info.name === 'chrome' && (!isNullOrUndefined(this.rangeElement) && !isNullOrUndefined(this.oldRangeElement) || !isNullOrUndefined(this.deleteRangeElement) && !isNullOrUndefined(this.deleteOldRangeElement)) && currentRange.startContainer.parentElement.tagName !== 'TD' && currentRange.startContainer.parentElement.tagName !== 'TH') { this.rangeElement = null; this.oldRangeElement = null; this.deleteRangeElement = null; this.deleteOldRangeElement = null; if (!this.isImageDelete) { args.preventDefault(); } args.preventDefault(); } this.parent.autoResize(); }; /** * Checks if inserting a tab would exceed the maxLength constraint. * * @returns {boolean} True if allowed, false if it would exceed maxLength. */ HtmlEditor.prototype.indentTab = function () { var tabSpaceLength = 4; var maxLength = this.parent.maxLength; var currentLength = this.parent.getText().replace(/(\r\n|\n|\r|\t)/gm, '').replace(/\u200B/g, '').length; var selectionLength = this.parent.getSelection().length; return maxLength === -1 || (currentLength - selectionLength + tabSpaceLength) <= maxLength; }; HtmlEditor.prototype.isEntireTableSelected = function (range) { var rangeNode = range.startContainer.nodeName === '#text' ? range.startContainer.parentElement : range.startContainer; var currentSelectedTable = closest(rangeNode, 'table'); var cells = currentSelectedTable ? currentSelectedTable.querySelectorAll('td, th') : null; var selectedCells = currentSelectedTable ? currentSelectedTable.querySelectorAll('.e-cell-select.e-multi-cells-select') : null; var isEntireTableSelcted = cells && selectedCells ? cells.length === selectedCells.length : false; return isEntireTableSelcted; }; HtmlEditor.prototype.isOrderedList = function (editorValue) { editorValue = editorValue.replace(/\u200B/g, ''); var olListStartRegex = [/^[1]+[.]+$/, /^[i]+[.]+$/, /^[a]+[.]+$/]; if (!isNullOrUndefined(editorValue)) { for (var i = 0; i < olListStartRegex.length; i++) { if (olListStartRegex[i].test(editorValue)) { return true; } } } return false; }; HtmlEditor.prototype.isUnOrderedList = function (editorValue) { editorValue = editorValue.replace(/\u200B/g, ''); var ulListStartRegex = [/^[*]$/, /^[-]$/]; if (!isNullOrUndefined(editorValue)) { for (var i = 0; i < ulListStartRegex.length; i++) { if (ulListStartRegex[i].test(editorValue)) { return true; } } } return false; }; HtmlEditor.prototype.isInlineProtectionNeeded = function (range, isDeleteOrBackspace) { if (!range || !isDeleteOrBackspace) { return false; } var collapsed = range.startContainer === range.endContainer && range.startOffset === range.endOffset; if (!collapsed) { return false; } var startContainer = range.startContainer.parentElement; var isInlineText = range.startContainer.nodeType === Node.TEXT_NODE && !isBlockNode(startContainer); if (!isInlineText) { return false; } var isCursorAtStart = range.startOffset === 0 || range.startOffset === 1; return isCursorAtStart; }; HtmlEditor.prototype.isInlineSurroundedByBR = function (rootInline) { var prevSibling = rootInline.previousSibling; var nextSibling = rootInline.nextSibling; var isPrevEmpty = prevSibling === null || prevSibling.nodeName === 'BR'; var isNextEmpty = nextSibling === null || nextSibling.nodeName === 'BR'; return isPrevEmpty && isNextEmpty; }; HtmlEditor.prototype.replaceInlineWithBreak = function (rootInline) { var parent = rootInline.parentElement; // Create temporary BR with focus marker var brElem = document.createElement('br'); addClass([brElem], 'focus-node'); parent.replaceChild(brElem, rootInline); // Set cursor to the BR and cleanup marker var focusNode = this.parent.rootContainer.querySelector('.focus-node'); this.parent.formatter.editorManager.nodeSelection.setCursorPoint(this.parent.contentModule.getDocument(), focusNode, 0); focusNode.removeAttribute('class'); }; HtmlEditor.prototype.insertZeroWidthSpace = function (inlineElement) { inlineElement.textContent = ''; inlineElement.appendChild(document.createTextNode('\u200B')); this.parent.formatter.editorManager.nodeSelection.setCursorPoint(this.parent.contentModule.getDocument(), inlineElement.firstChild, 1); }; HtmlEditor.prototype.processInlineElementDeletion = function (currentRange, args, rootInlineContainer, isIsolatedSingleChar) { var inlineElement = currentRange.startContainer.parentElement; var hasPlaceholder = rootInlineContainer.textContent.includes('\u200B'); var isBackspaceKeyPress = !hasPlaceholder && args.key === 'Backspace' && currentRange.startOffset === 1; var isDeleteKeyPress = !hasPlaceholder && args.key === 'Delete' && currentRange.startOffset === 0; if (hasPlaceholder) { // Second delete/backspace: remove the inline element entirely if (isIsolatedSingleChar && this.parent.enterKey === 'BR') { //Need to remove the inline element alone when enter key is <br> rootInlineContainer.parentElement.removeChild(rootInlineContainer); } else { this.replaceInlineWithBreak(rootInlineContainer); } } else if (isBackspaceKeyPress || isDeleteKeyPress) { // First delete/backspace: preserve inline element with placeholder this.insertZeroWidthSpace(inlineElement); } args.preventDefault(); }; HtmlEditor.prototype.isCheckList = function (editorValue) { editorValue = editorValue.replace(/\u200B/g, ''); // Updated regex to match checkbox patterns with at most one space: [], [x], [ ], [x ], [ x], [ x ] var ulListStartRegex = [/^\[\s?\]$/, /^\[\s?x\s?\]$/i]; if (!isNullOrUndefined(editorValue)) { for (var i = 0; i < ulListStartRegex.length; i++) { if (ulListStartRegex[i].test(editorValue)) { return true; } } } return false; }; HtmlEditor.prototype.hasMultipleTextNode = function (range) { this.isMention = false; if (range && range.startContainer && range.startContainer.parentNode) { var parentNode = range.startContainer.parentNode; if (range.startContainer.previousElementSibling && range.startContainer.previousElementSibling.classList.contains('e-mention-chip') && !range.startContainer.previousElementSibling.isContentEditable) { this.isMention = true; return true; } if (this.parent.enterKey === 'BR' || closest(parentNode, 'table')) { return false; } var childNodes = parentNode.childNodes; var textNodes = []; for (var i = 0; i < childNodes.length; i++) { var node = childNodes[i]; if (node && node.nodeType === Node.TEXT_NODE) { textNodes.push(node); if (textNodes.length > 1) { return true; } } } } return false; }; //Determines if the cursor is truly at the start of a block element HtmlEditor.prototype.isCursorAtBlockStart = function (range) { if (range.startOffset !== 0 || range.endOffset !== 0) { return false; } // Get the node where cursor is positioned var cursorNode = range.startContainer; // If cursor is in a text node, check its parent var elementAtCursor = cursorNode.nodeType === Node.TEXT_NODE ? cursorNode.parentElement : cursorNode; // First, check if we're in a table cell - we don't want to handle these if (elementAtCursor.tagName === 'TD' || elementAtCursor.tagName === 'TH') { return false; } // Find the block-level ancestor var blockNode = this.parent.formatter.editorManager.domNode.getImmediateBlockNode(elementAtCursor); // If cursor is directly in a block element at position 0, it's at the start if (cursorNode === blockNode && range.startOffset === 0) { return true; } // Otherwise, we need to check if the cursor is positioned at the absolute beginning of content var currentNode = range.startContainer; var previousContentFound = false; // Walk up the DOM tree until we reach the block parent while (currentNode && currentNode !== blockNode) { // Check if there's any previous sibling with content var sibling = currentNode.previousSibling; while (sibling) { // Skip empty text nodes if (sibling.nodeType === Node.TEXT_NODE && (!sibling.textContent || !sibling.textContent.trim())) { sibling = sibling.previousSibling; continue; } // If we found any non-empty previous sibling, cursor is not at block start previousContentFound = true; break; } if (previousContentFound) { break; } // Move up to parent and check again currentNode = currentNode.parentNode; } // If we reached the block parent without finding previous content, cursor is at start return !previousContentFound; }; HtmlEditor.prototype.backSpaceCleanup = function (e, currentRange) { var isLiElement = false; var isPreviousNotContentEditable = true; if (!isNOU(currentRange.startContainer.previousSibling) && currentRange.startContainer.previousSibling.nodeName === 'SPAN') { isPreviousNotContentEditable = currentRange.startContainer.previousSibling.contentEditable === 'false' ? false : true; } var checkNode = currentRange.startContainer.nodeName === '#text' ? currentRange.startContainer.parentElement : currentRange.startContainer; var isSelectedPositionNotStart = closest(currentRange.startContainer.nodeName === '#text' ? currentRange.startContainer.parentElement : currentRange.startContainer, 'li') ? checkNode.nodeName !== 'li' && isNOU(checkNode.previousSibling) : true; // Method to determine if cursor is truly at start var isCursorAtStart = this.isCursorAtBlockStart(currentRange); if (e.args.code === 'Backspace' && e.args.keyCode === 8 && isCursorAtStart && currentRange.startContainer.textContent !== ' ' && currentRange.startContainer.nodeValue !== '\u00A0' && this.parent.getSelection().length === 0 && currentRange.startContainer.textContent.length > 0 && isPreviousNotContentEditable && isSelectedPositionNotStart) { if ((!this.parent.formatter.editorManager.domNode.isBlockNode(checkNode) && !isNOU(checkNode.previousSibling) && checkNode.previousSibling.nodeName === 'BR') || (!isNOU(currentRange.startContainer.previousSibling) && currentRange.startContainer.previousSibling.nodeName === 'BR')) { return; } var isRangeCollapsed = currentRange.startOffset === currentRange.endOffset && currentRange.startContainer === currentRange.endContainer; if (isRangeCollapsed && this.shouldPreventListRemoval()) { return; } var immediateBlockNode = this.parent.formatter.editorManager.domNode. getImmediateBlockNode(currentRange.startContainer); if (!isNOU(this.parent.codeBlockModule)) { var blockNode = immediateBlockNode !== this.parent.inputElement ? immediateBlockNode : currentRange.startContainer; var firstPosition = this.parent.formatter.editorManager.nodeSelection.findFirstContentNode(blockNode); var cursorAtFirstPosition = firstPosition && firstPosition.node === (currentRange.startContainer.nodeName === 'CODE' ? currentRange.startContainer.firstChild : currentRange.startContainer) && currentRange.startOffset === 0; var isBlockPreviousElement = this.parent.formatter.editorManager.codeBlockObj.findParentOrPreviousSiblingCodeBlock(currentRange); var isCodeBlockElement = this.parent.formatter.editorManager.codeBlockObj.isValidCodeBlockStructure(blockNode); if ((!isNOU(isBlockPreviousElement) && cursorAtFirstPosition) || (!isNOU(isCodeBlockElement) && cursorAtFirstPosition)) { return; } } this.rangeElement = this.getRootBlockNode(currentRange.startContainer); if (this.rangeElement.tagName === 'OL' || this.rangeElement.tagName === 'UL') { var liElement = this.getRangeLiNode(currentRange.startContainer); if (liElement.previousElementSibling && this.parent.inputElement.contains(liElement.previousElementSibling) && liElement.previousElementSibling.childElementCount > 0) { this.oldRangeElement = liElement.previousElementSibling.lastElementChild.nodeName === 'BR' ? liElement.previousElementSibling : liElement.previousElementSibling.lastChild; if (!isNOU(liElement.lastElementChild) && liElement.lastElementChild.nodeName !== 'BR' && isNOU(liElement.lastElementChild.previousSibling) && liElement.lastChild.nodeName !== '#text') { this.rangeElement = liElement.lastElementChild; isLiElement = true; } else { this.rangeElement = liElement; } } } else if (this.rangeElement === this.parent.inputElement || immediateBlockNode === this.parent.inputElement || this.rangeElement.tagName === 'TABLE' || (!isNOU(this.rangeElement.previousElementSibling) && this.rangeElement.previousElementSibling.tagName === 'TABLE')) { return; } else { this.oldRangeElement = this.rangeElement.previousElementSibling; } var findBlockElement = this.parent.formatter.editorManager.domNode.blockNodes(); if (!isNOU(findBlockElement[0]) && currentRange.collapsed && currentRange.startOffset === 0 && currentRange.endOffset === 0 && findBlockElement[0].style.marginLeft !== '') { findBlockElement[0].style.marginLeft = (parseInt(findBlockElement[0].style.marginLeft, 10) <= 20) ? '' : (parseInt(findBlockElement[0].style.marginLeft, 10) - 20 + 'px'); } var findBlockElementSibiling = findBlockElement[0].previousSibling ? findBlockElement[0].previousSibling : this.findPreviousElementSibling(findBlockElement[0]); if (isNOU(this.oldRangeElement) && isNOU(findBlockElement[0].previousSibling)) { return; } else if (findBlockElementSibiling) { var prevSibling = findBlockElementSibiling; var currentElement = findBlockElement[0]; if ((prevSibling.nodeName === 'LI' && currentElement.closest('li') && currentElement.closest('li').querySelector('ul, ol'))) { // the nested list backspace is handled in list file so used return here. return; } if (prevSibling.textContent.trim()) { this.removeLastBr(prevSibling); var lastPosition = this.parent.formatter.editorManager.nodeSelection.findLastTextPosition(prevSibling); var cursorpointer = lastPosition.offset; var lastChild = lastPosition.node; var childNodes = Array.from(currentElement.childNodes); var save = this.nodeSelectionObj.save(currentRange, this.contentRenderer.getDocument()); if (this