/**1* Copyright 2013-2015 Facebook, Inc.2* All rights reserved.3*4* This source code is licensed under the BSD-style license found in the5* LICENSE file in the root directory of this source tree. An additional grant6* of patent rights can be found in the PATENTS file in the same directory.7*8* @providesModule BeforeInputEventPlugin9* @typechecks static-only10*/1112'use strict';1314var EventConstants = require("./EventConstants");15var EventPropagators = require("./EventPropagators");16var ExecutionEnvironment = require("./ExecutionEnvironment");17var FallbackCompositionState = require("./FallbackCompositionState");18var SyntheticCompositionEvent = require("./SyntheticCompositionEvent");19var SyntheticInputEvent = require("./SyntheticInputEvent");2021var keyOf = require("./keyOf");2223var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space24var START_KEYCODE = 229;2526var canUseCompositionEvent = (27ExecutionEnvironment.canUseDOM &&28'CompositionEvent' in window29);3031var documentMode = null;32if (ExecutionEnvironment.canUseDOM && 'documentMode' in document) {33documentMode = document.documentMode;34}3536// Webkit offers a very useful `textInput` event that can be used to37// directly represent `beforeInput`. The IE `textinput` event is not as38// useful, so we don't use it.39var canUseTextInputEvent = (40ExecutionEnvironment.canUseDOM &&41'TextEvent' in window &&42!documentMode &&43!isPresto()44);4546// In IE9+, we have access to composition events, but the data supplied47// by the native compositionend event may be incorrect. Japanese ideographic48// spaces, for instance (\u3000) are not recorded correctly.49var useFallbackCompositionData = (50ExecutionEnvironment.canUseDOM &&51(52(!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11)53)54);5556/**57* Opera <= 12 includes TextEvent in window, but does not fire58* text input events. Rely on keypress instead.59*/60function isPresto() {61var opera = window.opera;62return (63typeof opera === 'object' &&64typeof opera.version === 'function' &&65parseInt(opera.version(), 10) <= 1266);67}6869var SPACEBAR_CODE = 32;70var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);7172var topLevelTypes = EventConstants.topLevelTypes;7374// Events and their corresponding property names.75var eventTypes = {76beforeInput: {77phasedRegistrationNames: {78bubbled: keyOf({onBeforeInput: null}),79captured: keyOf({onBeforeInputCapture: null})80},81dependencies: [82topLevelTypes.topCompositionEnd,83topLevelTypes.topKeyPress,84topLevelTypes.topTextInput,85topLevelTypes.topPaste86]87},88compositionEnd: {89phasedRegistrationNames: {90bubbled: keyOf({onCompositionEnd: null}),91captured: keyOf({onCompositionEndCapture: null})92},93dependencies: [94topLevelTypes.topBlur,95topLevelTypes.topCompositionEnd,96topLevelTypes.topKeyDown,97topLevelTypes.topKeyPress,98topLevelTypes.topKeyUp,99topLevelTypes.topMouseDown100]101},102compositionStart: {103phasedRegistrationNames: {104bubbled: keyOf({onCompositionStart: null}),105captured: keyOf({onCompositionStartCapture: null})106},107dependencies: [108topLevelTypes.topBlur,109topLevelTypes.topCompositionStart,110topLevelTypes.topKeyDown,111topLevelTypes.topKeyPress,112topLevelTypes.topKeyUp,113topLevelTypes.topMouseDown114]115},116compositionUpdate: {117phasedRegistrationNames: {118bubbled: keyOf({onCompositionUpdate: null}),119captured: keyOf({onCompositionUpdateCapture: null})120},121dependencies: [122topLevelTypes.topBlur,123topLevelTypes.topCompositionUpdate,124topLevelTypes.topKeyDown,125topLevelTypes.topKeyPress,126topLevelTypes.topKeyUp,127topLevelTypes.topMouseDown128]129}130};131132// Track whether we've ever handled a keypress on the space key.133var hasSpaceKeypress = false;134135/**136* Return whether a native keypress event is assumed to be a command.137* This is required because Firefox fires `keypress` events for key commands138* (cut, copy, select-all, etc.) even though no character is inserted.139*/140function isKeypressCommand(nativeEvent) {141return (142(nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&143// ctrlKey && altKey is equivalent to AltGr, and is not a command.144!(nativeEvent.ctrlKey && nativeEvent.altKey)145);146}147148149/**150* Translate native top level events into event types.151*152* @param {string} topLevelType153* @return {object}154*/155function getCompositionEventType(topLevelType) {156switch (topLevelType) {157case topLevelTypes.topCompositionStart:158return eventTypes.compositionStart;159case topLevelTypes.topCompositionEnd:160return eventTypes.compositionEnd;161case topLevelTypes.topCompositionUpdate:162return eventTypes.compositionUpdate;163}164}165166/**167* Does our fallback best-guess model think this event signifies that168* composition has begun?169*170* @param {string} topLevelType171* @param {object} nativeEvent172* @return {boolean}173*/174function isFallbackCompositionStart(topLevelType, nativeEvent) {175return (176topLevelType === topLevelTypes.topKeyDown &&177nativeEvent.keyCode === START_KEYCODE178);179}180181/**182* Does our fallback mode think that this event is the end of composition?183*184* @param {string} topLevelType185* @param {object} nativeEvent186* @return {boolean}187*/188function isFallbackCompositionEnd(topLevelType, nativeEvent) {189switch (topLevelType) {190case topLevelTypes.topKeyUp:191// Command keys insert or clear IME input.192return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1);193case topLevelTypes.topKeyDown:194// Expect IME keyCode on each keydown. If we get any other195// code we must have exited earlier.196return (nativeEvent.keyCode !== START_KEYCODE);197case topLevelTypes.topKeyPress:198case topLevelTypes.topMouseDown:199case topLevelTypes.topBlur:200// Events are not possible without cancelling IME.201return true;202default:203return false;204}205}206207/**208* Google Input Tools provides composition data via a CustomEvent,209* with the `data` property populated in the `detail` object. If this210* is available on the event object, use it. If not, this is a plain211* composition event and we have nothing special to extract.212*213* @param {object} nativeEvent214* @return {?string}215*/216function getDataFromCustomEvent(nativeEvent) {217var detail = nativeEvent.detail;218if (typeof detail === 'object' && 'data' in detail) {219return detail.data;220}221return null;222}223224// Track the current IME composition fallback object, if any.225var currentComposition = null;226227/**228* @param {string} topLevelType Record from `EventConstants`.229* @param {DOMEventTarget} topLevelTarget The listening component root node.230* @param {string} topLevelTargetID ID of `topLevelTarget`.231* @param {object} nativeEvent Native browser event.232* @return {?object} A SyntheticCompositionEvent.233*/234function extractCompositionEvent(235topLevelType,236topLevelTarget,237topLevelTargetID,238nativeEvent239) {240var eventType;241var fallbackData;242243if (canUseCompositionEvent) {244eventType = getCompositionEventType(topLevelType);245} else if (!currentComposition) {246if (isFallbackCompositionStart(topLevelType, nativeEvent)) {247eventType = eventTypes.compositionStart;248}249} else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {250eventType = eventTypes.compositionEnd;251}252253if (!eventType) {254return null;255}256257if (useFallbackCompositionData) {258// The current composition is stored statically and must not be259// overwritten while composition continues.260if (!currentComposition && eventType === eventTypes.compositionStart) {261currentComposition = FallbackCompositionState.getPooled(topLevelTarget);262} else if (eventType === eventTypes.compositionEnd) {263if (currentComposition) {264fallbackData = currentComposition.getData();265}266}267}268269var event = SyntheticCompositionEvent.getPooled(270eventType,271topLevelTargetID,272nativeEvent273);274275if (fallbackData) {276// Inject data generated from fallback path into the synthetic event.277// This matches the property of native CompositionEventInterface.278event.data = fallbackData;279} else {280var customData = getDataFromCustomEvent(nativeEvent);281if (customData !== null) {282event.data = customData;283}284}285286EventPropagators.accumulateTwoPhaseDispatches(event);287return event;288}289290/**291* @param {string} topLevelType Record from `EventConstants`.292* @param {object} nativeEvent Native browser event.293* @return {?string} The string corresponding to this `beforeInput` event.294*/295function getNativeBeforeInputChars(topLevelType, nativeEvent) {296switch (topLevelType) {297case topLevelTypes.topCompositionEnd:298return getDataFromCustomEvent(nativeEvent);299case topLevelTypes.topKeyPress:300/**301* If native `textInput` events are available, our goal is to make302* use of them. However, there is a special case: the spacebar key.303* In Webkit, preventing default on a spacebar `textInput` event304* cancels character insertion, but it *also* causes the browser305* to fall back to its default spacebar behavior of scrolling the306* page.307*308* Tracking at:309* https://code.google.com/p/chromium/issues/detail?id=355103310*311* To avoid this issue, use the keypress event as if no `textInput`312* event is available.313*/314var which = nativeEvent.which;315if (which !== SPACEBAR_CODE) {316return null;317}318319hasSpaceKeypress = true;320return SPACEBAR_CHAR;321322case topLevelTypes.topTextInput:323// Record the characters to be added to the DOM.324var chars = nativeEvent.data;325326// If it's a spacebar character, assume that we have already handled327// it at the keypress level and bail immediately. Android Chrome328// doesn't give us keycodes, so we need to blacklist it.329if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {330return null;331}332333return chars;334335default:336// For other native event types, do nothing.337return null;338}339}340341/**342* For browsers that do not provide the `textInput` event, extract the343* appropriate string to use for SyntheticInputEvent.344*345* @param {string} topLevelType Record from `EventConstants`.346* @param {object} nativeEvent Native browser event.347* @return {?string} The fallback string for this `beforeInput` event.348*/349function getFallbackBeforeInputChars(topLevelType, nativeEvent) {350// If we are currently composing (IME) and using a fallback to do so,351// try to extract the composed characters from the fallback object.352if (currentComposition) {353if (354topLevelType === topLevelTypes.topCompositionEnd ||355isFallbackCompositionEnd(topLevelType, nativeEvent)356) {357var chars = currentComposition.getData();358FallbackCompositionState.release(currentComposition);359currentComposition = null;360return chars;361}362return null;363}364365switch (topLevelType) {366case topLevelTypes.topPaste:367// If a paste event occurs after a keypress, throw out the input368// chars. Paste events should not lead to BeforeInput events.369return null;370case topLevelTypes.topKeyPress:371/**372* As of v27, Firefox may fire keypress events even when no character373* will be inserted. A few possibilities:374*375* - `which` is `0`. Arrow keys, Esc key, etc.376*377* - `which` is the pressed key code, but no char is available.378* Ex: 'AltGr + d` in Polish. There is no modified character for379* this key combination and no character is inserted into the380* document, but FF fires the keypress for char code `100` anyway.381* No `input` event will occur.382*383* - `which` is the pressed key code, but a command combination is384* being used. Ex: `Cmd+C`. No character is inserted, and no385* `input` event will occur.386*/387if (nativeEvent.which && !isKeypressCommand(nativeEvent)) {388return String.fromCharCode(nativeEvent.which);389}390return null;391case topLevelTypes.topCompositionEnd:392return useFallbackCompositionData ? null : nativeEvent.data;393default:394return null;395}396}397398/**399* Extract a SyntheticInputEvent for `beforeInput`, based on either native400* `textInput` or fallback behavior.401*402* @param {string} topLevelType Record from `EventConstants`.403* @param {DOMEventTarget} topLevelTarget The listening component root node.404* @param {string} topLevelTargetID ID of `topLevelTarget`.405* @param {object} nativeEvent Native browser event.406* @return {?object} A SyntheticInputEvent.407*/408function extractBeforeInputEvent(409topLevelType,410topLevelTarget,411topLevelTargetID,412nativeEvent413) {414var chars;415416if (canUseTextInputEvent) {417chars = getNativeBeforeInputChars(topLevelType, nativeEvent);418} else {419chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);420}421422// If no characters are being inserted, no BeforeInput event should423// be fired.424if (!chars) {425return null;426}427428var event = SyntheticInputEvent.getPooled(429eventTypes.beforeInput,430topLevelTargetID,431nativeEvent432);433434event.data = chars;435EventPropagators.accumulateTwoPhaseDispatches(event);436return event;437}438439/**440* Create an `onBeforeInput` event to match441* http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.442*443* This event plugin is based on the native `textInput` event444* available in Chrome, Safari, Opera, and IE. This event fires after445* `onKeyPress` and `onCompositionEnd`, but before `onInput`.446*447* `beforeInput` is spec'd but not implemented in any browsers, and448* the `input` event does not provide any useful information about what has449* actually been added, contrary to the spec. Thus, `textInput` is the best450* available event to identify the characters that have actually been inserted451* into the target node.452*453* This plugin is also responsible for emitting `composition` events, thus454* allowing us to share composition fallback code for both `beforeInput` and455* `composition` event types.456*/457var BeforeInputEventPlugin = {458459eventTypes: eventTypes,460461/**462* @param {string} topLevelType Record from `EventConstants`.463* @param {DOMEventTarget} topLevelTarget The listening component root node.464* @param {string} topLevelTargetID ID of `topLevelTarget`.465* @param {object} nativeEvent Native browser event.466* @return {*} An accumulation of synthetic events.467* @see {EventPluginHub.extractEvents}468*/469extractEvents: function(470topLevelType,471topLevelTarget,472topLevelTargetID,473nativeEvent474) {475return [476extractCompositionEvent(477topLevelType,478topLevelTarget,479topLevelTargetID,480nativeEvent481),482extractBeforeInputEvent(483topLevelType,484topLevelTarget,485topLevelTargetID,486nativeEvent487)488];489}490};491492module.exports = BeforeInputEventPlugin;493494495