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. import type {TextInstance, Instance} from '../../client/ReactFiberConfigDOM';
    
  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 {registerTwoPhaseEvent} from '../EventRegistry';
    
  18. import {SyntheticEvent} from '../SyntheticEvent';
    
  19. import isTextInputElement from '../isTextInputElement';
    
  20. import {canUseDOM} from 'shared/ExecutionEnvironment';
    
  21. 
    
  22. import getEventTarget from '../getEventTarget';
    
  23. import isEventSupported from '../isEventSupported';
    
  24. import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
    
  25. import {updateValueIfChanged} from '../../client/inputValueTracking';
    
  26. import {setDefaultValue} from '../../client/ReactDOMInput';
    
  27. import {enqueueStateRestore} from '../ReactDOMControlledComponent';
    
  28. 
    
  29. import {
    
  30.   disableInputAttributeSyncing,
    
  31.   enableCustomElementPropertySupport,
    
  32. } from 'shared/ReactFeatureFlags';
    
  33. import {batchedUpdates} from '../ReactDOMUpdateBatching';
    
  34. import {
    
  35.   processDispatchQueue,
    
  36.   accumulateTwoPhaseListeners,
    
  37. } from '../DOMPluginEventSystem';
    
  38. import isCustomElement from '../../shared/isCustomElement';
    
  39. 
    
  40. function registerEvents() {
    
  41.   registerTwoPhaseEvent('onChange', [
    
  42.     'change',
    
  43.     'click',
    
  44.     'focusin',
    
  45.     'focusout',
    
  46.     'input',
    
  47.     'keydown',
    
  48.     'keyup',
    
  49.     'selectionchange',
    
  50.   ]);
    
  51. }
    
  52. 
    
  53. function createAndAccumulateChangeEvent(
    
  54.   dispatchQueue: DispatchQueue,
    
  55.   inst: null | Fiber,
    
  56.   nativeEvent: AnyNativeEvent,
    
  57.   target: null | EventTarget,
    
  58. ) {
    
  59.   // Flag this event loop as needing state restore.
    
  60.   enqueueStateRestore(((target: any): Node));
    
  61.   const listeners = accumulateTwoPhaseListeners(inst, 'onChange');
    
  62.   if (listeners.length > 0) {
    
  63.     const event: ReactSyntheticEvent = new SyntheticEvent(
    
  64.       'onChange',
    
  65.       'change',
    
  66.       null,
    
  67.       nativeEvent,
    
  68.       target,
    
  69.     );
    
  70.     dispatchQueue.push({event, listeners});
    
  71.   }
    
  72. }
    
  73. /**
    
  74.  * For IE shims
    
  75.  */
    
  76. let activeElement = null;
    
  77. let activeElementInst = null;
    
  78. 
    
  79. /**
    
  80.  * SECTION: handle `change` event
    
  81.  */
    
  82. function shouldUseChangeEvent(elem: Instance | TextInstance) {
    
  83.   const nodeName = elem.nodeName && elem.nodeName.toLowerCase();
    
  84.   return (
    
  85.     nodeName === 'select' ||
    
  86.     (nodeName === 'input' && (elem: any).type === 'file')
    
  87.   );
    
  88. }
    
  89. 
    
  90. function manualDispatchChangeEvent(nativeEvent: AnyNativeEvent) {
    
  91.   const dispatchQueue: DispatchQueue = [];
    
  92.   createAndAccumulateChangeEvent(
    
  93.     dispatchQueue,
    
  94.     activeElementInst,
    
  95.     nativeEvent,
    
  96.     getEventTarget(nativeEvent),
    
  97.   );
    
  98. 
    
  99.   // If change and propertychange bubbled, we'd just bind to it like all the
    
  100.   // other events and have it go through ReactBrowserEventEmitter. Since it
    
  101.   // doesn't, we manually listen for the events and so we have to enqueue and
    
  102.   // process the abstract event manually.
    
  103.   //
    
  104.   // Batching is necessary here in order to ensure that all event handlers run
    
  105.   // before the next rerender (including event handlers attached to ancestor
    
  106.   // elements instead of directly on the input). Without this, controlled
    
  107.   // components don't work properly in conjunction with event bubbling because
    
  108.   // the component is rerendered and the value reverted before all the event
    
  109.   // handlers can run. See https://github.com/facebook/react/issues/708.
    
  110.   batchedUpdates(runEventInBatch, dispatchQueue);
    
  111. }
    
  112. 
    
  113. function runEventInBatch(dispatchQueue: DispatchQueue) {
    
  114.   processDispatchQueue(dispatchQueue, 0);
    
  115. }
    
  116. 
    
  117. function getInstIfValueChanged(targetInst: Object) {
    
  118.   const targetNode = getNodeFromInstance(targetInst);
    
  119.   if (updateValueIfChanged(((targetNode: any): HTMLInputElement))) {
    
  120.     return targetInst;
    
  121.   }
    
  122. }
    
  123. 
    
  124. function getTargetInstForChangeEvent(
    
  125.   domEventName: DOMEventName,
    
  126.   targetInst: null | Fiber,
    
  127. ) {
    
  128.   if (domEventName === 'change') {
    
  129.     return targetInst;
    
  130.   }
    
  131. }
    
  132. 
    
  133. /**
    
  134.  * SECTION: handle `input` event
    
  135.  */
    
  136. let isInputEventSupported = false;
    
  137. if (canUseDOM) {
    
  138.   // IE9 claims to support the input event but fails to trigger it when
    
  139.   // deleting text, so we ignore its input events.
    
  140.   isInputEventSupported =
    
  141.     isEventSupported('input') &&
    
  142.     (!document.documentMode || document.documentMode > 9);
    
  143. }
    
  144. 
    
  145. /**
    
  146.  * (For IE <=9) Starts tracking propertychange events on the passed-in element
    
  147.  * and override the value property so that we can distinguish user events from
    
  148.  * value changes in JS.
    
  149.  */
    
  150. function startWatchingForValueChange(
    
  151.   target: Instance | TextInstance,
    
  152.   targetInst: null | Fiber,
    
  153. ) {
    
  154.   activeElement = target;
    
  155.   activeElementInst = targetInst;
    
  156.   (activeElement: any).attachEvent('onpropertychange', handlePropertyChange);
    
  157. }
    
  158. 
    
  159. /**
    
  160.  * (For IE <=9) Removes the event listeners from the currently-tracked element,
    
  161.  * if any exists.
    
  162.  */
    
  163. function stopWatchingForValueChange() {
    
  164.   if (!activeElement) {
    
  165.     return;
    
  166.   }
    
  167.   (activeElement: any).detachEvent('onpropertychange', handlePropertyChange);
    
  168.   activeElement = null;
    
  169.   activeElementInst = null;
    
  170. }
    
  171. 
    
  172. /**
    
  173.  * (For IE <=9) Handles a propertychange event, sending a `change` event if
    
  174.  * the value of the active element has changed.
    
  175.  */
    
  176. // $FlowFixMe[missing-local-annot]
    
  177. function handlePropertyChange(nativeEvent) {
    
  178.   if (nativeEvent.propertyName !== 'value') {
    
  179.     return;
    
  180.   }
    
  181.   if (getInstIfValueChanged(activeElementInst)) {
    
  182.     manualDispatchChangeEvent(nativeEvent);
    
  183.   }
    
  184. }
    
  185. 
    
  186. function handleEventsForInputEventPolyfill(
    
  187.   domEventName: DOMEventName,
    
  188.   target: Instance | TextInstance,
    
  189.   targetInst: null | Fiber,
    
  190. ) {
    
  191.   if (domEventName === 'focusin') {
    
  192.     // In IE9, propertychange fires for most input events but is buggy and
    
  193.     // doesn't fire when text is deleted, but conveniently, selectionchange
    
  194.     // appears to fire in all of the remaining cases so we catch those and
    
  195.     // forward the event if the value has changed
    
  196.     // In either case, we don't want to call the event handler if the value
    
  197.     // is changed from JS so we redefine a setter for `.value` that updates
    
  198.     // our activeElementValue variable, allowing us to ignore those changes
    
  199.     //
    
  200.     // stopWatching() should be a noop here but we call it just in case we
    
  201.     // missed a blur event somehow.
    
  202.     stopWatchingForValueChange();
    
  203.     startWatchingForValueChange(target, targetInst);
    
  204.   } else if (domEventName === 'focusout') {
    
  205.     stopWatchingForValueChange();
    
  206.   }
    
  207. }
    
  208. 
    
  209. // For IE8 and IE9.
    
  210. function getTargetInstForInputEventPolyfill(
    
  211.   domEventName: DOMEventName,
    
  212.   targetInst: null | Fiber,
    
  213. ) {
    
  214.   if (
    
  215.     domEventName === 'selectionchange' ||
    
  216.     domEventName === 'keyup' ||
    
  217.     domEventName === 'keydown'
    
  218.   ) {
    
  219.     // On the selectionchange event, the target is just document which isn't
    
  220.     // helpful for us so just check activeElement instead.
    
  221.     //
    
  222.     // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
    
  223.     // propertychange on the first input event after setting `value` from a
    
  224.     // script and fires only keydown, keypress, keyup. Catching keyup usually
    
  225.     // gets it and catching keydown lets us fire an event for the first
    
  226.     // keystroke if user does a key repeat (it'll be a little delayed: right
    
  227.     // before the second keystroke). Other input methods (e.g., paste) seem to
    
  228.     // fire selectionchange normally.
    
  229.     return getInstIfValueChanged(activeElementInst);
    
  230.   }
    
  231. }
    
  232. 
    
  233. /**
    
  234.  * SECTION: handle `click` event
    
  235.  */
    
  236. function shouldUseClickEvent(elem: any) {
    
  237.   // Use the `click` event to detect changes to checkbox and radio inputs.
    
  238.   // This approach works across all browsers, whereas `change` does not fire
    
  239.   // until `blur` in IE8.
    
  240.   const nodeName = elem.nodeName;
    
  241.   return (
    
  242.     nodeName &&
    
  243.     nodeName.toLowerCase() === 'input' &&
    
  244.     (elem.type === 'checkbox' || elem.type === 'radio')
    
  245.   );
    
  246. }
    
  247. 
    
  248. function getTargetInstForClickEvent(
    
  249.   domEventName: DOMEventName,
    
  250.   targetInst: null | Fiber,
    
  251. ) {
    
  252.   if (domEventName === 'click') {
    
  253.     return getInstIfValueChanged(targetInst);
    
  254.   }
    
  255. }
    
  256. 
    
  257. function getTargetInstForInputOrChangeEvent(
    
  258.   domEventName: DOMEventName,
    
  259.   targetInst: null | Fiber,
    
  260. ) {
    
  261.   if (domEventName === 'input' || domEventName === 'change') {
    
  262.     return getInstIfValueChanged(targetInst);
    
  263.   }
    
  264. }
    
  265. 
    
  266. function handleControlledInputBlur(node: HTMLInputElement, props: any) {
    
  267.   if (node.type !== 'number') {
    
  268.     return;
    
  269.   }
    
  270. 
    
  271.   if (!disableInputAttributeSyncing) {
    
  272.     const isControlled = props.value != null;
    
  273.     if (isControlled) {
    
  274.       // If controlled, assign the value attribute to the current value on blur
    
  275.       setDefaultValue((node: any), 'number', (node: any).value);
    
  276.     }
    
  277.   }
    
  278. }
    
  279. 
    
  280. /**
    
  281.  * This plugin creates an `onChange` event that normalizes change events
    
  282.  * across form elements. This event fires at a time when it's possible to
    
  283.  * change the element's value without seeing a flicker.
    
  284.  *
    
  285.  * Supported elements are:
    
  286.  * - input (see `isTextInputElement`)
    
  287.  * - textarea
    
  288.  * - select
    
  289.  */
    
  290. function extractEvents(
    
  291.   dispatchQueue: DispatchQueue,
    
  292.   domEventName: DOMEventName,
    
  293.   targetInst: null | Fiber,
    
  294.   nativeEvent: AnyNativeEvent,
    
  295.   nativeEventTarget: null | EventTarget,
    
  296.   eventSystemFlags: EventSystemFlags,
    
  297.   targetContainer: null | EventTarget,
    
  298. ) {
    
  299.   const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
    
  300. 
    
  301.   let getTargetInstFunc, handleEventFunc;
    
  302.   if (shouldUseChangeEvent(targetNode)) {
    
  303.     getTargetInstFunc = getTargetInstForChangeEvent;
    
  304.   } else if (isTextInputElement(((targetNode: any): HTMLElement))) {
    
  305.     if (isInputEventSupported) {
    
  306.       getTargetInstFunc = getTargetInstForInputOrChangeEvent;
    
  307.     } else {
    
  308.       getTargetInstFunc = getTargetInstForInputEventPolyfill;
    
  309.       handleEventFunc = handleEventsForInputEventPolyfill;
    
  310.     }
    
  311.   } else if (shouldUseClickEvent(targetNode)) {
    
  312.     getTargetInstFunc = getTargetInstForClickEvent;
    
  313.   } else if (
    
  314.     enableCustomElementPropertySupport &&
    
  315.     targetInst &&
    
  316.     isCustomElement(targetInst.elementType, targetInst.memoizedProps)
    
  317.   ) {
    
  318.     getTargetInstFunc = getTargetInstForChangeEvent;
    
  319.   }
    
  320. 
    
  321.   if (getTargetInstFunc) {
    
  322.     const inst = getTargetInstFunc(domEventName, targetInst);
    
  323.     if (inst) {
    
  324.       createAndAccumulateChangeEvent(
    
  325.         dispatchQueue,
    
  326.         inst,
    
  327.         nativeEvent,
    
  328.         nativeEventTarget,
    
  329.       );
    
  330.       return;
    
  331.     }
    
  332.   }
    
  333. 
    
  334.   if (handleEventFunc) {
    
  335.     handleEventFunc(domEventName, targetNode, targetInst);
    
  336.   }
    
  337. 
    
  338.   // When blurring, set the value attribute for number inputs
    
  339.   if (domEventName === 'focusout' && targetInst) {
    
  340.     // These props aren't necessarily the most current but we warn for changing
    
  341.     // between controlled and uncontrolled, so it doesn't matter and the previous
    
  342.     // code was also broken for changes.
    
  343.     const props = targetInst.memoizedProps;
    
  344.     handleControlledInputBlur(((targetNode: any): HTMLInputElement), props);
    
  345.   }
    
  346. }
    
  347. 
    
  348. export {registerEvents, extractEvents};