UNPKG

@syncfusion/ej2-richtexteditor

Version:
968 lines (966 loc) 87.2 kB
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 (