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 {DOMEventName} from '../../events/DOMEventNames';
    
  11. import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
    
  12. import type {AnyNativeEvent} from '../../events/PluginModuleType';
    
  13. import type {DispatchQueue} from '../DOMPluginEventSystem';
    
  14. import type {EventSystemFlags} from '../EventSystemFlags';
    
  15. import type {ReactSyntheticEvent} from '../ReactSyntheticEventType';
    
  16. 
    
  17. import {canUseDOM} from 'shared/ExecutionEnvironment';
    
  18. 
    
  19. import {registerTwoPhaseEvent} from '../EventRegistry';
    
  20. import {
    
  21.   getData as FallbackCompositionStateGetData,
    
  22.   initialize as FallbackCompositionStateInitialize,
    
  23.   reset as FallbackCompositionStateReset,
    
  24. } from '../FallbackCompositionState';
    
  25. import {
    
  26.   SyntheticCompositionEvent,
    
  27.   SyntheticInputEvent,
    
  28. } from '../SyntheticEvent';
    
  29. import {accumulateTwoPhaseListeners} from '../DOMPluginEventSystem';
    
  30. 
    
  31. const END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
    
  32. const START_KEYCODE = 229;
    
  33. 
    
  34. const canUseCompositionEvent = canUseDOM && 'CompositionEvent' in window;
    
  35. 
    
  36. let documentMode = null;
    
  37. if (canUseDOM && 'documentMode' in document) {
    
  38.   documentMode = document.documentMode;
    
  39. }
    
  40. 
    
  41. // Webkit offers a very useful `textInput` event that can be used to
    
  42. // directly represent `beforeInput`. The IE `textinput` event is not as
    
  43. // useful, so we don't use it.
    
  44. const canUseTextInputEvent =
    
  45.   canUseDOM && 'TextEvent' in window && !documentMode;
    
  46. 
    
  47. // In IE9+, we have access to composition events, but the data supplied
    
  48. // by the native compositionend event may be incorrect. Japanese ideographic
    
  49. // spaces, for instance (\u3000) are not recorded correctly.
    
  50. const useFallbackCompositionData =
    
  51.   canUseDOM &&
    
  52.   (!canUseCompositionEvent ||
    
  53.     (documentMode && documentMode > 8 && documentMode <= 11));
    
  54. 
    
  55. const SPACEBAR_CODE = 32;
    
  56. const SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
    
  57. 
    
  58. function registerEvents() {
    
  59.   registerTwoPhaseEvent('onBeforeInput', [
    
  60.     'compositionend',
    
  61.     'keypress',
    
  62.     'textInput',
    
  63.     'paste',
    
  64.   ]);
    
  65.   registerTwoPhaseEvent('onCompositionEnd', [
    
  66.     'compositionend',
    
  67.     'focusout',
    
  68.     'keydown',
    
  69.     'keypress',
    
  70.     'keyup',
    
  71.     'mousedown',
    
  72.   ]);
    
  73.   registerTwoPhaseEvent('onCompositionStart', [
    
  74.     'compositionstart',
    
  75.     'focusout',
    
  76.     'keydown',
    
  77.     'keypress',
    
  78.     'keyup',
    
  79.     'mousedown',
    
  80.   ]);
    
  81.   registerTwoPhaseEvent('onCompositionUpdate', [
    
  82.     'compositionupdate',
    
  83.     'focusout',
    
  84.     'keydown',
    
  85.     'keypress',
    
  86.     'keyup',
    
  87.     'mousedown',
    
  88.   ]);
    
  89. }
    
  90. 
    
  91. // Track whether we've ever handled a keypress on the space key.
    
  92. let hasSpaceKeypress = false;
    
  93. 
    
  94. /**
    
  95.  * Return whether a native keypress event is assumed to be a command.
    
  96.  * This is required because Firefox fires `keypress` events for key commands
    
  97.  * (cut, copy, select-all, etc.) even though no character is inserted.
    
  98.  */
    
  99. function isKeypressCommand(nativeEvent: any) {
    
  100.   return (
    
  101.     (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
    
  102.     // ctrlKey && altKey is equivalent to AltGr, and is not a command.
    
  103.     !(nativeEvent.ctrlKey && nativeEvent.altKey)
    
  104.   );
    
  105. }
    
  106. 
    
  107. /**
    
  108.  * Translate native top level events into event types.
    
  109.  */
    
  110. function getCompositionEventType(domEventName: DOMEventName) {
    
  111.   switch (domEventName) {
    
  112.     case 'compositionstart':
    
  113.       return 'onCompositionStart';
    
  114.     case 'compositionend':
    
  115.       return 'onCompositionEnd';
    
  116.     case 'compositionupdate':
    
  117.       return 'onCompositionUpdate';
    
  118.   }
    
  119. }
    
  120. 
    
  121. /**
    
  122.  * Does our fallback best-guess model think this event signifies that
    
  123.  * composition has begun?
    
  124.  */
    
  125. function isFallbackCompositionStart(
    
  126.   domEventName: DOMEventName,
    
  127.   nativeEvent: any,
    
  128. ): boolean {
    
  129.   return domEventName === 'keydown' && nativeEvent.keyCode === START_KEYCODE;
    
  130. }
    
  131. 
    
  132. /**
    
  133.  * Does our fallback mode think that this event is the end of composition?
    
  134.  */
    
  135. function isFallbackCompositionEnd(
    
  136.   domEventName: DOMEventName,
    
  137.   nativeEvent: any,
    
  138. ): boolean {
    
  139.   switch (domEventName) {
    
  140.     case 'keyup':
    
  141.       // Command keys insert or clear IME input.
    
  142.       return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
    
  143.     case 'keydown':
    
  144.       // Expect IME keyCode on each keydown. If we get any other
    
  145.       // code we must have exited earlier.
    
  146.       return nativeEvent.keyCode !== START_KEYCODE;
    
  147.     case 'keypress':
    
  148.     case 'mousedown':
    
  149.     case 'focusout':
    
  150.       // Events are not possible without cancelling IME.
    
  151.       return true;
    
  152.     default:
    
  153.       return false;
    
  154.   }
    
  155. }
    
  156. 
    
  157. /**
    
  158.  * Google Input Tools provides composition data via a CustomEvent,
    
  159.  * with the `data` property populated in the `detail` object. If this
    
  160.  * is available on the event object, use it. If not, this is a plain
    
  161.  * composition event and we have nothing special to extract.
    
  162.  *
    
  163.  * @param {object} nativeEvent
    
  164.  * @return {?string}
    
  165.  */
    
  166. function getDataFromCustomEvent(nativeEvent: any) {
    
  167.   const detail = nativeEvent.detail;
    
  168.   if (typeof detail === 'object' && 'data' in detail) {
    
  169.     return detail.data;
    
  170.   }
    
  171.   return null;
    
  172. }
    
  173. 
    
  174. /**
    
  175.  * Check if a composition event was triggered by Korean IME.
    
  176.  * Our fallback mode does not work well with IE's Korean IME,
    
  177.  * so just use native composition events when Korean IME is used.
    
  178.  * Although CompositionEvent.locale property is deprecated,
    
  179.  * it is available in IE, where our fallback mode is enabled.
    
  180.  *
    
  181.  * @param {object} nativeEvent
    
  182.  * @return {boolean}
    
  183.  */
    
  184. function isUsingKoreanIME(nativeEvent: any) {
    
  185.   return nativeEvent.locale === 'ko';
    
  186. }
    
  187. 
    
  188. // Track the current IME composition status, if any.
    
  189. let isComposing = false;
    
  190. 
    
  191. /**
    
  192.  * @return {?object} A SyntheticCompositionEvent.
    
  193.  */
    
  194. function extractCompositionEvent(
    
  195.   dispatchQueue: DispatchQueue,
    
  196.   domEventName: DOMEventName,
    
  197.   targetInst: null | Fiber,
    
  198.   nativeEvent: AnyNativeEvent,
    
  199.   nativeEventTarget: null | EventTarget,
    
  200. ) {
    
  201.   let eventType;
    
  202.   let fallbackData;
    
  203. 
    
  204.   if (canUseCompositionEvent) {
    
  205.     eventType = getCompositionEventType(domEventName);
    
  206.   } else if (!isComposing) {
    
  207.     if (isFallbackCompositionStart(domEventName, nativeEvent)) {
    
  208.       eventType = 'onCompositionStart';
    
  209.     }
    
  210.   } else if (isFallbackCompositionEnd(domEventName, nativeEvent)) {
    
  211.     eventType = 'onCompositionEnd';
    
  212.   }
    
  213. 
    
  214.   if (!eventType) {
    
  215.     return null;
    
  216.   }
    
  217. 
    
  218.   if (useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)) {
    
  219.     // The current composition is stored statically and must not be
    
  220.     // overwritten while composition continues.
    
  221.     if (!isComposing && eventType === 'onCompositionStart') {
    
  222.       isComposing = FallbackCompositionStateInitialize(nativeEventTarget);
    
  223.     } else if (eventType === 'onCompositionEnd') {
    
  224.       if (isComposing) {
    
  225.         fallbackData = FallbackCompositionStateGetData();
    
  226.       }
    
  227.     }
    
  228.   }
    
  229. 
    
  230.   const listeners = accumulateTwoPhaseListeners(targetInst, eventType);
    
  231.   if (listeners.length > 0) {
    
  232.     const event: ReactSyntheticEvent = new SyntheticCompositionEvent(
    
  233.       eventType,
    
  234.       domEventName,
    
  235.       null,
    
  236.       nativeEvent,
    
  237.       nativeEventTarget,
    
  238.     );
    
  239.     dispatchQueue.push({event, listeners});
    
  240.     if (fallbackData) {
    
  241.       // Inject data generated from fallback path into the synthetic event.
    
  242.       // This matches the property of native CompositionEventInterface.
    
  243.       // $FlowFixMe[incompatible-use]
    
  244.       event.data = fallbackData;
    
  245.     } else {
    
  246.       const customData = getDataFromCustomEvent(nativeEvent);
    
  247.       if (customData !== null) {
    
  248.         // $FlowFixMe[incompatible-use]
    
  249.         event.data = customData;
    
  250.       }
    
  251.     }
    
  252.   }
    
  253. }
    
  254. 
    
  255. function getNativeBeforeInputChars(
    
  256.   domEventName: DOMEventName,
    
  257.   nativeEvent: any,
    
  258. ): ?string {
    
  259.   switch (domEventName) {
    
  260.     case 'compositionend':
    
  261.       return getDataFromCustomEvent(nativeEvent);
    
  262.     case 'keypress':
    
  263.       /**
    
  264.        * If native `textInput` events are available, our goal is to make
    
  265.        * use of them. However, there is a special case: the spacebar key.
    
  266.        * In Webkit, preventing default on a spacebar `textInput` event
    
  267.        * cancels character insertion, but it *also* causes the browser
    
  268.        * to fall back to its default spacebar behavior of scrolling the
    
  269.        * page.
    
  270.        *
    
  271.        * Tracking at:
    
  272.        * https://code.google.com/p/chromium/issues/detail?id=355103
    
  273.        *
    
  274.        * To avoid this issue, use the keypress event as if no `textInput`
    
  275.        * event is available.
    
  276.        */
    
  277.       const which = nativeEvent.which;
    
  278.       if (which !== SPACEBAR_CODE) {
    
  279.         return null;
    
  280.       }
    
  281. 
    
  282.       hasSpaceKeypress = true;
    
  283.       return SPACEBAR_CHAR;
    
  284. 
    
  285.     case 'textInput':
    
  286.       // Record the characters to be added to the DOM.
    
  287.       const chars = nativeEvent.data;
    
  288. 
    
  289.       // If it's a spacebar character, assume that we have already handled
    
  290.       // it at the keypress level and bail immediately. Android Chrome
    
  291.       // doesn't give us keycodes, so we need to ignore it.
    
  292.       if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
    
  293.         return null;
    
  294.       }
    
  295. 
    
  296.       return chars;
    
  297. 
    
  298.     default:
    
  299.       // For other native event types, do nothing.
    
  300.       return null;
    
  301.   }
    
  302. }
    
  303. 
    
  304. /**
    
  305.  * For browsers that do not provide the `textInput` event, extract the
    
  306.  * appropriate string to use for SyntheticInputEvent.
    
  307.  */
    
  308. function getFallbackBeforeInputChars(
    
  309.   domEventName: DOMEventName,
    
  310.   nativeEvent: any,
    
  311. ): ?string {
    
  312.   // If we are currently composing (IME) and using a fallback to do so,
    
  313.   // try to extract the composed characters from the fallback object.
    
  314.   // If composition event is available, we extract a string only at
    
  315.   // compositionevent, otherwise extract it at fallback events.
    
  316.   if (isComposing) {
    
  317.     if (
    
  318.       domEventName === 'compositionend' ||
    
  319.       (!canUseCompositionEvent &&
    
  320.         isFallbackCompositionEnd(domEventName, nativeEvent))
    
  321.     ) {
    
  322.       const chars = FallbackCompositionStateGetData();
    
  323.       FallbackCompositionStateReset();
    
  324.       isComposing = false;
    
  325.       return chars;
    
  326.     }
    
  327.     return null;
    
  328.   }
    
  329. 
    
  330.   switch (domEventName) {
    
  331.     case 'paste':
    
  332.       // If a paste event occurs after a keypress, throw out the input
    
  333.       // chars. Paste events should not lead to BeforeInput events.
    
  334.       return null;
    
  335.     case 'keypress':
    
  336.       /**
    
  337.        * As of v27, Firefox may fire keypress events even when no character
    
  338.        * will be inserted. A few possibilities:
    
  339.        *
    
  340.        * - `which` is `0`. Arrow keys, Esc key, etc.
    
  341.        *
    
  342.        * - `which` is the pressed key code, but no char is available.
    
  343.        *   Ex: 'AltGr + d` in Polish. There is no modified character for
    
  344.        *   this key combination and no character is inserted into the
    
  345.        *   document, but FF fires the keypress for char code `100` anyway.
    
  346.        *   No `input` event will occur.
    
  347.        *
    
  348.        * - `which` is the pressed key code, but a command combination is
    
  349.        *   being used. Ex: `Cmd+C`. No character is inserted, and no
    
  350.        *   `input` event will occur.
    
  351.        */
    
  352.       if (!isKeypressCommand(nativeEvent)) {
    
  353.         // IE fires the `keypress` event when a user types an emoji via
    
  354.         // Touch keyboard of Windows.  In such a case, the `char` property
    
  355.         // holds an emoji character like `\uD83D\uDE0A`.  Because its length
    
  356.         // is 2, the property `which` does not represent an emoji correctly.
    
  357.         // In such a case, we directly return the `char` property instead of
    
  358.         // using `which`.
    
  359.         if (nativeEvent.char && nativeEvent.char.length > 1) {
    
  360.           return nativeEvent.char;
    
  361.         } else if (nativeEvent.which) {
    
  362.           return String.fromCharCode(nativeEvent.which);
    
  363.         }
    
  364.       }
    
  365.       return null;
    
  366.     case 'compositionend':
    
  367.       return useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)
    
  368.         ? null
    
  369.         : nativeEvent.data;
    
  370.     default:
    
  371.       return null;
    
  372.   }
    
  373. }
    
  374. 
    
  375. /**
    
  376.  * Extract a SyntheticInputEvent for `beforeInput`, based on either native
    
  377.  * `textInput` or fallback behavior.
    
  378.  *
    
  379.  * @return {?object} A SyntheticInputEvent.
    
  380.  */
    
  381. function extractBeforeInputEvent(
    
  382.   dispatchQueue: DispatchQueue,
    
  383.   domEventName: DOMEventName,
    
  384.   targetInst: null | Fiber,
    
  385.   nativeEvent: AnyNativeEvent,
    
  386.   nativeEventTarget: null | EventTarget,
    
  387. ) {
    
  388.   let chars;
    
  389. 
    
  390.   if (canUseTextInputEvent) {
    
  391.     chars = getNativeBeforeInputChars(domEventName, nativeEvent);
    
  392.   } else {
    
  393.     chars = getFallbackBeforeInputChars(domEventName, nativeEvent);
    
  394.   }
    
  395. 
    
  396.   // If no characters are being inserted, no BeforeInput event should
    
  397.   // be fired.
    
  398.   if (!chars) {
    
  399.     return null;
    
  400.   }
    
  401. 
    
  402.   const listeners = accumulateTwoPhaseListeners(targetInst, 'onBeforeInput');
    
  403.   if (listeners.length > 0) {
    
  404.     const event: ReactSyntheticEvent = new SyntheticInputEvent(
    
  405.       'onBeforeInput',
    
  406.       'beforeinput',
    
  407.       null,
    
  408.       nativeEvent,
    
  409.       nativeEventTarget,
    
  410.     );
    
  411.     dispatchQueue.push({event, listeners});
    
  412.     // $FlowFixMe[incompatible-use]
    
  413.     event.data = chars;
    
  414.   }
    
  415. }
    
  416. 
    
  417. /**
    
  418.  * Create an `onBeforeInput` event to match
    
  419.  * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
    
  420.  *
    
  421.  * This event plugin is based on the native `textInput` event
    
  422.  * available in Chrome, Safari, Opera, and IE. This event fires after
    
  423.  * `onKeyPress` and `onCompositionEnd`, but before `onInput`.
    
  424.  *
    
  425.  * `beforeInput` is spec'd but not implemented in any browsers, and
    
  426.  * the `input` event does not provide any useful information about what has
    
  427.  * actually been added, contrary to the spec. Thus, `textInput` is the best
    
  428.  * available event to identify the characters that have actually been inserted
    
  429.  * into the target node.
    
  430.  *
    
  431.  * This plugin is also responsible for emitting `composition` events, thus
    
  432.  * allowing us to share composition fallback code for both `beforeInput` and
    
  433.  * `composition` event types.
    
  434.  */
    
  435. function extractEvents(
    
  436.   dispatchQueue: DispatchQueue,
    
  437.   domEventName: DOMEventName,
    
  438.   targetInst: null | Fiber,
    
  439.   nativeEvent: AnyNativeEvent,
    
  440.   nativeEventTarget: null | EventTarget,
    
  441.   eventSystemFlags: EventSystemFlags,
    
  442.   targetContainer: EventTarget,
    
  443. ): void {
    
  444.   extractCompositionEvent(
    
  445.     dispatchQueue,
    
  446.     domEventName,
    
  447.     targetInst,
    
  448.     nativeEvent,
    
  449.     nativeEventTarget,
    
  450.   );
    
  451.   extractBeforeInputEvent(
    
  452.     dispatchQueue,
    
  453.     domEventName,
    
  454.     targetInst,
    
  455.     nativeEvent,
    
  456.     nativeEventTarget,
    
  457.   );
    
  458. }
    
  459. 
    
  460. export {registerEvents, extractEvents};