383 lines
13 KiB
JavaScript
383 lines
13 KiB
JavaScript
|
/**
|
||
|
* Copyright 2013-present 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.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var EventPropagators = require('./EventPropagators');
|
||
|
var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
|
||
|
var FallbackCompositionState = require('./FallbackCompositionState');
|
||
|
var SyntheticCompositionEvent = require('./SyntheticCompositionEvent');
|
||
|
var SyntheticInputEvent = require('./SyntheticInputEvent');
|
||
|
|
||
|
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);
|
||
|
|
||
|
// Events and their corresponding property names.
|
||
|
var eventTypes = {
|
||
|
beforeInput: {
|
||
|
phasedRegistrationNames: {
|
||
|
bubbled: 'onBeforeInput',
|
||
|
captured: 'onBeforeInputCapture'
|
||
|
},
|
||
|
dependencies: ['topCompositionEnd', 'topKeyPress', 'topTextInput', 'topPaste']
|
||
|
},
|
||
|
compositionEnd: {
|
||
|
phasedRegistrationNames: {
|
||
|
bubbled: 'onCompositionEnd',
|
||
|
captured: 'onCompositionEndCapture'
|
||
|
},
|
||
|
dependencies: ['topBlur', 'topCompositionEnd', 'topKeyDown', 'topKeyPress', 'topKeyUp', 'topMouseDown']
|
||
|
},
|
||
|
compositionStart: {
|
||
|
phasedRegistrationNames: {
|
||
|
bubbled: 'onCompositionStart',
|
||
|
captured: 'onCompositionStartCapture'
|
||
|
},
|
||
|
dependencies: ['topBlur', 'topCompositionStart', 'topKeyDown', 'topKeyPress', 'topKeyUp', 'topMouseDown']
|
||
|
},
|
||
|
compositionUpdate: {
|
||
|
phasedRegistrationNames: {
|
||
|
bubbled: 'onCompositionUpdate',
|
||
|
captured: 'onCompositionUpdateCapture'
|
||
|
},
|
||
|
dependencies: ['topBlur', 'topCompositionUpdate', 'topKeyDown', 'topKeyPress', 'topKeyUp', '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 'topCompositionStart':
|
||
|
return eventTypes.compositionStart;
|
||
|
case 'topCompositionEnd':
|
||
|
return eventTypes.compositionEnd;
|
||
|
case '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 === '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 'topKeyUp':
|
||
|
// Command keys insert or clear IME input.
|
||
|
return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
|
||
|
case 'topKeyDown':
|
||
|
// Expect IME keyCode on each keydown. If we get any other
|
||
|
// code we must have exited earlier.
|
||
|
return nativeEvent.keyCode !== START_KEYCODE;
|
||
|
case 'topKeyPress':
|
||
|
case 'topMouseDown':
|
||
|
case '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;
|
||
|
|
||
|
/**
|
||
|
* @return {?object} A SyntheticCompositionEvent.
|
||
|
*/
|
||
|
function extractCompositionEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
|
||
|
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(nativeEventTarget);
|
||
|
} else if (eventType === eventTypes.compositionEnd) {
|
||
|
if (currentComposition) {
|
||
|
fallbackData = currentComposition.getData();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var 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 {
|
||
|
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 'topCompositionEnd':
|
||
|
return getDataFromCustomEvent(nativeEvent);
|
||
|
case '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 '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 composition event is available, we extract a string only at
|
||
|
// compositionevent, otherwise extract it at fallback events.
|
||
|
if (currentComposition) {
|
||
|
if (topLevelType === 'topCompositionEnd' || !canUseCompositionEvent && isFallbackCompositionEnd(topLevelType, nativeEvent)) {
|
||
|
var chars = currentComposition.getData();
|
||
|
FallbackCompositionState.release(currentComposition);
|
||
|
currentComposition = null;
|
||
|
return chars;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
switch (topLevelType) {
|
||
|
case '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 '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 'topCompositionEnd':
|
||
|
return useFallbackCompositionData ? 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) {
|
||
|
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, targetInst, nativeEvent, nativeEventTarget);
|
||
|
|
||
|
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,
|
||
|
|
||
|
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
|
||
|
return [extractCompositionEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget), extractBeforeInputEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget)];
|
||
|
}
|
||
|
};
|
||
|
|
||
|
module.exports = BeforeInputEventPlugin;
|