UNPKG

@chayns-components/emoji-input

Version:
586 lines (563 loc) • 21.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.unwrapIgnoreEmojiSpanAtCursor = exports.setCursorPositionByAbsIndex = exports.setChildIndex = exports.saveSelection = exports.restoreSelection = exports.moveSelectionOffset = exports.moveCursorOutOfIgnoreEmojiSpan = exports.insertPseudoMarker = exports.insertInvisibleCursorMarker = exports.insertCursorAtMarker = exports.getCurrentCursorPosition = exports.getCharCodeThatWillBeDeleted = exports.findAndSelectText = void 0; var _number = require("./number"); var _text = require("./text"); let childIndex = -1; let endOffset = -1; let startOffset = -1; const saveSelection = (element, { shouldIgnoreEmptyTextNodes } = {}) => { const selection = window.getSelection(); if (!selection) { return; } const { anchorNode } = selection; if (!anchorNode) { return; } const range = selection.getRangeAt(0); let childNodesArray = Array.from(element.childNodes); if (shouldIgnoreEmptyTextNodes) { childNodesArray = childNodesArray.filter(({ nodeType, nodeValue }) => nodeType !== Node.TEXT_NODE || nodeValue !== '' && nodeValue !== '\u200B'); } childIndex = childNodesArray.indexOf(anchorNode); endOffset = range.endOffset; startOffset = range.startOffset; }; exports.saveSelection = saveSelection; const restoreSelection = element => { // Search for \u200C in child nodes. If found, set the childIndex, startOffset, and endOffset to the // position of the \u200C character. Also remove the \u200C character from the child node. If not found, // the childIndex, startOffset, and endOffset will be like before. const childNodesArray = Array.from(element.childNodes); let hasFoundNoJoiner = false; childNodesArray.forEach(node => { if (!hasFoundNoJoiner && node.nodeType === Node.TEXT_NODE && typeof node.nodeValue === 'string') { const noJoinerIndex = node.nodeValue.indexOf('\u200C'); if (noJoinerIndex !== -1) { hasFoundNoJoiner = true; childIndex = childNodesArray.indexOf(node); startOffset = noJoinerIndex; endOffset = noJoinerIndex; } } }); // Remove all no joiner characters from the child nodes if no joiner was found if (hasFoundNoJoiner) { childNodesArray.forEach(node => { if (node.nodeType === Node.TEXT_NODE && typeof node.nodeValue === 'string') { // eslint-disable-next-line no-param-reassign node.nodeValue = node.nodeValue.replace(/\u200C/g, ''); } }); } let childNode = element.childNodes[childIndex]; const selection = window.getSelection(); if (!childNode || !element || !selection) { return; } // noinspection SuspiciousTypeOfGuard if (typeof childNode.nodeValue !== 'string') { const elementTextLength = (0, _text.getElementTextLength)(childNode); if (childNode.nextSibling) { childNode = childNode.nextSibling; if (childNode.nodeType === Node.TEXT_NODE && childNode.nodeValue) { endOffset -= elementTextLength; startOffset -= elementTextLength; if (childNode.nodeValue.charCodeAt(endOffset) === 8203) { endOffset += 1; startOffset += 1; } } else { var _childNode$parentNode; const textNode = document.createTextNode('\u200B'); (_childNode$parentNode = childNode.parentNode) === null || _childNode$parentNode === void 0 || _childNode$parentNode.insertBefore(textNode, childNode.nextSibling); childNode = textNode; endOffset = textNode.length; startOffset = textNode.length; } } else { var _childNode$parentNode2; const textNode = document.createTextNode('\u200B'); (_childNode$parentNode2 = childNode.parentNode) === null || _childNode$parentNode2 === void 0 || _childNode$parentNode2.insertBefore(textNode, childNode.nextSibling); childNode = textNode; endOffset = textNode.length; startOffset = textNode.length; } } else if (childNode.nodeValue && endOffset > childNode.nodeValue.length) { var _childNode$nextSiblin; if ((_childNode$nextSiblin = childNode.nextSibling) !== null && _childNode$nextSiblin !== void 0 && _childNode$nextSiblin.nodeValue) { let elementTextLength = childNode.nodeValue.length; childNode = childNode.nextSibling; // noinspection SuspiciousTypeOfGuard if (typeof childNode.nodeValue !== 'string') { var _childNode$nextSiblin2; elementTextLength += (0, _text.getElementTextLength)(childNode); if ((_childNode$nextSiblin2 = childNode.nextSibling) !== null && _childNode$nextSiblin2 !== void 0 && _childNode$nextSiblin2.nodeValue) { childNode = childNode.nextSibling; if (childNode.nodeType === Node.TEXT_NODE && childNode.nodeValue) { endOffset -= elementTextLength; startOffset -= elementTextLength; if (childNode.nodeValue.charCodeAt(endOffset) === 8203) { endOffset += 1; startOffset += 1; } } else { var _childNode$parentNode3; const textNode = document.createTextNode('\u200B'); (_childNode$parentNode3 = childNode.parentNode) === null || _childNode$parentNode3 === void 0 || _childNode$parentNode3.insertBefore(textNode, childNode.nextSibling); childNode = textNode; endOffset = textNode.length; startOffset = textNode.length; } } else { var _childNode$parentNode4; const textNode = document.createTextNode('\u200B'); (_childNode$parentNode4 = childNode.parentNode) === null || _childNode$parentNode4 === void 0 || _childNode$parentNode4.insertBefore(textNode, childNode.nextSibling); childNode = textNode; endOffset = textNode.length; startOffset = textNode.length; } } } else { endOffset = childNode.nodeValue.length; startOffset = childNode.nodeValue.length; } } const range = document.createRange(); // noinspection SuspiciousTypeOfGuard if (typeof childNode.nodeValue === 'string') { startOffset = (0, _number.clamp)(startOffset, 0, childNode.nodeValue.length); endOffset = (0, _number.clamp)(endOffset, 0, childNode.nodeValue.length); } try { range.setStart(childNode, startOffset); range.setEnd(childNode, endOffset); } catch (error) { // Do nothing } selection.removeAllRanges(); selection.addRange(range); range.collapse(true); }; exports.restoreSelection = restoreSelection; const insertInvisibleCursorMarker = () => { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) { return; } const range = selection.getRangeAt(0); const textNode = range.startContainer; const offset = range.startOffset; // noinspection JSDeprecatedSymbols document.execCommand('delete', false); const span = document.createElement('span'); span.style.display = 'inline-block'; span.style.width = '0'; span.style.height = '0'; span.className = 'invisible-cursor-marker'; const parent = textNode.parentNode; if (parent) { if (textNode.nodeType === Node.TEXT_NODE) { const textContent = textNode.textContent || ''; const beforeText = textContent.slice(0, offset); const afterText = textContent.slice(offset); textNode.textContent = beforeText; parent.insertBefore(span, textNode.nextSibling); const afterTextNode = document.createTextNode(afterText); parent.insertBefore(afterTextNode, span.nextSibling); setTimeout(() => { // Set cursor to cursor element const newRange = document.createRange(); newRange.setStartAfter(span); newRange.setEndAfter(span); selection.removeAllRanges(); selection.addRange(newRange); // Remove cursor element span.remove(); }, 10); } } }; exports.insertInvisibleCursorMarker = insertInvisibleCursorMarker; const insertPseudoMarker = () => { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const marker = document.createElement('span'); marker.id = 'cursor-marker'; marker.style.display = 'inline-block'; marker.style.width = '0'; marker.style.height = '0'; range.insertNode(marker); } }; exports.insertPseudoMarker = insertPseudoMarker; const insertCursorAtMarker = ref => { var _ref$current; const marker = (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.querySelector('#cursor-marker'); if (marker) { const selection = window.getSelection(); const range = document.createRange(); range.setStartAfter(marker); range.setEndAfter(marker); selection === null || selection === void 0 || selection.removeAllRanges(); selection === null || selection === void 0 || selection.addRange(range); marker.remove(); } }; exports.insertCursorAtMarker = insertCursorAtMarker; const moveSelectionOffset = distance => { endOffset += distance; startOffset += distance; }; exports.moveSelectionOffset = moveSelectionOffset; const setChildIndex = index => { childIndex = index; }; /** * This function returns the code of the character that will be removed by the KeyDown event in the * next step, if the "Backspace" or "Delete" key was pressed and there is no selection of multiple * characters. * * @param event - Keyboard event from "onKeyDown" */ exports.setChildIndex = setChildIndex; const getCharCodeThatWillBeDeleted = event => { var _window$getSelection, _nextSibling$nodeValu; const range = (_window$getSelection = window.getSelection()) === null || _window$getSelection === void 0 ? void 0 : _window$getSelection.getRangeAt(0); /** * At this point the function is aborted if there is no selection range, several characters have * been selected and therefore no single letter is removed or neither the "Backspace" nor the * "Delete" key has been pressed. */ if (!range || range.endOffset !== range.startOffset || event.key !== 'Backspace' && event.key !== 'Delete' && event.key !== 'Unidentified') { return null; } if (event.key === 'Backspace' || event.key === 'Unidentified') { var _previousSibling$node; const { nodeValue, previousSibling } = range.startContainer; if (range.startOffset > 0) { return nodeValue === null || nodeValue === void 0 ? void 0 : nodeValue.charCodeAt(range.startOffset - 1); } return previousSibling === null || previousSibling === void 0 || (_previousSibling$node = previousSibling.nodeValue) === null || _previousSibling$node === void 0 ? void 0 : _previousSibling$node.charCodeAt(previousSibling.nodeValue.length - 1); } const { nextSibling, nodeValue } = range.endContainer; if (range.endOffset < ((nodeValue === null || nodeValue === void 0 ? void 0 : nodeValue.length) ?? 0)) { return nodeValue === null || nodeValue === void 0 ? void 0 : nodeValue.charCodeAt(range.endOffset); } return nextSibling === null || nextSibling === void 0 || (_nextSibling$nodeValu = nextSibling.nodeValue) === null || _nextSibling$nodeValu === void 0 ? void 0 : _nextSibling$nodeValu.charCodeAt(0); }; exports.getCharCodeThatWillBeDeleted = getCharCodeThatWillBeDeleted; const findAndSelectText = ({ editorElement, searchText, options = { shouldReplaceNearCursor: true } }) => { var _editorElement$textCo; if (!((_editorElement$textCo = editorElement.textContent) !== null && _editorElement$textCo !== void 0 && _editorElement$textCo.includes(searchText))) { return null; } const range = document.createRange(); const cursorPos = getCurrentCursorPosition(editorElement); const matches = []; let absCounter = 0; const searchNodesForText = node => { if (node.nodeType === Node.TEXT_NODE && node.textContent) { let index = node.textContent.indexOf(searchText); while (index !== -1) { matches.push({ node, offset: index, absIndex: absCounter + index }); index = node.textContent.indexOf(searchText, index + 1); } absCounter += node.textContent.length; } else if (node.nodeName !== 'LC_MENTION') { Array.from(node.childNodes).forEach(searchNodesForText); } }; searchNodesForText(editorElement); if (matches.length === 0) { return null; } let match = matches[0]; if (options !== null && options !== void 0 && options.shouldReplaceNearCursor && cursorPos !== null) { match = matches.reduce((prev, curr) => Math.abs(curr.absIndex - cursorPos) < Math.abs(prev.absIndex - cursorPos) ? curr : prev); } if (!match) { return null; } range.setStart(match.node, match.offset); range.setEnd(match.node, match.offset + searchText.length); return range; }; exports.findAndSelectText = findAndSelectText; const getCurrentCursorPosition = editorElement => { var _window$getSelection2, _window; if (!editorElement) { return null; } const sel = (_window$getSelection2 = (_window = window).getSelection) === null || _window$getSelection2 === void 0 ? void 0 : _window$getSelection2.call(_window); if (!sel || sel.rangeCount === 0) { return null; } const range = sel.getRangeAt(0); if (!editorElement.contains(range.commonAncestorContainer)) { return null; } const pre = document.createRange(); pre.selectNodeContents(editorElement); try { pre.setEnd(range.startContainer, range.startOffset); const container = document.createElement('div'); container.appendChild(pre.cloneContents()); const bbCodeUntilCursor = (0, _text.convertHTMLToText)(container.innerHTML, { preserveSpaces: true, shouldSerializeNoEmojiToBBCode: false }); return bbCodeUntilCursor.length; } catch { return (0, _text.convertHTMLToText)(editorElement.innerHTML, { shouldSerializeNoEmojiToBBCode: false }).length; } }; exports.getCurrentCursorPosition = getCurrentCursorPosition; const getCursorTargetByAbsIndex = ({ editorElement, position }) => { const childNodes = Array.from(editorElement.childNodes); if (childNodes.length === 0) { const textNode = document.createTextNode(''); editorElement.appendChild(textNode); return { node: textNode, offset: 0 }; } const clampedPosition = (0, _number.clamp)(position, 0, (0, _text.convertHTMLToText)(editorElement.innerHTML, { shouldSerializeNoEmojiToBBCode: false }).length); let absCounter = 0; const searchNodes = node => { if (node.nodeType === Node.TEXT_NODE) { const textNode = node; const nodeText = textNode.nodeValue ?? ''; const nextAbsCounter = absCounter + nodeText.length; if (clampedPosition <= nextAbsCounter) { return { node: textNode, offset: (0, _number.clamp)(clampedPosition - absCounter, 0, nodeText.length) }; } absCounter = nextAbsCounter; return null; } if (node.nodeName === 'LC_MENTION') { return null; } for (const child of Array.from(node.childNodes)) { const found = searchNodes(child); if (found) { return found; } } return null; }; for (const rootNode of childNodes) { const found = searchNodes(rootNode); if (found) { return found; } } const lastTextNode = editorElement.lastChild; if ((lastTextNode === null || lastTextNode === void 0 ? void 0 : lastTextNode.nodeType) === Node.TEXT_NODE) { const textNode = lastTextNode; return { node: textNode, offset: textNode.length }; } const appendedTextNode = document.createTextNode('\u200B'); editorElement.appendChild(appendedTextNode); return { node: appendedTextNode, offset: appendedTextNode.length }; }; const setCursorPositionByAbsIndex = ({ editorElement, position }) => { var _window$getSelection3, _window2; const selection = (_window$getSelection3 = (_window2 = window).getSelection) === null || _window$getSelection3 === void 0 ? void 0 : _window$getSelection3.call(_window2); if (!selection) { return; } const target = getCursorTargetByAbsIndex({ editorElement, position }); if (!target) { return; } const range = document.createRange(); try { range.setStart(target.node, target.offset); range.setEnd(target.node, target.offset); } catch { return; } selection.removeAllRanges(); selection.addRange(range); range.collapse(true); }; /** * Unwraps any no-emoji-convert span that contains the cursor. * This removes the span element but keeps its text content, preventing the cursor from getting stuck. */ exports.setCursorPositionByAbsIndex = setCursorPositionByAbsIndex; const unwrapIgnoreEmojiSpanAtCursor = editorElement => { if (!editorElement) { return; } const selection = window.getSelection(); if (!selection || !selection.rangeCount) { return; } const range = selection.getRangeAt(0); let { commonAncestorContainer } = range; const cursorOffset = range.startOffset; if (!editorElement.contains(commonAncestorContainer)) { return; } // Walk up the DOM tree to find if we're inside a no-emoji-convert span while (commonAncestorContainer && commonAncestorContainer !== editorElement) { if (commonAncestorContainer.nodeType === Node.ELEMENT_NODE && commonAncestorContainer.classList.contains('no-emoji-convert')) { // We're inside a no-emoji-convert span, unwrap it const span = commonAncestorContainer; const { parentNode } = span; if (!parentNode) { return; } // Find which text node contains the cursor let cursorNode = null; const cursorNodeOffset = cursorOffset; // Get all child nodes of the span const childNodes = Array.from(span.childNodes); // Insert all children before the span and find the cursor node childNodes.forEach(child => { const clonedChild = child.cloneNode(true); parentNode.insertBefore(clonedChild, span); // If this is the node that contains the cursor, track it if (child === range.startContainer) { cursorNode = clonedChild; } }); // Remove the span parentNode.removeChild(span); // Restore cursor position in the unwrapped content if (cursorNode) { range.setStart(cursorNode, cursorNodeOffset); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } break; } // @ts-expect-error - is needed commonAncestorContainer = commonAncestorContainer.parentNode; } }; /** * Moves the cursor outside of any no-emoji-convert span if it's currently inside one. * This prevents the cursor from getting "stuck" inside the protection span when typing. */ exports.unwrapIgnoreEmojiSpanAtCursor = unwrapIgnoreEmojiSpanAtCursor; const moveCursorOutOfIgnoreEmojiSpan = editorElement => { if (!editorElement) { return; } const selection = window.getSelection(); if (!selection || !selection.rangeCount) { return; } const range = selection.getRangeAt(0); let { commonAncestorContainer } = range; if (!editorElement.contains(commonAncestorContainer)) { return; } // Walk up the DOM tree to find if we're inside a no-emoji-convert span while (commonAncestorContainer && commonAncestorContainer !== editorElement) { if (commonAncestorContainer.nodeType === Node.ELEMENT_NODE && commonAncestorContainer.classList.contains('no-emoji-convert')) { // We're inside a no-emoji-convert span, move cursor to after it const span = commonAncestorContainer; const { nextSibling } = span; const { parentNode } = span; if (!parentNode) { return; } // Always ensure there's a text node AFTER the span let targetNode; if (nextSibling && nextSibling.nodeType === Node.TEXT_NODE) { // Next sibling is already a text node, use it targetNode = nextSibling; } else if (nextSibling && nextSibling.nodeType === Node.ELEMENT_NODE) { // Next sibling is an element, find first text node in it const walker = document.createTreeWalker(nextSibling, NodeFilter.SHOW_TEXT); const firstTextNode = walker.nextNode(); if (firstTextNode) { targetNode = firstTextNode; } else { // No text node found, create one targetNode = document.createTextNode('\u200B'); parentNode.insertBefore(targetNode, nextSibling); } } else { // No next sibling or it's not a text/element node, create a new text node targetNode = document.createTextNode('\u200B'); parentNode.insertBefore(targetNode, nextSibling); } // Place cursor at the start of the target node range.setStart(targetNode, 0); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); break; } // @ts-expect-error - is needed commonAncestorContainer = commonAncestorContainer.parentNode; } }; exports.moveCursorOutOfIgnoreEmojiSpan = moveCursorOutOfIgnoreEmojiSpan; //# sourceMappingURL=selection.js.map