@syncfusion/ej2-richtexteditor
Version:
Essential JS 2 RichTextEditor component
968 lines (966 loc) • 87.2 kB
JavaScript
import { createElement, detach, isNullOrUndefined } from '@syncfusion/ej2-base';
import * as EVENTS from '../../common/constant';
/**
* Code Block internal component
*
* @hidden
*/
var CodeBlockPlugin = /** @class */ (function () {
/**
* Constructor for creating the Code Block plugin
*
* @param {EditorManager} parent - specifies the parent element
* @hidden
*/
function CodeBlockPlugin(parent) {
this.parent = parent;
this.addEventListener();
}
/* Attaches event listeners for code block operations */
CodeBlockPlugin.prototype.addEventListener = function () {
this.parent.observer.on(EVENTS.CODE_BLOCK, this.applyCodeBlockHandler, this);
this.parent.observer.on(EVENTS.INTERNAL_DESTROY, this.destroy, this);
this.parent.observer.on(EVENTS.CODEBLOCK_INDENTATION, this.handleCodeBlockIndentation, this);
};
/* Removes all event listeners attached by this plugin */
CodeBlockPlugin.prototype.removeEventListener = function () {
this.parent.observer.off(EVENTS.CODE_BLOCK, this.applyCodeBlockHandler);
this.parent.observer.off(EVENTS.INTERNAL_DESTROY, this.destroy);
this.parent.observer.off(EVENTS.CODEBLOCK_INDENTATION, this.handleCodeBlockIndentation);
};
/* Destroys the code block plugin instance and cleans up resources */
CodeBlockPlugin.prototype.destroy = function () {
this.removeEventListener();
};
// Handles code block operations based on event type
CodeBlockPlugin.prototype.applyCodeBlockHandler = function (e) {
if (e.subCommand === 'CodeBlock' && !isNullOrUndefined(e.item) && !isNullOrUndefined(e.item.action) && (!isNullOrUndefined(e.event) || e.item.action === 'createCodeBlock')) {
switch (e.item.action) {
case 'createCodeBlock':
this.codeBlockCreation(e);
break;
case 'codeBlockPaste':
this.codeBlockPasteAction(e);
break;
case 'codeBlockEnter':
this.codeBlockEnterAction(e);
break;
case 'codeBlockBackSpace':
this.codeBlockBackSpaceAction(e);
break;
case 'codeBlockTabAction':
this.codeBlockTabAction(e);
break;
case 'codeBlockShiftTabAction':
this.codeBlockShiftTabAction(e);
break;
}
this.callBack(e);
}
};
// Executes the callback function with event details
CodeBlockPlugin.prototype.callBack = function (event) {
if (event.callBack) {
event.callBack({
requestType: event.subCommand,
action: event.item.action,
event: event.event,
editorMode: 'HTML',
range: this.parent.nodeSelection.getRange(this.parent.currentDocument),
elements: this.parent.domNode.blockNodes(true)
});
}
};
/**
* Determines if a node is inside a valid code block structure
*
* This method checks if the given node is within a proper code block structure
* (a PRE element containing a CODE element as its first child).
*
* @param {Node} node - The node to check
* @returns {HTMLElement|null} - The PRE element if the node is inside a valid code block, otherwise null
* @public
*/
CodeBlockPlugin.prototype.isValidCodeBlockStructure = function (node) {
var parentNodes = (node.nodeName === '#text' ? node.parentElement : node);
if (parentNodes !== this.parent.editableElement && !isNullOrUndefined(parentNodes.closest('pre')) && parentNodes.closest('pre').hasAttribute('data-language')
&& !isNullOrUndefined(parentNodes.closest('pre').firstChild) && parentNodes.closest('pre').firstChild.nodeName === 'CODE') {
return parentNodes.closest('pre');
}
return null;
};
/* Determines if the 'Enter' action occurs within a valid code block structure. */
CodeBlockPlugin.prototype.isCodeBlockEnterAction = function (range, e) {
var cursorAtPointer = range.startContainer === range.endContainer &&
range.startOffset === range.endOffset;
if (e.keyCode === 13 && cursorAtPointer && !isNullOrUndefined(this.isValidCodeBlockStructure(range.startContainer))) {
return true;
}
else {
return false;
}
};
// Handles backspace operations within code blocks
CodeBlockPlugin.prototype.codeBlockBackSpaceAction = function (e) {
var range = this.parent.nodeSelection.getRange(this.parent.editableElement.ownerDocument);
var startContainer = range.startContainer.nodeName === '#text' ?
range.startContainer.parentElement : range.startContainer;
var endContainer = range.endContainer.nodeName === '#text' ?
range.endContainer.parentElement : range.endContainer;
if (e.event.type === 'keyup') {
this.handleKeyUpBackspace(range, startContainer, endContainer);
}
else if (e.event.type === 'keydown') {
this.handleKeyDownBackspace(e, range, startContainer, endContainer);
}
};
// Handles keyup backspace operations within code blocks
CodeBlockPlugin.prototype.handleKeyUpBackspace = function (range, startContainer, endContainer) {
if (this.isCodeBlockElement(startContainer) && this.isCodeBlockElement(endContainer)) {
var codeBlockTarget = startContainer.closest('pre[data-language]');
var codeBlock = codeBlockTarget.querySelector('code');
// Handles case when an entire code block is selected and content is deleted
if (isNullOrUndefined(codeBlock) && range.startOffset === 0 &&
range.endOffset === 0 && range.startContainer.nodeName === 'PRE' &&
range.endContainer.nodeName === 'PRE') {
startContainer.closest('pre[data-language]').firstChild.remove();
var br = createElement('br');
var codeElement = createElement('code');
codeElement.appendChild(br);
codeBlockTarget.appendChild(codeElement);
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, br, 0);
}
}
};
// Checks if a node is inside a code block element
CodeBlockPlugin.prototype.isCodeBlockElement = function (node) {
return !isNullOrUndefined(node.closest('pre[data-language]'));
};
// Handles keydown backspace operations within code blocks
CodeBlockPlugin.prototype.handleKeyDownBackspace = function (e, range, startContainer, endContainer) {
// Handle cases where selection spans across code block boundaries
if (isNullOrUndefined(this.isValidCodeBlockStructure(startContainer)) &&
!isNullOrUndefined(this.isValidCodeBlockStructure(endContainer))) {
this.handleSelectionAcrossCodeBlockBoundary(e, range, startContainer, endContainer);
return;
}
// Handle cases where selection spans from code block to regular content
if (!isNullOrUndefined(this.isValidCodeBlockStructure(startContainer)) &&
isNullOrUndefined(this.isValidCodeBlockStructure(endContainer))) {
this.handleSelectionFromCodeBlockToRegular(e, range, startContainer);
return;
}
// Handle single point deletion (Delete or Backspace at a specific position)
if ((e.event.which === 46 || e.event.which === 8) &&
range.startContainer === range.endContainer && range.startOffset === range.endOffset) {
this.handleSinglePointDeletion(e, range, startContainer);
}
};
// Handles selection across code block boundary
CodeBlockPlugin.prototype.handleSelectionAcrossCodeBlockBoundary = function (e, range, startContainer, endContainer) {
e.event.preventDefault();
var codeBlockTarget = this.isValidCodeBlockStructure(endContainer);
var parentNode = this.parent.domNode.getImmediateBlockNode(startContainer);
range.deleteContents();
var cursorOffset = this.parent.nodeSelection.findLastTextPosition(parentNode);
var parentCursorOffset = this.parent.nodeSelection.findLastTextPosition(codeBlockTarget.previousSibling);
var textWrapper = codeBlockTarget.firstChild;
if (textWrapper && parentNode && parentNode.textContent !== '') {
this.moveContentToParent(textWrapper, parentNode, codeBlockTarget);
}
this.setCursorAfterBoundaryOperation(parentNode, codeBlockTarget, cursorOffset, parentCursorOffset);
this.cleanupEmptyElements(codeBlockTarget, parentNode);
};
// Moves content from code block to parent node
CodeBlockPlugin.prototype.moveContentToParent = function (textWrapper, parentNode, codeBlockTarget) {
while (textWrapper && textWrapper.firstChild) {
if (parentNode === this.parent.editableElement) {
parentNode.insertBefore(textWrapper.firstChild, codeBlockTarget);
}
else {
parentNode.appendChild(textWrapper.firstChild);
}
}
};
// Sets cursor position after boundary operation
CodeBlockPlugin.prototype.setCursorAfterBoundaryOperation = function (parentNode, codeBlockTarget, cursorOffset, parentCursorOffset) {
if (parentNode.textContent === '') {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, codeBlockTarget.firstChild, 0);
}
else if (parentNode === this.parent.editableElement) {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, parentCursorOffset.node, parentCursorOffset.offset);
}
else {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, cursorOffset.node, cursorOffset.offset);
}
};
// Cleans up empty elements after operation
CodeBlockPlugin.prototype.cleanupEmptyElements = function (codeBlockTarget, parentNode) {
if (codeBlockTarget.textContent.trim() === '') {
codeBlockTarget.remove();
}
if (parentNode.textContent === '') {
parentNode.remove();
}
};
/* Handles selection from code block to regular content */
CodeBlockPlugin.prototype.handleSelectionFromCodeBlockToRegular = function (e, range, startContainer) {
var _this = this;
var codeBlockTarget = this.isValidCodeBlockStructure(startContainer);
var blockNodes = this.parent.domNode.blockNodes(true);
range.deleteContents();
// Get code element and add BR element if missing
var codeElement = codeBlockTarget.querySelector('code');
if (this.addBrElementIfMissing(codeElement, range)) {
e.event.preventDefault();
return;
}
var items = e.item.currentFormat;
var cursorOffset = this.parent.nodeSelection.findLastTextPosition(codeBlockTarget);
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, cursorOffset.node, cursorOffset.offset);
range = this.parent.nodeSelection.getRange(this.parent.editableElement.ownerDocument);
var validBlockNodes = blockNodes.filter(function (node) {
return _this.parent.editableElement.contains(node);
});
this.setCursorMarkers(range);
this.processCodeBlockAction(range, validBlockNodes, items);
e.event.preventDefault();
};
/* Method to add a BR element to the code element if one doesn't exist */
CodeBlockPlugin.prototype.addBrElementIfMissing = function (codeElement, range) {
if (!isNullOrUndefined(codeElement) && codeElement.childNodes.length === 0) {
var br = createElement('br');
codeElement.appendChild(br);
range.setStartBefore(br);
range.setEndBefore(br);
return true;
}
return false;
};
// Handles single point deletion (Delete or Backspace)
CodeBlockPlugin.prototype.handleSinglePointDeletion = function (e, range, startContainer) {
var keyCode = e.event.which;
var codeBlockElement = this.isValidCodeBlockStructure(startContainer);
// Process delete key at code block boundary
if (keyCode === 46) {
this.handleDeleteKeyAtCodeBlockBoundary(e, range, codeBlockElement);
}
// Process next sibling code block deletion
this.handleNextSiblingCodeBlockDeletion(e, range, keyCode);
// Process backspace at code block start
this.handleBackspaceAtCodeBlockStart(e, range, keyCode, codeBlockElement);
// Process backspace after code block
this.handleBackspaceAfterCodeBlock(e, range, keyCode);
};
// Handles delete key press at code block boundary
CodeBlockPlugin.prototype.handleDeleteKeyAtCodeBlockBoundary = function (e, range, codeBlockElement) {
if (!codeBlockElement) {
return;
}
var rangeElement = (range.startContainer.nodeName === 'CODE')
? range.startContainer.childNodes[range.startOffset]
: range.startContainer;
var isCursorAtPointer = range.startContainer === range.endContainer &&
range.startOffset === range.endOffset;
// Check if the cursor is at a single point and the last child of the code block is a <br>, then remove the <br> to clean up the code block.
if (isCursorAtPointer && this.isBrAsLastChildInCodeBlock(codeBlockElement)) {
var codeElement = codeBlockElement.querySelector('code');
if (codeElement && codeElement.lastChild && codeElement.lastChild.nodeName === 'BR') {
codeElement.removeChild(codeElement.lastChild);
}
}
var rangeIsAtFirstPosition = isCursorAtPointer && codeBlockElement.querySelector('code').childNodes.length === 1 &&
codeBlockElement.querySelector('code').childNodes[0].nodeName === 'BR' && (codeBlockElement.querySelector('code').firstChild === rangeElement);
var isDeleteKey = e.event.which === 46;
// Finds the next sibling to merge its content into the code block when the delete key is pressed,
// the cursor is at the end of the code block, and the next sibling is not null.
var elementNextSibling = this.findNextValidSibling(codeBlockElement);
var lastNode = isDeleteKey &&
!isNullOrUndefined(codeBlockElement) &&
!isNullOrUndefined(elementNextSibling) &&
(this.isValidCodeBlockStructure(range.startContainer).querySelector('code').lastChild === rangeElement) &&
rangeElement.textContent.length === (rangeElement.nodeName === 'BR' ? 0 : range.startOffset);
var codeBlockNextSiblingIsNull = rangeIsAtFirstPosition && isNullOrUndefined(elementNextSibling);
var codeBlockNextSibling = rangeIsAtFirstPosition && !isNullOrUndefined(elementNextSibling);
var codeBlockPreviousSiblingIsNull = rangeIsAtFirstPosition && isNullOrUndefined(codeBlockElement.previousSibling);
if (codeBlockNextSiblingIsNull && codeBlockPreviousSiblingIsNull) {
this.handleActionWhenNextSiblingIsNull(e, codeBlockElement);
}
else if (codeBlockNextSibling) {
this.handleActionWhenNextSiblingExists(e, codeBlockElement, elementNextSibling);
}
else if (lastNode) {
e.event.preventDefault();
this.mergeNextContentIntoCodeBlock(codeBlockElement, rangeElement);
}
};
/* This method checks if the last child of the code block's code element is a <br>
and ensures it is not preceded by another <br> to prevent consecutive empty lines. */
CodeBlockPlugin.prototype.isBrAsLastChildInCodeBlock = function (codeBlockElement) {
var codeElement = codeBlockElement.querySelector('code');
var lastChild = codeElement.lastChild;
var previousSibling = lastChild ? lastChild.previousSibling : null;
return lastChild && lastChild.nodeName === 'BR' &&
(!isNullOrUndefined(previousSibling) && previousSibling.nodeName !== 'BR');
};
/*
* Handle action when the next sibling is null
*/
CodeBlockPlugin.prototype.handleActionWhenNextSiblingIsNull = function (e, codeBlockElement) {
e.event.preventDefault();
this.nodeCreateBasedOnEnterAction(codeBlockElement, e.enterAction);
detach(codeBlockElement);
};
/*
* Handle action when the next sibling exists
*/
CodeBlockPlugin.prototype.handleActionWhenNextSiblingExists = function (e, codeBlockElement, elementNextSibling) {
e.event.preventDefault();
var firstPosition = this.parent.nodeSelection.findFirstContentNode(elementNextSibling);
if (firstPosition.node.nodeName === 'BR') {
var newRange = this.parent.editableElement.ownerDocument.createRange();
newRange.setStartBefore(firstPosition.node);
newRange.setEndBefore(firstPosition.node);
this.parent.nodeSelection.setRange(this.parent.currentDocument, newRange);
}
else {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, firstPosition.node, 0);
}
this.processListElement(codeBlockElement);
};
/*
* Process the list element for the code block
*/
CodeBlockPlugin.prototype.processListElement = function (codeBlockElement) {
var listElement = codeBlockElement.parentElement;
if (listElement && listElement.nodeName !== 'LI' && listElement.lastChild === codeBlockElement) {
listElement = codeBlockElement.closest('li');
}
detach(codeBlockElement);
if (listElement && listElement.nodeName === 'LI') {
var parentList = listElement.parentElement;
detach(listElement);
if (parentList &&
(parentList.nodeName === 'UL' || parentList.nodeName === 'OL') &&
!parentList.querySelector('li')) {
detach(parentList);
}
}
};
/*
* Finds the next valid sibling element until reaching the parent.editableElement.
* If no sibling exists, climbs up through parents to find a sibling.
* @param element The current element to start searching from
* @returns The next valid sibling HTMLElement or null if none found
*/
CodeBlockPlugin.prototype.findNextValidSibling = function (element) {
var current = element.nextSibling;
if (isNullOrUndefined(current) && element !== this.parent.editableElement) {
var parent_1 = element.parentElement;
while (parent_1 && parent_1 !== this.parent.editableElement) {
current = parent_1.nextSibling;
while (current && !this.isValidNode(current)) {
current = current.nextSibling;
}
if (current && (current.nodeName === 'TH' || current.nodeName === 'TD')) {
return null;
}
if (current) {
break;
}
parent_1 = parent_1.parentElement;
}
}
// If the current node is a list, find the first <li> within it.
if (current && (current.nodeName === 'UL' || current.nodeName === 'OL')) {
var firstListItem = this.findFirstListItem(current);
if (firstListItem) {
return firstListItem;
}
}
return current;
};
/* Helper method to find the first <li> in (potentially nested) lists */
CodeBlockPlugin.prototype.findFirstListItem = function (listElement) {
for (var i = 0; i < listElement.childNodes.length; i++) {
var child = listElement.childNodes[i];
if (child.nodeName === 'LI') {
var nestedList = child.querySelector('OL,UL');
if (nestedList) {
var nestedListItem = this.findFirstListItem(nestedList);
if (nestedListItem) {
return nestedListItem;
}
}
return child;
}
}
return null;
};
CodeBlockPlugin.prototype.isValidNode = function (node) {
if (node.nodeType === Node.ELEMENT_NODE) {
return true;
}
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent.trim().length > 0;
}
return false;
};
CodeBlockPlugin.prototype.nodeCreateBasedOnEnterAction = function (target, enterAction) {
var enterElement = createElement(enterAction);
var br = createElement('br');
if (enterAction === 'P' || enterAction === 'DIV') {
enterElement.appendChild(br);
}
target.parentNode.insertBefore(enterElement, target);
if (enterAction === 'BR') {
var newRange = this.parent.editableElement.ownerDocument.createRange();
newRange.setStartBefore(enterElement);
newRange.setEndBefore(enterElement);
this.parent.nodeSelection.setRange(this.parent.currentDocument, newRange);
}
else {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, enterElement, 0);
}
};
// Merges next content into code block
CodeBlockPlugin.prototype.mergeNextContentIntoCodeBlock = function (codeBlockElement, rangeElement) {
var nextSibling = this.findNextValidSibling(codeBlockElement);
var codeElement = codeBlockElement.querySelector('code');
if (nextSibling.nodeName === 'BR') {
this.processNode(nextSibling, codeElement);
detach(nextSibling);
}
else if (!isNullOrUndefined(nextSibling)) {
if (rangeElement.nodeName === 'BR') {
detach(rangeElement);
}
if (!this.parent.domNode.isBlockNode(nextSibling)) {
this.processInlineNextSiblings(nextSibling, codeElement);
}
else {
this.processNode(nextSibling, codeElement);
if (nextSibling && nextSibling.nodeName === 'LI' && nextSibling.parentElement.querySelectorAll('li').length === 1) {
detach(nextSibling.parentElement);
}
else {
nextSibling.parentNode.removeChild(nextSibling);
}
}
}
};
// Processes inline next siblings into code block
CodeBlockPlugin.prototype.processInlineNextSiblings = function (startNode, codeElement) {
var nextSibling = startNode;
var shouldContinue = true;
while (nextSibling && shouldContinue) {
var currentSibling = nextSibling;
nextSibling = nextSibling.nextSibling;
if (currentSibling.nodeName !== 'BR' &&
!this.parent.domNode.isBlockNode(currentSibling)) {
this.processNode(currentSibling, codeElement);
currentSibling.parentNode.removeChild(currentSibling);
}
else {
shouldContinue = false;
}
}
};
// Handles next sibling code block deletion
CodeBlockPlugin.prototype.handleNextSiblingCodeBlockDeletion = function (e, range, keyCode) {
var immediateBlockNode = this.parent.domNode.getImmediateBlockNode(range.startContainer);
var blockNode = immediateBlockNode !== this.parent.editableElement ?
immediateBlockNode : range.startContainer;
var lastPosition = this.parent.nodeSelection.findLastTextPosition(blockNode);
var cursorAtLastPosition = lastPosition &&
lastPosition.node === range.startContainer &&
lastPosition.offset === range.startOffset;
var isNextSiblingCodeBlock = this.findParentOrNextSiblingCodeBlock(range);
var nextSiblingElemCodeBlock = (keyCode === 46) &&
!isNullOrUndefined(isNextSiblingCodeBlock) && cursorAtLastPosition;
if (nextSiblingElemCodeBlock) {
e.event.preventDefault();
this.mergeCodeBlockWithCurrentNode(isNextSiblingCodeBlock);
}
};
// Merges code block with current node
CodeBlockPlugin.prototype.mergeCodeBlockWithCurrentNode = function (nodeInfo) {
var codeBlockElement = nodeInfo.nextSibling;
var codeElement = codeBlockElement.querySelector('code');
var currentNode = nodeInfo.currentNode;
if (this.parent.domNode.isBlockNode(currentNode)) {
while (codeElement.firstChild) {
var child = codeElement.firstChild;
currentNode.appendChild(child);
}
codeBlockElement.parentNode.removeChild(codeBlockElement);
}
else {
var parentNode = currentNode.parentNode;
var nextSibling = currentNode.nextSibling;
while (codeElement.firstChild) {
var child = codeElement.firstChild;
parentNode.insertBefore(child, nextSibling);
}
codeBlockElement.parentNode.removeChild(codeBlockElement);
}
};
// Handles backspace at code block start position
CodeBlockPlugin.prototype.handleBackspaceAtCodeBlockStart = function (e, range, keyCode, codeBlockElement) {
var firstPosition = this.parent.nodeSelection.findFirstContentNode(this.parent.domNode.getImmediateBlockNode(range.startContainer));
var cursorAtFirstPosition = firstPosition.node &&
firstPosition.node === (range.startContainer.nodeName === 'CODE' ?
range.startContainer.firstChild : range.startContainer) &&
range.startOffset === 0;
var isCodeBlockCurrentElement = (keyCode === 8) &&
!isNullOrUndefined(codeBlockElement) && cursorAtFirstPosition;
if (isCodeBlockCurrentElement) {
e.event.preventDefault();
var codeElement = codeBlockElement.querySelector('code');
if (!isNullOrUndefined(codeBlockElement.previousSibling)) {
var previousElement = codeBlockElement.previousSibling;
var cursorOffset = this.parent.nodeSelection.findLastTextPosition(previousElement);
this.mergePreviousElementWithCodeBlock(previousElement, codeElement, codeBlockElement);
if (!isNullOrUndefined(previousElement)) {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, cursorOffset.node, cursorOffset.offset);
}
}
else if (isNullOrUndefined(codeBlockElement.previousSibling)) {
if (codeBlockElement.textContent.length === 0) {
this.nodeCreateBasedOnEnterAction(codeBlockElement, e.enterAction);
detach(codeBlockElement);
}
}
}
};
// Merges previous element with code block
CodeBlockPlugin.prototype.mergePreviousElementWithCodeBlock = function (previousElement, codeElement, codeBlockElement) {
if (this.parent.domNode.isBlockNode(previousElement)) {
while (codeElement.firstChild) {
var child = codeElement.firstChild;
previousElement.appendChild(child);
}
codeBlockElement.parentNode.removeChild(codeBlockElement);
}
else {
// Logic for inline elements
var parentNode = previousElement.parentNode;
var nextSibling = previousElement.nextSibling;
while (codeElement.firstChild) {
var child = codeElement.firstChild;
parentNode.insertBefore(child, nextSibling);
}
codeBlockElement.parentNode.removeChild(codeBlockElement);
}
};
// Handles backspace after code block
CodeBlockPlugin.prototype.handleBackspaceAfterCodeBlock = function (e, range, keyCode) {
var immediateBlockNode = this.parent.domNode.getImmediateBlockNode(range.startContainer);
var blockNode = immediateBlockNode !== this.parent.editableElement ? immediateBlockNode : range.startContainer;
var firstPosition = this.parent.nodeSelection.findFirstContentNode(blockNode);
var cursorAtFirstPosition = firstPosition.node &&
firstPosition.node === (range.startContainer.nodeName === 'CODE' ?
range.startContainer.firstChild : range.startContainer) &&
range.startOffset === 0;
var isBlockElement = this.findParentOrPreviousSiblingCodeBlock(range);
// Check if the current node is a table
var isCurrentNodeTable = isBlockElement && isBlockElement.currentNode.nodeName === 'TABLE';
var backspacePreviousCodeBlock = (keyCode === 8) &&
!isNullOrUndefined(isBlockElement) && cursorAtFirstPosition && !isCurrentNodeTable;
if (backspacePreviousCodeBlock) {
e.event.preventDefault();
this.mergePreviousCodeBlockWithCurrent(isBlockElement);
}
};
// Merges previous code block with current element
CodeBlockPlugin.prototype.mergePreviousCodeBlockWithCurrent = function (blockInfo) {
var _this = this;
var codeBlockElement = blockInfo.previousSibling;
var codeElement = codeBlockElement.querySelector('code');
var currentNode = blockInfo.currentNode;
var insertPosition = null;
var isFirstNode = true;
var processAndTrackNode = function (node, targetElement) {
_this.processNode(node, targetElement);
if (isFirstNode) {
insertPosition = targetElement.lastChild;
isFirstNode = false;
}
};
if (this.parent.domNode.isBlockNode(currentNode)) {
this.processMergeBlockNode(currentNode, codeElement, processAndTrackNode);
}
else {
this.processMergeInlineNode(currentNode, codeElement, processAndTrackNode);
}
if (insertPosition) {
if (insertPosition.nodeType === Node.TEXT_NODE) {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, insertPosition, 0);
}
}
};
// Processes merge of block node with code element
CodeBlockPlugin.prototype.processMergeBlockNode = function (currentNode, codeElement, processFunc) {
while (currentNode.firstChild) {
var child = currentNode.firstChild;
currentNode.removeChild(child);
processFunc(child, codeElement);
}
currentNode.parentNode.removeChild(currentNode);
};
// Processes merge of inline node with code element
CodeBlockPlugin.prototype.processMergeInlineNode = function (startNode, codeElement, processFunc) {
var node = startNode;
while (node) {
var nextSibling = node.nextSibling;
if (node.nodeName === 'BR' || this.parent.domNode.isBlockNode(node)) {
break;
}
if (node.parentNode) {
node.parentNode.removeChild(node);
}
processFunc(node, codeElement);
node = nextSibling;
}
};
// Handles Enter key press within code blocks
CodeBlockPlugin.prototype.codeBlockEnterAction = function (e) {
var range = this.parent.nodeSelection.getRange(this.parent.editableElement.ownerDocument);
var startContainer = range.startContainer.nodeName === '#text' ?
range.startContainer.parentElement : range.startContainer;
var endContainer = range.endContainer.nodeName === '#text' ?
range.endContainer.parentElement : range.endContainer;
// Check if cursor is inside a code block at a specific position
if (this.isCodeBlockPointSelection(range, startContainer, endContainer)) {
this.handleCodeBlockPointSelection(e, range, startContainer);
}
// Handle selection entirely within code block
else if (this.isSelectionWithinCodeBlock(range, startContainer, endContainer)) {
this.handleSelectionWithinCodeBlock(e, range);
}
// Handle selection starting outside code block but ending inside
else if (this.isSelectionOutsideToInside(startContainer, endContainer)) {
this.handleOutsideToInsideSelection(e, range, endContainer);
}
// Handle selection starting inside code block but ending outside
else if (this.isSelectionInsideToOutside(startContainer, endContainer)) {
this.handleInsideToOutsideSelection(e, range, startContainer);
}
};
// Checks if the selection is a point selection inside a code block
CodeBlockPlugin.prototype.isCodeBlockPointSelection = function (range, startContainer, endContainer) {
var isPointSelection = range.startContainer === range.endContainer &&
range.startOffset === range.endOffset;
var inCodeBlock = !isNullOrUndefined(startContainer.closest('pre[data-language]')) &&
!isNullOrUndefined(endContainer.closest('pre[data-language]'));
if (!isPointSelection || !inCodeBlock) {
return false;
}
var firstAppend = this.isFirstAppendScenario(range);
var lastAppend = this.isLastAppendScenario(range);
return firstAppend || lastAppend;
};
// Checks if it's a first append scenario inside a code block
CodeBlockPlugin.prototype.isFirstAppendScenario = function (range) {
return range.startOffset === 0 && range.endOffset === 0 &&
range.startContainer.nodeName === 'CODE' &&
range.startContainer.childNodes[range.startOffset] &&
range.endContainer.childNodes[range.endOffset] &&
range.startContainer.childNodes[range.endOffset].nodeName === 'BR' &&
range.startContainer.childNodes.length > 1 &&
!isNullOrUndefined(range.startContainer.childNodes[range.endOffset + 1]);
};
// Checks if it's a last append scenario inside a code block
CodeBlockPlugin.prototype.isLastAppendScenario = function (range) {
return range.startContainer.nodeName === 'CODE' &&
range.startContainer.childNodes[range.startOffset] &&
range.startContainer.childNodes[range.startOffset].nodeName === 'BR' &&
range.startContainer.lastChild === range.startContainer.childNodes[range.startOffset] &&
!isNullOrUndefined(range.startContainer.childNodes[range.startOffset - 1]) &&
range.startContainer.childNodes[range.startOffset - 1].nodeName === 'BR' &&
!isNullOrUndefined(range.startContainer.childNodes[range.startOffset - 2]) &&
range.startContainer.childNodes[range.startOffset - 2].nodeName === 'BR';
};
// Handles point selection inside a code block
CodeBlockPlugin.prototype.handleCodeBlockPointSelection = function (e, range, startContainer) {
var codeBlock = this.isValidCodeBlockStructure(startContainer);
// Check if the code block is inside a list
var listElement = codeBlock.closest('UL,OL');
if (!isNullOrUndefined(listElement)) {
e.event.preventDefault();
// Create new list item element based on enter action
var enterElement = createElement('LI');
var br = createElement('br');
enterElement.appendChild(br);
var isFirstAppend = this.isFirstAppendScenario(range);
var codeBlockCodeElement = codeBlock.querySelector('code');
if (isFirstAppend) {
detach(codeBlockCodeElement.firstChild);
listElement.insertBefore(enterElement, codeBlock.closest('li'));
}
else {
detach(codeBlockCodeElement.lastChild);
detach(codeBlockCodeElement.lastChild);
var currentListItem = codeBlock.closest('li');
var nextListElement = currentListItem.querySelectorAll('UL,OL');
for (var i = 0; i < nextListElement.length; i++) {
if (nextListElement[i].nodeName === 'UL' || nextListElement[i].nodeName === 'OL') {
enterElement.appendChild(nextListElement[i]);
}
}
listElement.insertBefore(enterElement, currentListItem.nextElementSibling);
}
this.setNewRangeBeforeBrElement(br);
}
else {
e.event.preventDefault();
// Create new element based on enter action
var enterElement = createElement(e.enterAction);
var br = createElement('br');
if (e.enterAction === 'P' || e.enterAction === 'DIV') {
enterElement.appendChild(br);
}
var isFirstAppend = this.isFirstAppendScenario(range);
var codeBlockCodeElement = codeBlock.querySelector('code');
if (isFirstAppend) {
detach(codeBlockCodeElement.firstChild);
codeBlock.parentElement.insertBefore(enterElement, codeBlock);
}
else {
detach(codeBlockCodeElement.lastChild);
detach(codeBlockCodeElement.lastChild);
codeBlock.parentElement.insertBefore(enterElement, codeBlock.nextElementSibling);
}
this.setNewRangeBeforeBrElement(br);
}
};
CodeBlockPlugin.prototype.setNewRangeBeforeBrElement = function (element) {
var newRange = this.parent.editableElement.ownerDocument.createRange();
newRange.setStartBefore(element);
newRange.setEndBefore(element);
this.parent.nodeSelection.setRange(this.parent.currentDocument, newRange);
};
// Checks if selection is entirely within a code block
CodeBlockPlugin.prototype.isSelectionWithinCodeBlock = function (range, startContainer, endContainer) {
return (!isNullOrUndefined(this.isValidCodeBlockStructure(startContainer)) &&
!isNullOrUndefined(this.isValidCodeBlockStructure(endContainer)));
};
// Handles selection that's entirely within a code block
CodeBlockPlugin.prototype.handleSelectionWithinCodeBlock = function (e, range) {
e.event.preventDefault();
var codeBlock = this.isValidCodeBlockStructure(range.startContainer);
// Delete selection contents if range spans multiple positions
if (range.startContainer !== range.endContainer || range.startOffset !== range.endOffset) {
range.deleteContents();
}
var codeElement = range.endContainer.nodeName === '#text' ?
range.endContainer.parentElement.closest('code') :
range.endContainer.closest('code');
var isAtEnd = codeElement && codeElement.lastChild === range.startContainer &&
(range.endOffset === (range.endContainer.nodeType === Node.TEXT_NODE ?
range.endContainer.textContent.length : range.endContainer.childNodes.length));
var addExtraBrElement = isAtEnd &&
(isNullOrUndefined(range.startContainer.nextSibling) ||
(!isNullOrUndefined(range.startContainer.nextSibling) &&
range.startContainer.nextSibling.nodeName !== 'BR'));
var br = createElement('br');
range.insertNode(br);
if (addExtraBrElement) {
var extraBr = createElement('br');
br.parentNode.insertBefore(extraBr, br.nextSibling);
}
codeBlock.normalize();
// Check if there's a text node after the BR
var nextNode = br.nextSibling;
if (nextNode && nextNode.nodeType === Node.TEXT_NODE) {
// Set range to beginning of the text node
range.setStart(nextNode, 0);
range.setEnd(nextNode, 0);
}
else {
range.setStartAfter(br);
range.setEndAfter(br);
}
this.parent.nodeSelection.setRange(this.parent.currentDocument, range);
};
// Checks if selection starts outside code block but ends inside
CodeBlockPlugin.prototype.isSelectionOutsideToInside = function (startContainer, endContainer) {
return isNullOrUndefined(this.isValidCodeBlockStructure(startContainer)) &&
!isNullOrUndefined(this.isValidCodeBlockStructure(endContainer));
};
// Handles selection from outside to inside code block
CodeBlockPlugin.prototype.handleOutsideToInsideSelection = function (e, range, endContainer) {
e.event.preventDefault();
var codeBlock = this.isValidCodeBlockStructure(endContainer);
var codeElement = codeBlock.querySelector('code');
var codeBlockPreviousSibling = codeBlock.previousSibling;
// Determine if the entire content of the code block is selected
var isFullCodeBlockSelection = range.endContainer === codeElement.lastChild &&
range.endOffset === codeElement.lastChild.textContent.length;
range.deleteContents();
if (isFullCodeBlockSelection && codeBlockPreviousSibling.textContent.length === 0) {
this.nodeCreateBasedOnEnterAction(codeBlock, e.enterAction);
codeBlock.parentNode.removeChild(codeBlock);
if (codeBlockPreviousSibling.textContent.length === 0) {
codeBlockPreviousSibling.parentNode.removeChild(codeBlockPreviousSibling);
}
}
else if (codeElement && codeElement.childNodes.length !== 0 && codeElement.textContent !== '') {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, codeBlock, 0);
}
else if (codeElement && codeElement.children.length === 0 &&
codeElement.childNodes[0] && codeElement.childNodes[0].nodeName !== 'BR') {
var br = createElement('br');
codeElement.appendChild(br);
range.setStartAfter(br);
range.setEndAfter(br);
}
};
// Checks if selection starts inside code block but ends outside
CodeBlockPlugin.prototype.isSelectionInsideToOutside = function (startContainer, endContainer) {
return !isNullOrUndefined(this.isValidCodeBlockStructure(startContainer)) &&
isNullOrUndefined(this.isValidCodeBlockStructure(endContainer));
};
// Handles selection from inside to outside code block
CodeBlockPlugin.prototype.handleInsideToOutsideSelection = function (e, range, startContainer) {
e.event.preventDefault();
range.deleteContents();
var codeBlock = this.isValidCodeBlockStructure(startContainer);
var codeElement = codeBlock.querySelector('code');
if (codeElement && codeElement.innerHTML === '' &&
codeElement.childNodes[0] && codeElement.childNodes[0].nodeName !== 'BR') {
var br = createElement('br');
codeElement.appendChild(br);
range.setStartAfter(br);
range.setEndAfter(br);
}
// Set cursor to next node after the code block
var nextNode = codeBlock.nextSibling;
if (nextNode) {
if (nextNode.nodeType === Node.TEXT_NODE) {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, nextNode, 0);
}
else {
var firstTextNode = this.findFirstTextNode(nextNode);
if (firstTextNode) {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, firstTextNode, 0);
}
else {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, nextNode, 0);
}
}
}
};
/* Recursively searches for the first text node within a DOM element
* Returns the first text node found during depth-first search
* or null if no text nodes exist within the element
*/
CodeBlockPlugin.prototype.findFirstTextNode = function (node) {
if (node.nodeType === Node.TEXT_NODE) {
return node;
}
for (var i = 0; i < node.childNodes.length; i++) {
var textNode = this.findFirstTextNode(node.childNodes[i]);
if (!isNullOrUndefined(textNode)) {
return textNode;
}
}
return null;
};
/* Gets the current selection range from the document
* Returns the first range in the current selection
*/
CodeBlockPlugin.prototype.getSelectionRange = function () {
var selection = this.parent.editableElement.ownerDocument.defaultView.getSelection();
return selection.getRangeAt(0);
};
// Handles paste operations in code blocks by processing clipboard data
CodeBlockPlugin.prototype.codeBlockPasteAction = function (e) {
var range = this.parent.nodeSelection.getRange(this.parent.editableElement.ownerDocument);
var startContainer = range.startContainer.nodeName === '#text' ?
range.startContainer.parentElement : range.startContainer;
var endContainer = range.endContainer.nodeName === '#text' ?
range.endContainer.parentElement : range.endContainer;
if (!this.isValidCodeBlockStructure(startContainer) &&
!this.isValidCodeBlockStructure(endContainer)) {
return;
}
e.event.preventDefault();
var clipboardData = e.event.clipboardData;
var plainText = clipboardData.getData('text/plain');
if (!range) {
return;
}
if (this.isPointSelection(range)) {
this.handlePointSelectionPaste(range, plainText);
}
else if (this.isSameCodeBlockSelection(startContainer, endContainer)) {
this.handleSameCodeBlockPaste(range, plainText);
}
else {
this.handleCrossCodeBlockPaste(range, plainText, startContainer, endContainer, e);
}
};
/* Determines if the range represents a collapsed selection (caret position)
* Returns true if selection is a single point rather than a range of content
*/
CodeBlockPlugin.prototype.isPointSelection = function (range) {
return range.startContainer === range.endContainer && range.startOffset === range.endOffset;
};
// Determines if both selection endpoints are within the same code block
CodeBlockPlugin.prototype.isSameCodeBlockSelection = function (startContainer, endContainer) {
return !isNullOrUndefined(this.isValidCodeBlockStructure(endContainer)) &&
!isNullOrUndefined(this.isValidCodeBlockStructure(startContainer)) &&
this.isValidCodeBlockStructure(endContainer) === this.isValidCodeBlockStructure(startContainer);
};
// Inserts plain text at cursor position when there's a point selection
CodeBlockPlugin.prototype.handlePointSelectionPaste = function (range, plainText) {
var codeBlockElement = this.isValidCodeBlockStructure(range.startContainer);
var textNode = document.createTextNode(plainText);
range.insertNode(textNode);
var cursorOffset = this.parent.nodeSelection.findLastTextPosition(textNode);
if (cursorOffset && cursorOffset.node) {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, cursorOffset.node, cursorOffset.offset);
}
codeBlockElement.normalize();
};
// Replaces selected content with pasted text when selection is within same code block
CodeBlockPlugin.prototype.handleSameCodeBlockPaste = function (range, plainText) {
var textNode = document.createTextNode(plainText);
range.deleteContents();
range.insertNode(textNode);
var cursorOffset = this.parent.nodeSelection.findLastTextPosition(textNode);
if (cursorOffset && cursorOffset.node) {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, cursorOffset.node, cursorOffset.offset);
}
};
// Handles complex paste operations that span across code blocks or between code block and regular content
CodeBlockPlugin.prototype.handleCrossCodeBlockPaste = function (range, plainText, startContainer, endContainer, e) {
var _this = this;
var blockNodes = this.parent.domNode.blockNodes();
range.deleteContents();
var textNode = document.createTextNode(plainText);
if (this.isValidCodeBlockStructure(endContainer)) {
var codeElement = this.isValidCodeBlockStructure(endContainer)
.querySelector('code');
codeElement.insertBefore(textNode, codeElement.firstChild);
}
else if (this.isValidCodeBlockStructure(startContainer)) {
var codeElement = this.isValidCodeBlockStructure(startContainer)
.querySelector('code');
codeElement.appendChild(textNode);
}
var cursorOffset = this.parent.nodeSelection.findLastTextPosition(textNode);
if (cursorOffset && cursorOffset.node) {
this.parent.nodeSelection.setCursorPoint(this.parent.currentDocument, cursorOffset.node, cursorOffset.offset);
var updatedRange = this.getSelectionRange();
var validBlockNodes = blockNodes.filter(function (node) {
return _this.parent.editableElement.contains(node);
});
this.setCursorMarkers(updatedRange);
var items = e.item.currentFormat;
this.processCodeBlockAction(updatedRange, validBlockNodes, items);
}
};
// Extracts content from code block and converts it to regular elements
CodeBlockPlugin.prototype.extractAndWrapCodeBlockContent = function (