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 getActiveElement from './getActiveElement';
    
  9. 
    
  10. import {getOffsets, setOffsets} from './ReactDOMSelection';
    
  11. import {ELEMENT_NODE, TEXT_NODE} from './HTMLNodeType';
    
  12. 
    
  13. function isTextNode(node) {
    
  14.   return node && node.nodeType === TEXT_NODE;
    
  15. }
    
  16. 
    
  17. function containsNode(outerNode, innerNode) {
    
  18.   if (!outerNode || !innerNode) {
    
  19.     return false;
    
  20.   } else if (outerNode === innerNode) {
    
  21.     return true;
    
  22.   } else if (isTextNode(outerNode)) {
    
  23.     return false;
    
  24.   } else if (isTextNode(innerNode)) {
    
  25.     return containsNode(outerNode, innerNode.parentNode);
    
  26.   } else if ('contains' in outerNode) {
    
  27.     return outerNode.contains(innerNode);
    
  28.   } else if (outerNode.compareDocumentPosition) {
    
  29.     return !!(outerNode.compareDocumentPosition(innerNode) & 16);
    
  30.   } else {
    
  31.     return false;
    
  32.   }
    
  33. }
    
  34. 
    
  35. function isInDocument(node) {
    
  36.   return (
    
  37.     node &&
    
  38.     node.ownerDocument &&
    
  39.     containsNode(node.ownerDocument.documentElement, node)
    
  40.   );
    
  41. }
    
  42. 
    
  43. function isSameOriginFrame(iframe) {
    
  44.   try {
    
  45.     // Accessing the contentDocument of a HTMLIframeElement can cause the browser
    
  46.     // to throw, e.g. if it has a cross-origin src attribute.
    
  47.     // Safari will show an error in the console when the access results in "Blocked a frame with origin". e.g:
    
  48.     // iframe.contentDocument.defaultView;
    
  49.     // A safety way is to access one of the cross origin properties: Window or Location
    
  50.     // Which might result in "SecurityError" DOM Exception and it is compatible to Safari.
    
  51.     // https://html.spec.whatwg.org/multipage/browsers.html#integration-with-idl
    
  52. 
    
  53.     return typeof iframe.contentWindow.location.href === 'string';
    
  54.   } catch (err) {
    
  55.     return false;
    
  56.   }
    
  57. }
    
  58. 
    
  59. function getActiveElementDeep() {
    
  60.   let win = window;
    
  61.   let element = getActiveElement();
    
  62.   while (element instanceof win.HTMLIFrameElement) {
    
  63.     if (isSameOriginFrame(element)) {
    
  64.       win = element.contentWindow;
    
  65.     } else {
    
  66.       return element;
    
  67.     }
    
  68.     element = getActiveElement(win.document);
    
  69.   }
    
  70.   return element;
    
  71. }
    
  72. 
    
  73. /**
    
  74.  * @ReactInputSelection: React input selection module. Based on Selection.js,
    
  75.  * but modified to be suitable for react and has a couple of bug fixes (doesn't
    
  76.  * assume buttons have range selections allowed).
    
  77.  * Input selection module for React.
    
  78.  */
    
  79. 
    
  80. /**
    
  81.  * @hasSelectionCapabilities: we get the element types that support selection
    
  82.  * from https://html.spec.whatwg.org/#do-not-apply, looking at `selectionStart`
    
  83.  * and `selectionEnd` rows.
    
  84.  */
    
  85. export function hasSelectionCapabilities(elem) {
    
  86.   const nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
    
  87.   return (
    
  88.     nodeName &&
    
  89.     ((nodeName === 'input' &&
    
  90.       (elem.type === 'text' ||
    
  91.         elem.type === 'search' ||
    
  92.         elem.type === 'tel' ||
    
  93.         elem.type === 'url' ||
    
  94.         elem.type === 'password')) ||
    
  95.       nodeName === 'textarea' ||
    
  96.       elem.contentEditable === 'true')
    
  97.   );
    
  98. }
    
  99. 
    
  100. export function getSelectionInformation() {
    
  101.   const focusedElem = getActiveElementDeep();
    
  102.   return {
    
  103.     focusedElem: focusedElem,
    
  104.     selectionRange: hasSelectionCapabilities(focusedElem)
    
  105.       ? getSelection(focusedElem)
    
  106.       : null,
    
  107.   };
    
  108. }
    
  109. 
    
  110. /**
    
  111.  * @restoreSelection: If any selection information was potentially lost,
    
  112.  * restore it. This is useful when performing operations that could remove dom
    
  113.  * nodes and place them back in, resulting in focus being lost.
    
  114.  */
    
  115. export function restoreSelection(priorSelectionInformation) {
    
  116.   const curFocusedElem = getActiveElementDeep();
    
  117.   const priorFocusedElem = priorSelectionInformation.focusedElem;
    
  118.   const priorSelectionRange = priorSelectionInformation.selectionRange;
    
  119.   if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) {
    
  120.     if (
    
  121.       priorSelectionRange !== null &&
    
  122.       hasSelectionCapabilities(priorFocusedElem)
    
  123.     ) {
    
  124.       setSelection(priorFocusedElem, priorSelectionRange);
    
  125.     }
    
  126. 
    
  127.     // Focusing a node can change the scroll position, which is undesirable
    
  128.     const ancestors = [];
    
  129.     let ancestor = priorFocusedElem;
    
  130.     while ((ancestor = ancestor.parentNode)) {
    
  131.       if (ancestor.nodeType === ELEMENT_NODE) {
    
  132.         ancestors.push({
    
  133.           element: ancestor,
    
  134.           left: ancestor.scrollLeft,
    
  135.           top: ancestor.scrollTop,
    
  136.         });
    
  137.       }
    
  138.     }
    
  139. 
    
  140.     if (typeof priorFocusedElem.focus === 'function') {
    
  141.       priorFocusedElem.focus();
    
  142.     }
    
  143. 
    
  144.     for (let i = 0; i < ancestors.length; i++) {
    
  145.       const info = ancestors[i];
    
  146.       info.element.scrollLeft = info.left;
    
  147.       info.element.scrollTop = info.top;
    
  148.     }
    
  149.   }
    
  150. }
    
  151. 
    
  152. /**
    
  153.  * @getSelection: Gets the selection bounds of a focused textarea, input or
    
  154.  * contentEditable node.
    
  155.  * -@input: Look up selection bounds of this input
    
  156.  * -@return {start: selectionStart, end: selectionEnd}
    
  157.  */
    
  158. export function getSelection(input) {
    
  159.   let selection;
    
  160. 
    
  161.   if ('selectionStart' in input) {
    
  162.     // Modern browser with input or textarea.
    
  163.     selection = {
    
  164.       start: input.selectionStart,
    
  165.       end: input.selectionEnd,
    
  166.     };
    
  167.   } else {
    
  168.     // Content editable or old IE textarea.
    
  169.     selection = getOffsets(input);
    
  170.   }
    
  171. 
    
  172.   return selection || {start: 0, end: 0};
    
  173. }
    
  174. 
    
  175. /**
    
  176.  * @setSelection: Sets the selection bounds of a textarea or input and focuses
    
  177.  * the input.
    
  178.  * -@input     Set selection bounds of this input or textarea
    
  179.  * -@offsets   Object of same form that is returned from get*
    
  180.  */
    
  181. export function setSelection(input, offsets) {
    
  182.   const start = offsets.start;
    
  183.   let end = offsets.end;
    
  184.   if (end === undefined) {
    
  185.     end = start;
    
  186.   }
    
  187. 
    
  188.   if ('selectionStart' in input) {
    
  189.     input.selectionStart = start;
    
  190.     input.selectionEnd = Math.min(end, input.value.length);
    
  191.   } else {
    
  192.     setOffsets(input, offsets);
    
  193.   }
    
  194. }