diff --git a/src/browser/ReactEventEmitter.js b/src/browser/ReactEventEmitter.js index 11697d0caf..c09ed460de 100644 --- a/src/browser/ReactEventEmitter.js +++ b/src/browser/ReactEventEmitter.js @@ -115,6 +115,7 @@ var topEventMapping = { topPaste: 'paste', topScroll: 'scroll', topSelectionChange: 'selectionchange', + topTextInput: 'textInput', topTouchCancel: 'touchcancel', topTouchEnd: 'touchend', topTouchMove: 'touchmove', diff --git a/src/browser/eventPlugins/BeforeInputEventPlugin.js b/src/browser/eventPlugins/BeforeInputEventPlugin.js new file mode 100644 index 0000000000..3031305f94 --- /dev/null +++ b/src/browser/eventPlugins/BeforeInputEventPlugin.js @@ -0,0 +1,171 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule BeforeInputEventPlugin + * @typechecks static-only + */ + +"use strict"; + +var EventConstants = require('EventConstants'); +var EventPropagators = require('EventPropagators'); +var ExecutionEnvironment = require('ExecutionEnvironment'); +var SyntheticInputEvent = require('SyntheticInputEvent'); + +var keyOf = require('keyOf'); + +var useBeforeInputEvent = ( + ExecutionEnvironment.canUseDOM && + 'TextEvent' in window && + !('documentMode' in document) +); + +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 + ] + } +}; + +// Track characters inserted via keypress and composition events. +var fallbackChars = null; + +/** + * 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) + ); +} + +/** + * 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. + */ +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) { + + var chars; + + if (useBeforeInputEvent) { + // For browsers that support `textInput` events natively, don't do + // anything with keypress, composition, etc. + if (topLevelType !== topLevelTypes.topTextInput) { + return; + } + chars = nativeEvent.data; + } else { + 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. + fallbackChars = null; + break; + 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)) { + fallbackChars = String.fromCharCode(nativeEvent.which); + } + break; + case topLevelTypes.topCompositionEnd: + fallbackChars = nativeEvent.data; + break; + } + + // If no changes have occurred to the fallback string, no relevant + // event has fired and we're done. + if (fallbackChars === null) { + return; + } + + chars = fallbackChars; + } + + // If no characters are being inserted, no BeforeInput event should + // be fired. + if (!chars) { + return; + } + + var event = SyntheticInputEvent.getPooled( + eventTypes.beforeInput, + topLevelTargetID, + nativeEvent + ); + + event.data = chars; + fallbackChars = null; + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; + } +}; + +module.exports = BeforeInputEventPlugin; diff --git a/src/browser/eventPlugins/CompositionEventPlugin.js b/src/browser/eventPlugins/CompositionEventPlugin.js index fc3d5d89dc..babbdf95f2 100644 --- a/src/browser/eventPlugins/CompositionEventPlugin.js +++ b/src/browser/eventPlugins/CompositionEventPlugin.js @@ -44,7 +44,11 @@ var useCompositionEvent = ( // events as triggers. var useFallbackData = ( !useCompositionEvent || - 'documentMode' in document && document.documentMode > 8 + ( + 'documentMode' in document && + document.documentMode > 8 && + document.documentMode <= 11 + ) ); var topLevelTypes = EventConstants.topLevelTypes; diff --git a/src/browser/eventPlugins/DefaultEventPluginOrder.js b/src/browser/eventPlugins/DefaultEventPluginOrder.js index 8e059248a9..7d300ee907 100644 --- a/src/browser/eventPlugins/DefaultEventPluginOrder.js +++ b/src/browser/eventPlugins/DefaultEventPluginOrder.js @@ -37,6 +37,7 @@ var DefaultEventPluginOrder = [ keyOf({ChangeEventPlugin: null}), keyOf({SelectEventPlugin: null}), keyOf({CompositionEventPlugin: null}), + keyOf({BeforeInputEventPlugin: null}), keyOf({AnalyticsEventPlugin: null}), keyOf({MobileSafariClickEventPlugin: null}) ]; diff --git a/src/browser/syntheticEvents/SyntheticInputEvent.js b/src/browser/syntheticEvents/SyntheticInputEvent.js new file mode 100644 index 0000000000..8ba33ed4fc --- /dev/null +++ b/src/browser/syntheticEvents/SyntheticInputEvent.js @@ -0,0 +1,52 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule SyntheticInputEvent + * @typechecks static-only + */ + +"use strict"; + +var SyntheticEvent = require('SyntheticEvent'); + +/** + * @interface Event + * @see http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105 + * /#events-inputevents + */ +var InputEventInterface = { + data: null +}; + +/** + * @param {object} dispatchConfig Configuration used to dispatch this event. + * @param {string} dispatchMarker Marker identifying the event target. + * @param {object} nativeEvent Native browser event. + * @extends {SyntheticUIEvent} + */ +function SyntheticInputEvent( + dispatchConfig, + dispatchMarker, + nativeEvent) { + SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent); +} + +SyntheticEvent.augmentClass( + SyntheticInputEvent, + InputEventInterface +); + +module.exports = SyntheticInputEvent; + diff --git a/src/browser/ui/ReactDefaultInjection.js b/src/browser/ui/ReactDefaultInjection.js index b4b4080f3d..c29040388b 100644 --- a/src/browser/ui/ReactDefaultInjection.js +++ b/src/browser/ui/ReactDefaultInjection.js @@ -47,6 +47,7 @@ var ReactMount = require('ReactMount'); var SelectEventPlugin = require('SelectEventPlugin'); var ServerReactRootIndex = require('ServerReactRootIndex'); var SimpleEventPlugin = require('SimpleEventPlugin'); +var BeforeInputEventPlugin = require('BeforeInputEventPlugin'); var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); @@ -74,7 +75,8 @@ function inject() { ChangeEventPlugin: ChangeEventPlugin, CompositionEventPlugin: CompositionEventPlugin, MobileSafariClickEventPlugin: MobileSafariClickEventPlugin, - SelectEventPlugin: SelectEventPlugin + SelectEventPlugin: SelectEventPlugin, + BeforeInputEventPlugin: BeforeInputEventPlugin }); ReactInjection.DOM.injectComponentClasses({ diff --git a/src/event/EventConstants.js b/src/event/EventConstants.js index 6c8b45c373..ee8508340f 100644 --- a/src/event/EventConstants.js +++ b/src/event/EventConstants.js @@ -61,6 +61,7 @@ var topLevelTypes = keyMirror({ topScroll: null, topSelectionChange: null, topSubmit: null, + topTextInput: null, topTouchCancel: null, topTouchEnd: null, topTouchMove: null,