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.
494 lines
15 KiB
494 lines
15 KiB
10 years ago
|
/**
|
||
|
* 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;
|