mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
499 lines
14 KiB
JavaScript
499 lines
14 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
|
|
|
|
import {canUseDOM} from 'shared/ExecutionEnvironment';
|
|
|
|
import {
|
|
TOP_BLUR,
|
|
TOP_COMPOSITION_START,
|
|
TOP_COMPOSITION_END,
|
|
TOP_COMPOSITION_UPDATE,
|
|
TOP_KEY_DOWN,
|
|
TOP_KEY_PRESS,
|
|
TOP_KEY_UP,
|
|
TOP_MOUSE_DOWN,
|
|
TOP_TEXT_INPUT,
|
|
TOP_PASTE,
|
|
} from './DOMTopLevelEventTypes';
|
|
import {
|
|
getData as FallbackCompositionStateGetData,
|
|
initialize as FallbackCompositionStateInitialize,
|
|
reset as FallbackCompositionStateReset,
|
|
} from './FallbackCompositionState';
|
|
import SyntheticCompositionEvent from './SyntheticCompositionEvent';
|
|
import SyntheticInputEvent from './SyntheticInputEvent';
|
|
import {accumulateTwoPhaseListeners} from './DOMModernPluginEventSystem';
|
|
|
|
const END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
|
|
const START_KEYCODE = 229;
|
|
|
|
const canUseCompositionEvent = canUseDOM && 'CompositionEvent' in window;
|
|
|
|
let documentMode = null;
|
|
if (canUseDOM && 'documentMode' in document) {
|
|
documentMode = document.documentMode;
|
|
}
|
|
|
|
// Webkit offers a very useful `textInput` event that can be used to
|
|
// directly represent `beforeInput`. The IE `textinput` event is not as
|
|
// useful, so we don't use it.
|
|
const canUseTextInputEvent =
|
|
canUseDOM && 'TextEvent' in window && !documentMode;
|
|
|
|
// In IE9+, we have access to composition events, but the data supplied
|
|
// by the native compositionend event may be incorrect. Japanese ideographic
|
|
// spaces, for instance (\u3000) are not recorded correctly.
|
|
const useFallbackCompositionData =
|
|
canUseDOM &&
|
|
(!canUseCompositionEvent ||
|
|
(documentMode && documentMode > 8 && documentMode <= 11));
|
|
|
|
const SPACEBAR_CODE = 32;
|
|
const SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
|
|
|
|
// Events and their corresponding property names.
|
|
const eventTypes = {
|
|
beforeInput: {
|
|
phasedRegistrationNames: {
|
|
bubbled: 'onBeforeInput',
|
|
captured: 'onBeforeInputCapture',
|
|
},
|
|
dependencies: [
|
|
TOP_COMPOSITION_END,
|
|
TOP_KEY_PRESS,
|
|
TOP_TEXT_INPUT,
|
|
TOP_PASTE,
|
|
],
|
|
},
|
|
compositionEnd: {
|
|
phasedRegistrationNames: {
|
|
bubbled: 'onCompositionEnd',
|
|
captured: 'onCompositionEndCapture',
|
|
},
|
|
dependencies: [
|
|
TOP_BLUR,
|
|
TOP_COMPOSITION_END,
|
|
TOP_KEY_DOWN,
|
|
TOP_KEY_PRESS,
|
|
TOP_KEY_UP,
|
|
TOP_MOUSE_DOWN,
|
|
],
|
|
},
|
|
compositionStart: {
|
|
phasedRegistrationNames: {
|
|
bubbled: 'onCompositionStart',
|
|
captured: 'onCompositionStartCapture',
|
|
},
|
|
dependencies: [
|
|
TOP_BLUR,
|
|
TOP_COMPOSITION_START,
|
|
TOP_KEY_DOWN,
|
|
TOP_KEY_PRESS,
|
|
TOP_KEY_UP,
|
|
TOP_MOUSE_DOWN,
|
|
],
|
|
},
|
|
compositionUpdate: {
|
|
phasedRegistrationNames: {
|
|
bubbled: 'onCompositionUpdate',
|
|
captured: 'onCompositionUpdateCapture',
|
|
},
|
|
dependencies: [
|
|
TOP_BLUR,
|
|
TOP_COMPOSITION_UPDATE,
|
|
TOP_KEY_DOWN,
|
|
TOP_KEY_PRESS,
|
|
TOP_KEY_UP,
|
|
TOP_MOUSE_DOWN,
|
|
],
|
|
},
|
|
};
|
|
|
|
// Track whether we've ever handled a keypress on the space key.
|
|
let hasSpaceKeypress = false;
|
|
|
|
/**
|
|
* Return whether a native keypress event is assumed to be a command.
|
|
* This is required because Firefox fires `keypress` events for key commands
|
|
* (cut, copy, select-all, etc.) even though no character is inserted.
|
|
*/
|
|
function isKeypressCommand(nativeEvent) {
|
|
return (
|
|
(nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
|
|
// ctrlKey && altKey is equivalent to AltGr, and is not a command.
|
|
!(nativeEvent.ctrlKey && nativeEvent.altKey)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Translate native top level events into event types.
|
|
*
|
|
* @param {string} topLevelType
|
|
* @return {object}
|
|
*/
|
|
function getCompositionEventType(topLevelType) {
|
|
switch (topLevelType) {
|
|
case TOP_COMPOSITION_START:
|
|
return eventTypes.compositionStart;
|
|
case TOP_COMPOSITION_END:
|
|
return eventTypes.compositionEnd;
|
|
case TOP_COMPOSITION_UPDATE:
|
|
return eventTypes.compositionUpdate;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Does our fallback best-guess model think this event signifies that
|
|
* composition has begun?
|
|
*
|
|
* @param {string} topLevelType
|
|
* @param {object} nativeEvent
|
|
* @return {boolean}
|
|
*/
|
|
function isFallbackCompositionStart(topLevelType, nativeEvent) {
|
|
return topLevelType === TOP_KEY_DOWN && nativeEvent.keyCode === START_KEYCODE;
|
|
}
|
|
|
|
/**
|
|
* Does our fallback mode think that this event is the end of composition?
|
|
*
|
|
* @param {string} topLevelType
|
|
* @param {object} nativeEvent
|
|
* @return {boolean}
|
|
*/
|
|
function isFallbackCompositionEnd(topLevelType, nativeEvent) {
|
|
switch (topLevelType) {
|
|
case TOP_KEY_UP:
|
|
// Command keys insert or clear IME input.
|
|
return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
|
|
case TOP_KEY_DOWN:
|
|
// Expect IME keyCode on each keydown. If we get any other
|
|
// code we must have exited earlier.
|
|
return nativeEvent.keyCode !== START_KEYCODE;
|
|
case TOP_KEY_PRESS:
|
|
case TOP_MOUSE_DOWN:
|
|
case TOP_BLUR:
|
|
// Events are not possible without cancelling IME.
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Google Input Tools provides composition data via a CustomEvent,
|
|
* with the `data` property populated in the `detail` object. If this
|
|
* is available on the event object, use it. If not, this is a plain
|
|
* composition event and we have nothing special to extract.
|
|
*
|
|
* @param {object} nativeEvent
|
|
* @return {?string}
|
|
*/
|
|
function getDataFromCustomEvent(nativeEvent) {
|
|
const detail = nativeEvent.detail;
|
|
if (typeof detail === 'object' && 'data' in detail) {
|
|
return detail.data;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Check if a composition event was triggered by Korean IME.
|
|
* Our fallback mode does not work well with IE's Korean IME,
|
|
* so just use native composition events when Korean IME is used.
|
|
* Although CompositionEvent.locale property is deprecated,
|
|
* it is available in IE, where our fallback mode is enabled.
|
|
*
|
|
* @param {object} nativeEvent
|
|
* @return {boolean}
|
|
*/
|
|
function isUsingKoreanIME(nativeEvent) {
|
|
return nativeEvent.locale === 'ko';
|
|
}
|
|
|
|
// Track the current IME composition status, if any.
|
|
let isComposing = false;
|
|
|
|
/**
|
|
* @return {?object} A SyntheticCompositionEvent.
|
|
*/
|
|
function extractCompositionEvent(
|
|
topLevelType,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
) {
|
|
let eventType;
|
|
let fallbackData;
|
|
|
|
if (canUseCompositionEvent) {
|
|
eventType = getCompositionEventType(topLevelType);
|
|
} else if (!isComposing) {
|
|
if (isFallbackCompositionStart(topLevelType, nativeEvent)) {
|
|
eventType = eventTypes.compositionStart;
|
|
}
|
|
} else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {
|
|
eventType = eventTypes.compositionEnd;
|
|
}
|
|
|
|
if (!eventType) {
|
|
return null;
|
|
}
|
|
|
|
if (useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)) {
|
|
// The current composition is stored statically and must not be
|
|
// overwritten while composition continues.
|
|
if (!isComposing && eventType === eventTypes.compositionStart) {
|
|
isComposing = FallbackCompositionStateInitialize(nativeEventTarget);
|
|
} else if (eventType === eventTypes.compositionEnd) {
|
|
if (isComposing) {
|
|
fallbackData = FallbackCompositionStateGetData();
|
|
}
|
|
}
|
|
}
|
|
|
|
const event = SyntheticCompositionEvent.getPooled(
|
|
eventType,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
);
|
|
|
|
if (fallbackData) {
|
|
// Inject data generated from fallback path into the synthetic event.
|
|
// This matches the property of native CompositionEventInterface.
|
|
event.data = fallbackData;
|
|
} else {
|
|
const customData = getDataFromCustomEvent(nativeEvent);
|
|
if (customData !== null) {
|
|
event.data = customData;
|
|
}
|
|
}
|
|
|
|
accumulateTwoPhaseListeners(event);
|
|
return event;
|
|
}
|
|
|
|
/**
|
|
* @param {TopLevelType} topLevelType Number from `TopLevelType`.
|
|
* @param {object} nativeEvent Native browser event.
|
|
* @return {?string} The string corresponding to this `beforeInput` event.
|
|
*/
|
|
function getNativeBeforeInputChars(topLevelType: TopLevelType, nativeEvent) {
|
|
switch (topLevelType) {
|
|
case TOP_COMPOSITION_END:
|
|
return getDataFromCustomEvent(nativeEvent);
|
|
case TOP_KEY_PRESS:
|
|
/**
|
|
* If native `textInput` events are available, our goal is to make
|
|
* use of them. However, there is a special case: the spacebar key.
|
|
* In Webkit, preventing default on a spacebar `textInput` event
|
|
* cancels character insertion, but it *also* causes the browser
|
|
* to fall back to its default spacebar behavior of scrolling the
|
|
* page.
|
|
*
|
|
* Tracking at:
|
|
* https://code.google.com/p/chromium/issues/detail?id=355103
|
|
*
|
|
* To avoid this issue, use the keypress event as if no `textInput`
|
|
* event is available.
|
|
*/
|
|
const which = nativeEvent.which;
|
|
if (which !== SPACEBAR_CODE) {
|
|
return null;
|
|
}
|
|
|
|
hasSpaceKeypress = true;
|
|
return SPACEBAR_CHAR;
|
|
|
|
case TOP_TEXT_INPUT:
|
|
// Record the characters to be added to the DOM.
|
|
const chars = nativeEvent.data;
|
|
|
|
// If it's a spacebar character, assume that we have already handled
|
|
// it at the keypress level and bail immediately. Android Chrome
|
|
// doesn't give us keycodes, so we need to ignore it.
|
|
if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
|
|
return null;
|
|
}
|
|
|
|
return chars;
|
|
|
|
default:
|
|
// For other native event types, do nothing.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For browsers that do not provide the `textInput` event, extract the
|
|
* appropriate string to use for SyntheticInputEvent.
|
|
*
|
|
* @param {number} topLevelType Number from `TopLevelEventTypes`.
|
|
* @param {object} nativeEvent Native browser event.
|
|
* @return {?string} The fallback string for this `beforeInput` event.
|
|
*/
|
|
function getFallbackBeforeInputChars(topLevelType: TopLevelType, nativeEvent) {
|
|
// If we are currently composing (IME) and using a fallback to do so,
|
|
// try to extract the composed characters from the fallback object.
|
|
// If composition event is available, we extract a string only at
|
|
// compositionevent, otherwise extract it at fallback events.
|
|
if (isComposing) {
|
|
if (
|
|
topLevelType === TOP_COMPOSITION_END ||
|
|
(!canUseCompositionEvent &&
|
|
isFallbackCompositionEnd(topLevelType, nativeEvent))
|
|
) {
|
|
const chars = FallbackCompositionStateGetData();
|
|
FallbackCompositionStateReset();
|
|
isComposing = false;
|
|
return chars;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
switch (topLevelType) {
|
|
case TOP_PASTE:
|
|
// If a paste event occurs after a keypress, throw out the input
|
|
// chars. Paste events should not lead to BeforeInput events.
|
|
return null;
|
|
case TOP_KEY_PRESS:
|
|
/**
|
|
* As of v27, Firefox may fire keypress events even when no character
|
|
* will be inserted. A few possibilities:
|
|
*
|
|
* - `which` is `0`. Arrow keys, Esc key, etc.
|
|
*
|
|
* - `which` is the pressed key code, but no char is available.
|
|
* Ex: 'AltGr + d` in Polish. There is no modified character for
|
|
* this key combination and no character is inserted into the
|
|
* document, but FF fires the keypress for char code `100` anyway.
|
|
* No `input` event will occur.
|
|
*
|
|
* - `which` is the pressed key code, but a command combination is
|
|
* being used. Ex: `Cmd+C`. No character is inserted, and no
|
|
* `input` event will occur.
|
|
*/
|
|
if (!isKeypressCommand(nativeEvent)) {
|
|
// IE fires the `keypress` event when a user types an emoji via
|
|
// Touch keyboard of Windows. In such a case, the `char` property
|
|
// holds an emoji character like `\uD83D\uDE0A`. Because its length
|
|
// is 2, the property `which` does not represent an emoji correctly.
|
|
// In such a case, we directly return the `char` property instead of
|
|
// using `which`.
|
|
if (nativeEvent.char && nativeEvent.char.length > 1) {
|
|
return nativeEvent.char;
|
|
} else if (nativeEvent.which) {
|
|
return String.fromCharCode(nativeEvent.which);
|
|
}
|
|
}
|
|
return null;
|
|
case TOP_COMPOSITION_END:
|
|
return useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)
|
|
? null
|
|
: nativeEvent.data;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract a SyntheticInputEvent for `beforeInput`, based on either native
|
|
* `textInput` or fallback behavior.
|
|
*
|
|
* @return {?object} A SyntheticInputEvent.
|
|
*/
|
|
function extractBeforeInputEvent(
|
|
topLevelType,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
) {
|
|
let chars;
|
|
|
|
if (canUseTextInputEvent) {
|
|
chars = getNativeBeforeInputChars(topLevelType, nativeEvent);
|
|
} else {
|
|
chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);
|
|
}
|
|
|
|
// If no characters are being inserted, no BeforeInput event should
|
|
// be fired.
|
|
if (!chars) {
|
|
return null;
|
|
}
|
|
|
|
const event = SyntheticInputEvent.getPooled(
|
|
eventTypes.beforeInput,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
);
|
|
|
|
event.data = chars;
|
|
accumulateTwoPhaseListeners(event);
|
|
return event;
|
|
}
|
|
|
|
/**
|
|
* Create an `onBeforeInput` event to match
|
|
* http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
|
|
*
|
|
* This event plugin is based on the native `textInput` event
|
|
* available in Chrome, Safari, Opera, and IE. This event fires after
|
|
* `onKeyPress` and `onCompositionEnd`, but before `onInput`.
|
|
*
|
|
* `beforeInput` is spec'd but not implemented in any browsers, and
|
|
* the `input` event does not provide any useful information about what has
|
|
* actually been added, contrary to the spec. Thus, `textInput` is the best
|
|
* available event to identify the characters that have actually been inserted
|
|
* into the target node.
|
|
*
|
|
* This plugin is also responsible for emitting `composition` events, thus
|
|
* allowing us to share composition fallback code for both `beforeInput` and
|
|
* `composition` event types.
|
|
*/
|
|
const BeforeInputEventPlugin = {
|
|
eventTypes: eventTypes,
|
|
|
|
extractEvents: function(
|
|
topLevelType,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
eventSystemFlags,
|
|
) {
|
|
const composition = extractCompositionEvent(
|
|
topLevelType,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
);
|
|
|
|
const beforeInput = extractBeforeInputEvent(
|
|
topLevelType,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
);
|
|
|
|
if (composition === null) {
|
|
return beforeInput;
|
|
}
|
|
|
|
if (beforeInput === null) {
|
|
return composition;
|
|
}
|
|
|
|
return [composition, beforeInput];
|
|
},
|
|
};
|
|
|
|
export default BeforeInputEventPlugin;
|