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.  * @flow
    
  8.  */
    
  9. 
    
  10. import type {AnyNativeEvent} from '../PluginModuleType';
    
  11. import type {DOMEventName} from '../DOMEventNames';
    
  12. import type {DispatchQueue} from '../DOMPluginEventSystem';
    
  13. import type {EventSystemFlags} from '../EventSystemFlags';
    
  14. import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
    
  15. import type {ReactSyntheticEvent} from '../ReactSyntheticEventType';
    
  16. 
    
  17. import {canUseDOM} from 'shared/ExecutionEnvironment';
    
  18. import {SyntheticEvent} from '../../events/SyntheticEvent';
    
  19. import isTextInputElement from '../isTextInputElement';
    
  20. import shallowEqual from 'shared/shallowEqual';
    
  21. 
    
  22. import {registerTwoPhaseEvent} from '../EventRegistry';
    
  23. import getActiveElement from '../../client/getActiveElement';
    
  24. import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
    
  25. import {hasSelectionCapabilities} from '../../client/ReactInputSelection';
    
  26. import {DOCUMENT_NODE} from '../../client/HTMLNodeType';
    
  27. import {accumulateTwoPhaseListeners} from '../DOMPluginEventSystem';
    
  28. 
    
  29. const skipSelectionChangeEvent =
    
  30.   canUseDOM && 'documentMode' in document && document.documentMode <= 11;
    
  31. 
    
  32. function registerEvents() {
    
  33.   registerTwoPhaseEvent('onSelect', [
    
  34.     'focusout',
    
  35.     'contextmenu',
    
  36.     'dragend',
    
  37.     'focusin',
    
  38.     'keydown',
    
  39.     'keyup',
    
  40.     'mousedown',
    
  41.     'mouseup',
    
  42.     'selectionchange',
    
  43.   ]);
    
  44. }
    
  45. 
    
  46. let activeElement = null;
    
  47. let activeElementInst = null;
    
  48. let lastSelection = null;
    
  49. let mouseDown = false;
    
  50. 
    
  51. /**
    
  52.  * Get an object which is a unique representation of the current selection.
    
  53.  *
    
  54.  * The return value will not be consistent across nodes or browsers, but
    
  55.  * two identical selections on the same node will return identical objects.
    
  56.  */
    
  57. function getSelection(node: any) {
    
  58.   if ('selectionStart' in node && hasSelectionCapabilities(node)) {
    
  59.     return {
    
  60.       start: node.selectionStart,
    
  61.       end: node.selectionEnd,
    
  62.     };
    
  63.   } else {
    
  64.     const win =
    
  65.       (node.ownerDocument && node.ownerDocument.defaultView) || window;
    
  66.     const selection = win.getSelection();
    
  67.     return {
    
  68.       anchorNode: selection.anchorNode,
    
  69.       anchorOffset: selection.anchorOffset,
    
  70.       focusNode: selection.focusNode,
    
  71.       focusOffset: selection.focusOffset,
    
  72.     };
    
  73.   }
    
  74. }
    
  75. 
    
  76. /**
    
  77.  * Get document associated with the event target.
    
  78.  */
    
  79. function getEventTargetDocument(eventTarget: any) {
    
  80.   return eventTarget.window === eventTarget
    
  81.     ? eventTarget.document
    
  82.     : eventTarget.nodeType === DOCUMENT_NODE
    
  83.     ? eventTarget
    
  84.     : eventTarget.ownerDocument;
    
  85. }
    
  86. 
    
  87. /**
    
  88.  * Poll selection to see whether it's changed.
    
  89.  *
    
  90.  * @param {object} nativeEvent
    
  91.  * @param {object} nativeEventTarget
    
  92.  * @return {?SyntheticEvent}
    
  93.  */
    
  94. function constructSelectEvent(
    
  95.   dispatchQueue: DispatchQueue,
    
  96.   nativeEvent: AnyNativeEvent,
    
  97.   nativeEventTarget: null | EventTarget,
    
  98. ) {
    
  99.   // Ensure we have the right element, and that the user is not dragging a
    
  100.   // selection (this matches native `select` event behavior). In HTML5, select
    
  101.   // fires only on input and textarea thus if there's no focused element we
    
  102.   // won't dispatch.
    
  103.   const doc = getEventTargetDocument(nativeEventTarget);
    
  104. 
    
  105.   if (
    
  106.     mouseDown ||
    
  107.     activeElement == null ||
    
  108.     activeElement !== getActiveElement(doc)
    
  109.   ) {
    
  110.     return;
    
  111.   }
    
  112. 
    
  113.   // Only fire when selection has actually changed.
    
  114.   const currentSelection = getSelection(activeElement);
    
  115.   if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
    
  116.     lastSelection = currentSelection;
    
  117. 
    
  118.     const listeners = accumulateTwoPhaseListeners(
    
  119.       activeElementInst,
    
  120.       'onSelect',
    
  121.     );
    
  122.     if (listeners.length > 0) {
    
  123.       const event: ReactSyntheticEvent = new SyntheticEvent(
    
  124.         'onSelect',
    
  125.         'select',
    
  126.         null,
    
  127.         nativeEvent,
    
  128.         nativeEventTarget,
    
  129.       );
    
  130.       dispatchQueue.push({event, listeners});
    
  131.       event.target = activeElement;
    
  132.     }
    
  133.   }
    
  134. }
    
  135. 
    
  136. /**
    
  137.  * This plugin creates an `onSelect` event that normalizes select events
    
  138.  * across form elements.
    
  139.  *
    
  140.  * Supported elements are:
    
  141.  * - input (see `isTextInputElement`)
    
  142.  * - textarea
    
  143.  * - contentEditable
    
  144.  *
    
  145.  * This differs from native browser implementations in the following ways:
    
  146.  * - Fires on contentEditable fields as well as inputs.
    
  147.  * - Fires for collapsed selection.
    
  148.  * - Fires after user input.
    
  149.  */
    
  150. function extractEvents(
    
  151.   dispatchQueue: DispatchQueue,
    
  152.   domEventName: DOMEventName,
    
  153.   targetInst: null | Fiber,
    
  154.   nativeEvent: AnyNativeEvent,
    
  155.   nativeEventTarget: null | EventTarget,
    
  156.   eventSystemFlags: EventSystemFlags,
    
  157.   targetContainer: EventTarget,
    
  158. ) {
    
  159.   const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
    
  160. 
    
  161.   switch (domEventName) {
    
  162.     // Track the input node that has focus.
    
  163.     case 'focusin':
    
  164.       if (
    
  165.         isTextInputElement((targetNode: any)) ||
    
  166.         targetNode.contentEditable === 'true'
    
  167.       ) {
    
  168.         activeElement = targetNode;
    
  169.         activeElementInst = targetInst;
    
  170.         lastSelection = null;
    
  171.       }
    
  172.       break;
    
  173.     case 'focusout':
    
  174.       activeElement = null;
    
  175.       activeElementInst = null;
    
  176.       lastSelection = null;
    
  177.       break;
    
  178.     // Don't fire the event while the user is dragging. This matches the
    
  179.     // semantics of the native select event.
    
  180.     case 'mousedown':
    
  181.       mouseDown = true;
    
  182.       break;
    
  183.     case 'contextmenu':
    
  184.     case 'mouseup':
    
  185.     case 'dragend':
    
  186.       mouseDown = false;
    
  187.       constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget);
    
  188.       break;
    
  189.     // Chrome and IE fire non-standard event when selection is changed (and
    
  190.     // sometimes when it hasn't). IE's event fires out of order with respect
    
  191.     // to key and input events on deletion, so we discard it.
    
  192.     //
    
  193.     // Firefox doesn't support selectionchange, so check selection status
    
  194.     // after each key entry. The selection changes after keydown and before
    
  195.     // keyup, but we check on keydown as well in the case of holding down a
    
  196.     // key, when multiple keydown events are fired but only one keyup is.
    
  197.     // This is also our approach for IE handling, for the reason above.
    
  198.     case 'selectionchange':
    
  199.       if (skipSelectionChangeEvent) {
    
  200.         break;
    
  201.       }
    
  202.     // falls through
    
  203.     case 'keydown':
    
  204.     case 'keyup':
    
  205.       constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget);
    
  206.   }
    
  207. }
    
  208. 
    
  209. export {registerEvents, extractEvents};