mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #2690 from salier/before-input
Combine BeforeInput and Composition event plugins
This commit is contained in:
@@ -15,14 +15,44 @@
|
||||
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' in document || isPresto())
|
||||
!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)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -56,11 +86,53 @@ var eventTypes = {
|
||||
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 characters inserted via keypress and composition events.
|
||||
var fallbackChars = null;
|
||||
var fallbackBeforeInputChars = null;
|
||||
|
||||
// Track whether we've ever handled a keypress on the space key.
|
||||
var hasSpaceKeypress = false;
|
||||
@@ -78,6 +150,298 @@ function isKeypressCommand(nativeEvent) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var event = SyntheticInputEvent.getPooled(
|
||||
eventTypes.beforeInput,
|
||||
topLevelTargetID,
|
||||
nativeEvent
|
||||
);
|
||||
|
||||
event.data = chars;
|
||||
fallbackBeforeInputChars = null;
|
||||
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.
|
||||
@@ -91,6 +455,10 @@ function isKeypressCommand(nativeEvent) {
|
||||
* 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 = {
|
||||
|
||||
@@ -105,115 +473,25 @@ var BeforeInputEventPlugin = {
|
||||
* @see {EventPluginHub.extractEvents}
|
||||
*/
|
||||
extractEvents: function(
|
||||
topLevelType,
|
||||
topLevelTarget,
|
||||
topLevelTargetID,
|
||||
nativeEvent) {
|
||||
|
||||
var chars;
|
||||
|
||||
if (canUseTextInputEvent) {
|
||||
switch (topLevelType) {
|
||||
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;
|
||||
}
|
||||
|
||||
hasSpaceKeypress = true;
|
||||
chars = SPACEBAR_CHAR;
|
||||
break;
|
||||
|
||||
case topLevelTypes.topTextInput:
|
||||
// Record the characters to be added to the DOM.
|
||||
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;
|
||||
}
|
||||
|
||||
// Otherwise, carry on.
|
||||
break;
|
||||
|
||||
default:
|
||||
// For other native event types, do nothing.
|
||||
return;
|
||||
}
|
||||
} 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;
|
||||
topLevelType,
|
||||
topLevelTarget,
|
||||
topLevelTargetID,
|
||||
nativeEvent
|
||||
) {
|
||||
return [
|
||||
extractCompositionEvent(
|
||||
topLevelType,
|
||||
topLevelTarget,
|
||||
topLevelTargetID,
|
||||
nativeEvent
|
||||
),
|
||||
extractBeforeInputEvent(
|
||||
topLevelType,
|
||||
topLevelTarget,
|
||||
topLevelTargetID,
|
||||
nativeEvent
|
||||
),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
/**
|
||||
* Copyright 2013-2014, 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 CompositionEventPlugin
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var EventConstants = require('EventConstants');
|
||||
var EventPropagators = require('EventPropagators');
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
var ReactInputSelection = require('ReactInputSelection');
|
||||
var SyntheticCompositionEvent = require('SyntheticCompositionEvent');
|
||||
|
||||
var getTextContentAccessor = require('getTextContentAccessor');
|
||||
var keyOf = require('keyOf');
|
||||
|
||||
var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
|
||||
var START_KEYCODE = 229;
|
||||
|
||||
var useCompositionEvent = (
|
||||
ExecutionEnvironment.canUseDOM &&
|
||||
'CompositionEvent' in window
|
||||
);
|
||||
|
||||
// In IE9+, we have access to composition events, but the data supplied
|
||||
// by the native compositionend event may be incorrect. In Korean, for example,
|
||||
// the compositionend event contains only one character regardless of
|
||||
// how many characters have been composed since compositionstart.
|
||||
// We therefore use the fallback data while still using the native
|
||||
// events as triggers.
|
||||
var useFallbackData = (
|
||||
!useCompositionEvent ||
|
||||
(
|
||||
'documentMode' in document &&
|
||||
document.documentMode > 8 &&
|
||||
document.documentMode <= 11
|
||||
)
|
||||
);
|
||||
|
||||
var topLevelTypes = EventConstants.topLevelTypes;
|
||||
var currentComposition = null;
|
||||
|
||||
// Events and their corresponding property names.
|
||||
var eventTypes = {
|
||||
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
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 isFallbackStart(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 isFallbackEnd(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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class stores information about selection and document state
|
||||
* so we can figure out what changed at a later date.
|
||||
*
|
||||
* @param {DOMEventTarget} root
|
||||
*/
|
||||
function FallbackCompositionState(root) {
|
||||
this.root = root;
|
||||
this.startSelection = ReactInputSelection.getSelection(root);
|
||||
this.startValue = this.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current text of input.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
FallbackCompositionState.prototype.getText = function() {
|
||||
return this.root.value || this.root[getTextContentAccessor()];
|
||||
};
|
||||
|
||||
/**
|
||||
* Text that has changed since the start of composition.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
FallbackCompositionState.prototype.getData = function() {
|
||||
var endValue = this.getText();
|
||||
var prefixLength = this.startSelection.start;
|
||||
var suffixLength = this.startValue.length - this.startSelection.end;
|
||||
|
||||
return endValue.substr(
|
||||
prefixLength,
|
||||
endValue.length - suffixLength - prefixLength
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This plugin creates `onCompositionStart`, `onCompositionUpdate` and
|
||||
* `onCompositionEnd` events on inputs, textareas and contentEditable
|
||||
* nodes.
|
||||
*/
|
||||
var CompositionEventPlugin = {
|
||||
|
||||
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 eventType;
|
||||
var data;
|
||||
|
||||
if (useCompositionEvent) {
|
||||
eventType = getCompositionEventType(topLevelType);
|
||||
} else if (!currentComposition) {
|
||||
if (isFallbackStart(topLevelType, nativeEvent)) {
|
||||
eventType = eventTypes.compositionStart;
|
||||
}
|
||||
} else if (isFallbackEnd(topLevelType, nativeEvent)) {
|
||||
eventType = eventTypes.compositionEnd;
|
||||
}
|
||||
|
||||
if (useFallbackData) {
|
||||
// The current composition is stored statically and must not be
|
||||
// overwritten while composition continues.
|
||||
if (!currentComposition && eventType === eventTypes.compositionStart) {
|
||||
currentComposition = new FallbackCompositionState(topLevelTarget);
|
||||
} else if (eventType === eventTypes.compositionEnd) {
|
||||
if (currentComposition) {
|
||||
data = currentComposition.getData();
|
||||
currentComposition = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (eventType) {
|
||||
var event = SyntheticCompositionEvent.getPooled(
|
||||
eventType,
|
||||
topLevelTargetID,
|
||||
nativeEvent
|
||||
);
|
||||
if (data) {
|
||||
// Inject data generated from fallback path into the synthetic event.
|
||||
// This matches the property of native CompositionEventInterface.
|
||||
event.data = data;
|
||||
}
|
||||
EventPropagators.accumulateTwoPhaseDispatches(event);
|
||||
return event;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CompositionEventPlugin;
|
||||
@@ -29,7 +29,6 @@ var DefaultEventPluginOrder = [
|
||||
keyOf({EnterLeaveEventPlugin: null}),
|
||||
keyOf({ChangeEventPlugin: null}),
|
||||
keyOf({SelectEventPlugin: null}),
|
||||
keyOf({CompositionEventPlugin: null}),
|
||||
keyOf({BeforeInputEventPlugin: null}),
|
||||
keyOf({AnalyticsEventPlugin: null}),
|
||||
keyOf({MobileSafariClickEventPlugin: null})
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright 2013 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 FallbackCompositionState
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var PooledClass = require('PooledClass');
|
||||
|
||||
var assign = require('Object.assign');
|
||||
var getTextContentAccessor = require('getTextContentAccessor');
|
||||
|
||||
/**
|
||||
* This helper class stores information about text content of a target node,
|
||||
* allowing comparison of content before and after a given event.
|
||||
*
|
||||
* Identify the node where selection currently begins, then observe
|
||||
* both its text content and its current position in the DOM. Since the
|
||||
* browser may natively replace the target node during composition, we can
|
||||
* use its position to find its replacement.
|
||||
*
|
||||
* @param {DOMEventTarget} root
|
||||
*/
|
||||
function FallbackCompositionState(root) {
|
||||
this._root = root;
|
||||
this._startText = this.getText();
|
||||
this._fallbackText = null;
|
||||
}
|
||||
|
||||
assign(FallbackCompositionState.prototype, {
|
||||
/**
|
||||
* Get current text of input.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
getText: function() {
|
||||
if ('value' in this._root) {
|
||||
return this._root.value;
|
||||
}
|
||||
return this._root[getTextContentAccessor()];
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine the differing substring between the initially stored
|
||||
* text content and the current content.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
getData: function() {
|
||||
if (this._fallbackText) {
|
||||
return this._fallbackText;
|
||||
}
|
||||
|
||||
var endValue = this.getText();
|
||||
var startValue = this._startText;
|
||||
var startLength = startValue.length;
|
||||
var endLength = endValue.length;
|
||||
|
||||
for (var start = 0; start < startLength; start++) {
|
||||
if (startValue[start] !== endValue[start]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var minEnd = startLength - start;
|
||||
for (var end = 1; end <= minEnd; end++) {
|
||||
if (startValue[startLength - end] !== endValue[endLength - end]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var sliceTail = end > 1 ? 1 - end : undefined;
|
||||
this._fallbackText = endValue.slice(start, sliceTail);
|
||||
return this._fallbackText;
|
||||
}
|
||||
});
|
||||
|
||||
PooledClass.addPoolingTo(FallbackCompositionState);
|
||||
|
||||
module.exports = FallbackCompositionState;
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright 2013-2014, 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
describe('FallbackCompositionState', function() {
|
||||
var FallbackCompositionState;
|
||||
|
||||
var TEXT = 'Hello world';
|
||||
|
||||
beforeEach(function() {
|
||||
FallbackCompositionState = require('FallbackCompositionState');
|
||||
});
|
||||
|
||||
function getInput() {
|
||||
var input = document.createElement('input');
|
||||
input.value = TEXT;
|
||||
return input;
|
||||
}
|
||||
|
||||
function getTextarea() {
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.value = TEXT;
|
||||
return textarea;
|
||||
}
|
||||
|
||||
function getContentEditable() {
|
||||
var editable = document.createElement('div');
|
||||
var inner = document.createElement('span');
|
||||
inner.appendChild(document.createTextNode(TEXT));
|
||||
editable.appendChild(inner);
|
||||
return editable;
|
||||
}
|
||||
|
||||
function assertExtractedData(modifiedValue, expectedData) {
|
||||
var input = getInput();
|
||||
var composition = FallbackCompositionState.getPooled(input);
|
||||
input.value = modifiedValue;
|
||||
expect(composition.getData()).toBe(expectedData);
|
||||
FallbackCompositionState.release(composition);
|
||||
}
|
||||
|
||||
it('extracts value via `getText()`', function() {
|
||||
var composition = FallbackCompositionState.getPooled(getInput());
|
||||
expect(composition.getText()).toBe(TEXT);
|
||||
FallbackCompositionState.release(composition);
|
||||
|
||||
composition = FallbackCompositionState.getPooled(getTextarea());
|
||||
expect(composition.getText()).toBe(TEXT);
|
||||
FallbackCompositionState.release(composition);
|
||||
|
||||
composition = FallbackCompositionState.getPooled(getContentEditable());
|
||||
expect(composition.getText()).toBe(TEXT);
|
||||
FallbackCompositionState.release(composition);
|
||||
});
|
||||
|
||||
describe('Extract fallback data inserted at collapsed cursor', function() {
|
||||
it('extracts when inserted at start of text', function() {
|
||||
assertExtractedData('XXXHello world', 'XXX');
|
||||
});
|
||||
|
||||
it('extracts when inserted within text', function() {
|
||||
assertExtractedData('Hello XXXworld', 'XXX');
|
||||
});
|
||||
|
||||
it('extracts when inserted at end of text', function() {
|
||||
assertExtractedData('Hello worldXXX', 'XXX');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Extract fallback data for non-collapsed range', function() {
|
||||
it('extracts when inserted at start of text', function() {
|
||||
assertExtractedData('XXX world', 'XXX');
|
||||
});
|
||||
|
||||
it('extracts when inserted within text', function() {
|
||||
assertExtractedData('HelXXXrld', 'XXX');
|
||||
});
|
||||
|
||||
it('extracts when inserted at end of text', function() {
|
||||
assertExtractedData('Hello XXX', 'XXX');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,6 @@
|
||||
var BeforeInputEventPlugin = require('BeforeInputEventPlugin');
|
||||
var ChangeEventPlugin = require('ChangeEventPlugin');
|
||||
var ClientReactRootIndex = require('ClientReactRootIndex');
|
||||
var CompositionEventPlugin = require('CompositionEventPlugin');
|
||||
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
|
||||
var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
@@ -66,7 +65,6 @@ function inject() {
|
||||
SimpleEventPlugin: SimpleEventPlugin,
|
||||
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
|
||||
ChangeEventPlugin: ChangeEventPlugin,
|
||||
CompositionEventPlugin: CompositionEventPlugin,
|
||||
MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,
|
||||
SelectEventPlugin: SelectEventPlugin,
|
||||
BeforeInputEventPlugin: BeforeInputEventPlugin
|
||||
|
||||
Reference in New Issue
Block a user