/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import getNodeForCharacterOffset from './getNodeForCharacterOffset'; import { TEXT_NODE } from './HTMLNodeType'; /** * @param {DOMElement} outerNode * @return {?object} */ export function getOffsets(outerNode) { const { ownerDocument } = outerNode; const win = ownerDocument && ownerDocument.defaultView || window; const selection = win.getSelection && win.getSelection(); if (!selection || selection.rangeCount === 0) { return null; } const { anchorNode, anchorOffset, focusNode, focusOffset } = selection; // In Firefox, anchorNode and focusNode can be "anonymous divs", e.g. the // up/down buttons on an . Anonymous divs do not seem to // expose properties, triggering a "Permission denied error" if any of its // properties are accessed. The only seemingly possible way to avoid erroring // is to access a property that typically works for non-anonymous divs and // catch any error that may otherwise arise. See // https://bugzilla.mozilla.org/show_bug.cgi?id=208427 try { /* eslint-disable ft-flow/no-unused-expressions */ anchorNode.nodeType; focusNode.nodeType; /* eslint-enable ft-flow/no-unused-expressions */ } catch (e) { return null; } return getModernOffsetsFromPoints(outerNode, anchorNode, anchorOffset, focusNode, focusOffset); } /** * Returns {start, end} where `start` is the character/codepoint index of * (anchorNode, anchorOffset) within the textContent of `outerNode`, and * `end` is the index of (focusNode, focusOffset). * * Returns null if you pass in garbage input but we should probably just crash. * * Exported only for testing. */ export function getModernOffsetsFromPoints(outerNode, anchorNode, anchorOffset, focusNode, focusOffset) { let length = 0; let start = -1; let end = -1; let indexWithinAnchor = 0; let indexWithinFocus = 0; let node = outerNode; let parentNode = null; outer: while (true) { let next = null; while (true) { if (node === anchorNode && (anchorOffset === 0 || node.nodeType === TEXT_NODE)) { start = length + anchorOffset; } if (node === focusNode && (focusOffset === 0 || node.nodeType === TEXT_NODE)) { end = length + focusOffset; } if (node.nodeType === TEXT_NODE) { length += node.nodeValue.length; } if ((next = node.firstChild) === null) { break; } // Moving from `node` to its first child `next`. parentNode = node; node = next; } while (true) { if (node === outerNode) { // If `outerNode` has children, this is always the second time visiting // it. If it has no children, this is still the first loop, and the only // valid selection is anchorNode and focusNode both equal to this node // and both offsets 0, in which case we will have handled above. break outer; } if (parentNode === anchorNode && ++indexWithinAnchor === anchorOffset) { start = length; } if (parentNode === focusNode && ++indexWithinFocus === focusOffset) { end = length; } if ((next = node.nextSibling) !== null) { break; } node = parentNode; parentNode = node.parentNode; } // Moving from `node` to its next sibling `next`. node = next; } if (start === -1 || end === -1) { // This should never happen. (Would happen if the anchor/focus nodes aren't // actually inside the passed-in node.) return null; } return { start: start, end: end }; } /** * In modern non-IE browsers, we can support both forward and backward * selections. * * Note: IE10+ supports the Selection object, but it does not support * the `extend` method, which means that even in modern IE, it's not possible * to programmatically create a backward selection. Thus, for all IE * versions, we use the old IE API to create our selections. * * @param {DOMElement|DOMTextNode} node * @param {object} offsets */ export function setOffsets(node, offsets) { const doc = node.ownerDocument || document; const win = doc && doc.defaultView || window; // Edge fails with "Object expected" in some scenarios. // (For instance: TinyMCE editor used in a list component that supports pasting to add more, // fails when pasting 100+ items) if (!win.getSelection) { return; } const selection = win.getSelection(); const length = node.textContent.length; let start = offsets.start |> Math.min(%, length); let end = offsets.end === undefined ? start : offsets.end |> Math.min(%, length); // IE 11 uses modern selection, but doesn't support the extend method. // Flip backward selections, so we can set with a single range. if (!selection.extend && start > end) { const temp = end; end = start; start = temp; } const startMarker = node |> getNodeForCharacterOffset(%, start); const endMarker = node |> getNodeForCharacterOffset(%, end); if (startMarker && endMarker) { if (selection.rangeCount === 1 && selection.anchorNode === startMarker.node && selection.anchorOffset === startMarker.offset && selection.focusNode === endMarker.node && selection.focusOffset === endMarker.offset) { return; } const range = doc.createRange(); startMarker.node |> range.setStart(%, startMarker.offset); selection.removeAllRanges(); if (start > end) { range |> selection.addRange(%); endMarker.node |> selection.extend(%, endMarker.offset); } else { endMarker.node |> range.setEnd(%, endMarker.offset); range |> selection.addRange(%); } } }