@syncfusion/ej2-richtexteditor
Version:
Essential JS 2 RichTextEditor component
884 lines (882 loc) • 93.4 kB
JavaScript
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(), ' ', 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(), ' ', 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