You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
493 lines
15 KiB
493 lines
15 KiB
/** |
|
* Copyright 2013-2015 Facebook, Inc. |
|
* All rights reserved. |
|
* |
|
* This source code is licensed under the BSD-style license found in the |
|
* LICENSE file in the root directory of this source tree. An additional grant |
|
* of patent rights can be found in the PATENTS file in the same directory. |
|
* |
|
* @providesModule BeforeInputEventPlugin |
|
* @typechecks static-only |
|
*/ |
|
|
|
'use strict'; |
|
|
|
var EventConstants = require("./EventConstants"); |
|
var EventPropagators = require("./EventPropagators"); |
|
var ExecutionEnvironment = require("./ExecutionEnvironment"); |
|
var FallbackCompositionState = require("./FallbackCompositionState"); |
|
var SyntheticCompositionEvent = require("./SyntheticCompositionEvent"); |
|
var SyntheticInputEvent = require("./SyntheticInputEvent"); |
|
|
|
var keyOf = require("./keyOf"); |
|
|
|
var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space |
|
var START_KEYCODE = 229; |
|
|
|
var canUseCompositionEvent = ( |
|
ExecutionEnvironment.canUseDOM && |
|
'CompositionEvent' in window |
|
); |
|
|
|
var documentMode = null; |
|
if (ExecutionEnvironment.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. |
|
var canUseTextInputEvent = ( |
|
ExecutionEnvironment.canUseDOM && |
|
'TextEvent' in window && |
|
!documentMode && |
|
!isPresto() |
|
); |
|
|
|
// 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. |
|
var useFallbackCompositionData = ( |
|
ExecutionEnvironment.canUseDOM && |
|
( |
|
(!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11) |
|
) |
|
); |
|
|
|
/** |
|
* Opera <= 12 includes TextEvent in window, but does not fire |
|
* text input events. Rely on keypress instead. |
|
*/ |
|
function isPresto() { |
|
var opera = window.opera; |
|
return ( |
|
typeof opera === 'object' && |
|
typeof opera.version === 'function' && |
|
parseInt(opera.version(), 10) <= 12 |
|
); |
|
} |
|
|
|
var SPACEBAR_CODE = 32; |
|
var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE); |
|
|
|
var topLevelTypes = EventConstants.topLevelTypes; |
|
|
|
// Events and their corresponding property names. |
|
var eventTypes = { |
|
beforeInput: { |
|
phasedRegistrationNames: { |
|
bubbled: keyOf({onBeforeInput: null}), |
|
captured: keyOf({onBeforeInputCapture: null}) |
|
}, |
|
dependencies: [ |
|
topLevelTypes.topCompositionEnd, |
|
topLevelTypes.topKeyPress, |
|
topLevelTypes.topTextInput, |
|
topLevelTypes.topPaste |
|
] |
|
}, |
|
compositionEnd: { |
|
phasedRegistrationNames: { |
|
bubbled: keyOf({onCompositionEnd: null}), |
|
captured: keyOf({onCompositionEndCapture: null}) |
|
}, |
|
dependencies: [ |
|
topLevelTypes.topBlur, |
|
topLevelTypes.topCompositionEnd, |
|
topLevelTypes.topKeyDown, |
|
topLevelTypes.topKeyPress, |
|
topLevelTypes.topKeyUp, |
|
topLevelTypes.topMouseDown |
|
] |
|
}, |
|
compositionStart: { |
|
phasedRegistrationNames: { |
|
bubbled: keyOf({onCompositionStart: null}), |
|
captured: keyOf({onCompositionStartCapture: null}) |
|
}, |
|
dependencies: [ |
|
topLevelTypes.topBlur, |
|
topLevelTypes.topCompositionStart, |
|
topLevelTypes.topKeyDown, |
|
topLevelTypes.topKeyPress, |
|
topLevelTypes.topKeyUp, |
|
topLevelTypes.topMouseDown |
|
] |
|
}, |
|
compositionUpdate: { |
|
phasedRegistrationNames: { |
|
bubbled: keyOf({onCompositionUpdate: null}), |
|
captured: keyOf({onCompositionUpdateCapture: null}) |
|
}, |
|
dependencies: [ |
|
topLevelTypes.topBlur, |
|
topLevelTypes.topCompositionUpdate, |
|
topLevelTypes.topKeyDown, |
|
topLevelTypes.topKeyPress, |
|
topLevelTypes.topKeyUp, |
|
topLevelTypes.topMouseDown |
|
] |
|
} |
|
}; |
|
|
|
// Track whether we've ever handled a keypress on the space key. |
|
var 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 topLevelTypes.topCompositionStart: |
|
return eventTypes.compositionStart; |
|
case topLevelTypes.topCompositionEnd: |
|
return eventTypes.compositionEnd; |
|
case topLevelTypes.topCompositionUpdate: |
|
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 === topLevelTypes.topKeyDown && |
|
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 topLevelTypes.topKeyUp: |
|
// Command keys insert or clear IME input. |
|
return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1); |
|
case topLevelTypes.topKeyDown: |
|
// Expect IME keyCode on each keydown. If we get any other |
|
// code we must have exited earlier. |
|
return (nativeEvent.keyCode !== START_KEYCODE); |
|
case topLevelTypes.topKeyPress: |
|
case topLevelTypes.topMouseDown: |
|
case topLevelTypes.topBlur: |
|
// 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) { |
|
var detail = nativeEvent.detail; |
|
if (typeof detail === 'object' && 'data' in detail) { |
|
return detail.data; |
|
} |
|
return null; |
|
} |
|
|
|
// Track the current IME composition fallback object, if any. |
|
var currentComposition = null; |
|
|
|
/** |
|
* @param {string} topLevelType Record from `EventConstants`. |
|
* @param {DOMEventTarget} topLevelTarget The listening component root node. |
|
* @param {string} topLevelTargetID ID of `topLevelTarget`. |
|
* @param {object} nativeEvent Native browser event. |
|
* @return {?object} A SyntheticCompositionEvent. |
|
*/ |
|
function extractCompositionEvent( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID, |
|
nativeEvent |
|
) { |
|
var eventType; |
|
var fallbackData; |
|
|
|
if (canUseCompositionEvent) { |
|
eventType = getCompositionEventType(topLevelType); |
|
} else if (!currentComposition) { |
|
if (isFallbackCompositionStart(topLevelType, nativeEvent)) { |
|
eventType = eventTypes.compositionStart; |
|
} |
|
} else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) { |
|
eventType = eventTypes.compositionEnd; |
|
} |
|
|
|
if (!eventType) { |
|
return null; |
|
} |
|
|
|
if (useFallbackCompositionData) { |
|
// The current composition is stored statically and must not be |
|
// overwritten while composition continues. |
|
if (!currentComposition && eventType === eventTypes.compositionStart) { |
|
currentComposition = FallbackCompositionState.getPooled(topLevelTarget); |
|
} else if (eventType === eventTypes.compositionEnd) { |
|
if (currentComposition) { |
|
fallbackData = currentComposition.getData(); |
|
} |
|
} |
|
} |
|
|
|
var event = SyntheticCompositionEvent.getPooled( |
|
eventType, |
|
topLevelTargetID, |
|
nativeEvent |
|
); |
|
|
|
if (fallbackData) { |
|
// Inject data generated from fallback path into the synthetic event. |
|
// This matches the property of native CompositionEventInterface. |
|
event.data = fallbackData; |
|
} else { |
|
var customData = getDataFromCustomEvent(nativeEvent); |
|
if (customData !== null) { |
|
event.data = customData; |
|
} |
|
} |
|
|
|
EventPropagators.accumulateTwoPhaseDispatches(event); |
|
return event; |
|
} |
|
|
|
/** |
|
* @param {string} topLevelType Record from `EventConstants`. |
|
* @param {object} nativeEvent Native browser event. |
|
* @return {?string} The string corresponding to this `beforeInput` event. |
|
*/ |
|
function getNativeBeforeInputChars(topLevelType, nativeEvent) { |
|
switch (topLevelType) { |
|
case topLevelTypes.topCompositionEnd: |
|
return getDataFromCustomEvent(nativeEvent); |
|
case topLevelTypes.topKeyPress: |
|
/** |
|
* 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. |
|
*/ |
|
var which = nativeEvent.which; |
|
if (which !== SPACEBAR_CODE) { |
|
return null; |
|
} |
|
|
|
hasSpaceKeypress = true; |
|
return SPACEBAR_CHAR; |
|
|
|
case topLevelTypes.topTextInput: |
|
// Record the characters to be added to the DOM. |
|
var 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 blacklist 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 {string} topLevelType Record from `EventConstants`. |
|
* @param {object} nativeEvent Native browser event. |
|
* @return {?string} The fallback string for this `beforeInput` event. |
|
*/ |
|
function getFallbackBeforeInputChars(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 (currentComposition) { |
|
if ( |
|
topLevelType === topLevelTypes.topCompositionEnd || |
|
isFallbackCompositionEnd(topLevelType, nativeEvent) |
|
) { |
|
var chars = currentComposition.getData(); |
|
FallbackCompositionState.release(currentComposition); |
|
currentComposition = null; |
|
return chars; |
|
} |
|
return null; |
|
} |
|
|
|
switch (topLevelType) { |
|
case topLevelTypes.topPaste: |
|
// If a paste event occurs after a keypress, throw out the input |
|
// chars. Paste events should not lead to BeforeInput events. |
|
return null; |
|
case topLevelTypes.topKeyPress: |
|
/** |
|
* 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 (nativeEvent.which && !isKeypressCommand(nativeEvent)) { |
|
return String.fromCharCode(nativeEvent.which); |
|
} |
|
return null; |
|
case topLevelTypes.topCompositionEnd: |
|
return useFallbackCompositionData ? null : nativeEvent.data; |
|
default: |
|
return null; |
|
} |
|
} |
|
|
|
/** |
|
* Extract a SyntheticInputEvent for `beforeInput`, based on either native |
|
* `textInput` or fallback behavior. |
|
* |
|
* @param {string} topLevelType Record from `EventConstants`. |
|
* @param {DOMEventTarget} topLevelTarget The listening component root node. |
|
* @param {string} topLevelTargetID ID of `topLevelTarget`. |
|
* @param {object} nativeEvent Native browser event. |
|
* @return {?object} A SyntheticInputEvent. |
|
*/ |
|
function extractBeforeInputEvent( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID, |
|
nativeEvent |
|
) { |
|
var 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; |
|
} |
|
|
|
var event = SyntheticInputEvent.getPooled( |
|
eventTypes.beforeInput, |
|
topLevelTargetID, |
|
nativeEvent |
|
); |
|
|
|
event.data = chars; |
|
EventPropagators.accumulateTwoPhaseDispatches(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. |
|
*/ |
|
var BeforeInputEventPlugin = { |
|
|
|
eventTypes: eventTypes, |
|
|
|
/** |
|
* @param {string} topLevelType Record from `EventConstants`. |
|
* @param {DOMEventTarget} topLevelTarget The listening component root node. |
|
* @param {string} topLevelTargetID ID of `topLevelTarget`. |
|
* @param {object} nativeEvent Native browser event. |
|
* @return {*} An accumulation of synthetic events. |
|
* @see {EventPluginHub.extractEvents} |
|
*/ |
|
extractEvents: function( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID, |
|
nativeEvent |
|
) { |
|
return [ |
|
extractCompositionEvent( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID, |
|
nativeEvent |
|
), |
|
extractBeforeInputEvent( |
|
topLevelType, |
|
topLevelTarget, |
|
topLevelTargetID, |
|
nativeEvent |
|
) |
|
]; |
|
} |
|
}; |
|
|
|
module.exports = BeforeInputEventPlugin;
|
|
|