1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  */
    
  7. 
    
  8. import getNodeForCharacterOffset from './getNodeForCharacterOffset';
    
  9. import {TEXT_NODE} from './HTMLNodeType';
    
  10. 
    
  11. /**
    
  12.  * @param {DOMElement} outerNode
    
  13.  * @return {?object}
    
  14.  */
    
  15. export function getOffsets(outerNode) {
    
  16.   const {ownerDocument} = outerNode;
    
  17.   const win = (ownerDocument && ownerDocument.defaultView) || window;
    
  18.   const selection = win.getSelection && win.getSelection();
    
  19. 
    
  20.   if (!selection || selection.rangeCount === 0) {
    
  21.     return null;
    
  22.   }
    
  23. 
    
  24.   const {anchorNode, anchorOffset, focusNode, focusOffset} = selection;
    
  25. 
    
  26.   // In Firefox, anchorNode and focusNode can be "anonymous divs", e.g. the
    
  27.   // up/down buttons on an <input type="number">. Anonymous divs do not seem to
    
  28.   // expose properties, triggering a "Permission denied error" if any of its
    
  29.   // properties are accessed. The only seemingly possible way to avoid erroring
    
  30.   // is to access a property that typically works for non-anonymous divs and
    
  31.   // catch any error that may otherwise arise. See
    
  32.   // https://bugzilla.mozilla.org/show_bug.cgi?id=208427
    
  33.   try {
    
  34.     /* eslint-disable ft-flow/no-unused-expressions */
    
  35.     anchorNode.nodeType;
    
  36.     focusNode.nodeType;
    
  37.     /* eslint-enable ft-flow/no-unused-expressions */
    
  38.   } catch (e) {
    
  39.     return null;
    
  40.   }
    
  41. 
    
  42.   return getModernOffsetsFromPoints(
    
  43.     outerNode,
    
  44.     anchorNode,
    
  45.     anchorOffset,
    
  46.     focusNode,
    
  47.     focusOffset,
    
  48.   );
    
  49. }
    
  50. 
    
  51. /**
    
  52.  * Returns {start, end} where `start` is the character/codepoint index of
    
  53.  * (anchorNode, anchorOffset) within the textContent of `outerNode`, and
    
  54.  * `end` is the index of (focusNode, focusOffset).
    
  55.  *
    
  56.  * Returns null if you pass in garbage input but we should probably just crash.
    
  57.  *
    
  58.  * Exported only for testing.
    
  59.  */
    
  60. export function getModernOffsetsFromPoints(
    
  61.   outerNode,
    
  62.   anchorNode,
    
  63.   anchorOffset,
    
  64.   focusNode,
    
  65.   focusOffset,
    
  66. ) {
    
  67.   let length = 0;
    
  68.   let start = -1;
    
  69.   let end = -1;
    
  70.   let indexWithinAnchor = 0;
    
  71.   let indexWithinFocus = 0;
    
  72.   let node = outerNode;
    
  73.   let parentNode = null;
    
  74. 
    
  75.   outer: while (true) {
    
  76.     let next = null;
    
  77. 
    
  78.     while (true) {
    
  79.       if (
    
  80.         node === anchorNode &&
    
  81.         (anchorOffset === 0 || node.nodeType === TEXT_NODE)
    
  82.       ) {
    
  83.         start = length + anchorOffset;
    
  84.       }
    
  85.       if (
    
  86.         node === focusNode &&
    
  87.         (focusOffset === 0 || node.nodeType === TEXT_NODE)
    
  88.       ) {
    
  89.         end = length + focusOffset;
    
  90.       }
    
  91. 
    
  92.       if (node.nodeType === TEXT_NODE) {
    
  93.         length += node.nodeValue.length;
    
  94.       }
    
  95. 
    
  96.       if ((next = node.firstChild) === null) {
    
  97.         break;
    
  98.       }
    
  99.       // Moving from `node` to its first child `next`.
    
  100.       parentNode = node;
    
  101.       node = next;
    
  102.     }
    
  103. 
    
  104.     while (true) {
    
  105.       if (node === outerNode) {
    
  106.         // If `outerNode` has children, this is always the second time visiting
    
  107.         // it. If it has no children, this is still the first loop, and the only
    
  108.         // valid selection is anchorNode and focusNode both equal to this node
    
  109.         // and both offsets 0, in which case we will have handled above.
    
  110.         break outer;
    
  111.       }
    
  112.       if (parentNode === anchorNode && ++indexWithinAnchor === anchorOffset) {
    
  113.         start = length;
    
  114.       }
    
  115.       if (parentNode === focusNode && ++indexWithinFocus === focusOffset) {
    
  116.         end = length;
    
  117.       }
    
  118.       if ((next = node.nextSibling) !== null) {
    
  119.         break;
    
  120.       }
    
  121.       node = parentNode;
    
  122.       parentNode = node.parentNode;
    
  123.     }
    
  124. 
    
  125.     // Moving from `node` to its next sibling `next`.
    
  126.     node = next;
    
  127.   }
    
  128. 
    
  129.   if (start === -1 || end === -1) {
    
  130.     // This should never happen. (Would happen if the anchor/focus nodes aren't
    
  131.     // actually inside the passed-in node.)
    
  132.     return null;
    
  133.   }
    
  134. 
    
  135.   return {
    
  136.     start: start,
    
  137.     end: end,
    
  138.   };
    
  139. }
    
  140. 
    
  141. /**
    
  142.  * In modern non-IE browsers, we can support both forward and backward
    
  143.  * selections.
    
  144.  *
    
  145.  * Note: IE10+ supports the Selection object, but it does not support
    
  146.  * the `extend` method, which means that even in modern IE, it's not possible
    
  147.  * to programmatically create a backward selection. Thus, for all IE
    
  148.  * versions, we use the old IE API to create our selections.
    
  149.  *
    
  150.  * @param {DOMElement|DOMTextNode} node
    
  151.  * @param {object} offsets
    
  152.  */
    
  153. export function setOffsets(node, offsets) {
    
  154.   const doc = node.ownerDocument || document;
    
  155.   const win = (doc && doc.defaultView) || window;
    
  156. 
    
  157.   // Edge fails with "Object expected" in some scenarios.
    
  158.   // (For instance: TinyMCE editor used in a list component that supports pasting to add more,
    
  159.   // fails when pasting 100+ items)
    
  160.   if (!win.getSelection) {
    
  161.     return;
    
  162.   }
    
  163. 
    
  164.   const selection = win.getSelection();
    
  165.   const length = node.textContent.length;
    
  166.   let start = Math.min(offsets.start, length);
    
  167.   let end = offsets.end === undefined ? start : Math.min(offsets.end, length);
    
  168. 
    
  169.   // IE 11 uses modern selection, but doesn't support the extend method.
    
  170.   // Flip backward selections, so we can set with a single range.
    
  171.   if (!selection.extend && start > end) {
    
  172.     const temp = end;
    
  173.     end = start;
    
  174.     start = temp;
    
  175.   }
    
  176. 
    
  177.   const startMarker = getNodeForCharacterOffset(node, start);
    
  178.   const endMarker = getNodeForCharacterOffset(node, end);
    
  179. 
    
  180.   if (startMarker && endMarker) {
    
  181.     if (
    
  182.       selection.rangeCount === 1 &&
    
  183.       selection.anchorNode === startMarker.node &&
    
  184.       selection.anchorOffset === startMarker.offset &&
    
  185.       selection.focusNode === endMarker.node &&
    
  186.       selection.focusOffset === endMarker.offset
    
  187.     ) {
    
  188.       return;
    
  189.     }
    
  190.     const range = doc.createRange();
    
  191.     range.setStart(startMarker.node, startMarker.offset);
    
  192.     selection.removeAllRanges();
    
  193. 
    
  194.     if (start > end) {
    
  195.       selection.addRange(range);
    
  196.       selection.extend(endMarker.node, endMarker.offset);
    
  197.     } else {
    
  198.       range.setEnd(endMarker.node, endMarker.offset);
    
  199.       selection.addRange(range);
    
  200.     }
    
  201.   }
    
  202. }