UNPKG

10.7 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2013-present, Facebook, Inc.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 */
8
9'use strict';
10
11var EventPluginHub = require('./EventPluginHub');
12var EventPropagators = require('./EventPropagators');
13var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
14var ReactDOMComponentTree = require('./ReactDOMComponentTree');
15var ReactUpdates = require('./ReactUpdates');
16var SyntheticEvent = require('./SyntheticEvent');
17
18var inputValueTracking = require('./inputValueTracking');
19var getEventTarget = require('./getEventTarget');
20var isEventSupported = require('./isEventSupported');
21var isTextInputElement = require('./isTextInputElement');
22
23var eventTypes = {
24 change: {
25 phasedRegistrationNames: {
26 bubbled: 'onChange',
27 captured: 'onChangeCapture'
28 },
29 dependencies: ['topBlur', 'topChange', 'topClick', 'topFocus', 'topInput', 'topKeyDown', 'topKeyUp', 'topSelectionChange']
30 }
31};
32
33function createAndAccumulateChangeEvent(inst, nativeEvent, target) {
34 var event = SyntheticEvent.getPooled(eventTypes.change, inst, nativeEvent, target);
35 event.type = 'change';
36 EventPropagators.accumulateTwoPhaseDispatches(event);
37 return event;
38}
39/**
40 * For IE shims
41 */
42var activeElement = null;
43var activeElementInst = null;
44
45/**
46 * SECTION: handle `change` event
47 */
48function shouldUseChangeEvent(elem) {
49 var nodeName = elem.nodeName && elem.nodeName.toLowerCase();
50 return nodeName === 'select' || nodeName === 'input' && elem.type === 'file';
51}
52
53var doesChangeEventBubble = false;
54if (ExecutionEnvironment.canUseDOM) {
55 // See `handleChange` comment below
56 doesChangeEventBubble = isEventSupported('change') && (!document.documentMode || document.documentMode > 8);
57}
58
59function manualDispatchChangeEvent(nativeEvent) {
60 var event = createAndAccumulateChangeEvent(activeElementInst, nativeEvent, getEventTarget(nativeEvent));
61
62 // If change and propertychange bubbled, we'd just bind to it like all the
63 // other events and have it go through ReactBrowserEventEmitter. Since it
64 // doesn't, we manually listen for the events and so we have to enqueue and
65 // process the abstract event manually.
66 //
67 // Batching is necessary here in order to ensure that all event handlers run
68 // before the next rerender (including event handlers attached to ancestor
69 // elements instead of directly on the input). Without this, controlled
70 // components don't work properly in conjunction with event bubbling because
71 // the component is rerendered and the value reverted before all the event
72 // handlers can run. See https://github.com/facebook/react/issues/708.
73 ReactUpdates.batchedUpdates(runEventInBatch, event);
74}
75
76function runEventInBatch(event) {
77 EventPluginHub.enqueueEvents(event);
78 EventPluginHub.processEventQueue(false);
79}
80
81function startWatchingForChangeEventIE8(target, targetInst) {
82 activeElement = target;
83 activeElementInst = targetInst;
84 activeElement.attachEvent('onchange', manualDispatchChangeEvent);
85}
86
87function stopWatchingForChangeEventIE8() {
88 if (!activeElement) {
89 return;
90 }
91 activeElement.detachEvent('onchange', manualDispatchChangeEvent);
92 activeElement = null;
93 activeElementInst = null;
94}
95
96function getInstIfValueChanged(targetInst, nativeEvent) {
97 var updated = inputValueTracking.updateValueIfChanged(targetInst);
98 var simulated = nativeEvent.simulated === true && ChangeEventPlugin._allowSimulatedPassThrough;
99
100 if (updated || simulated) {
101 return targetInst;
102 }
103}
104
105function getTargetInstForChangeEvent(topLevelType, targetInst) {
106 if (topLevelType === 'topChange') {
107 return targetInst;
108 }
109}
110
111function handleEventsForChangeEventIE8(topLevelType, target, targetInst) {
112 if (topLevelType === 'topFocus') {
113 // stopWatching() should be a noop here but we call it just in case we
114 // missed a blur event somehow.
115 stopWatchingForChangeEventIE8();
116 startWatchingForChangeEventIE8(target, targetInst);
117 } else if (topLevelType === 'topBlur') {
118 stopWatchingForChangeEventIE8();
119 }
120}
121
122/**
123 * SECTION: handle `input` event
124 */
125var isInputEventSupported = false;
126if (ExecutionEnvironment.canUseDOM) {
127 // IE9 claims to support the input event but fails to trigger it when
128 // deleting text, so we ignore its input events.
129
130 isInputEventSupported = isEventSupported('input') && (!document.documentMode || document.documentMode > 9);
131}
132
133/**
134 * (For IE <=9) Starts tracking propertychange events on the passed-in element
135 * and override the value property so that we can distinguish user events from
136 * value changes in JS.
137 */
138function startWatchingForValueChange(target, targetInst) {
139 activeElement = target;
140 activeElementInst = targetInst;
141 activeElement.attachEvent('onpropertychange', handlePropertyChange);
142}
143
144/**
145 * (For IE <=9) Removes the event listeners from the currently-tracked element,
146 * if any exists.
147 */
148function stopWatchingForValueChange() {
149 if (!activeElement) {
150 return;
151 }
152 activeElement.detachEvent('onpropertychange', handlePropertyChange);
153
154 activeElement = null;
155 activeElementInst = null;
156}
157
158/**
159 * (For IE <=9) Handles a propertychange event, sending a `change` event if
160 * the value of the active element has changed.
161 */
162function handlePropertyChange(nativeEvent) {
163 if (nativeEvent.propertyName !== 'value') {
164 return;
165 }
166 if (getInstIfValueChanged(activeElementInst, nativeEvent)) {
167 manualDispatchChangeEvent(nativeEvent);
168 }
169}
170
171function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) {
172 if (topLevelType === 'topFocus') {
173 // In IE8, we can capture almost all .value changes by adding a
174 // propertychange handler and looking for events with propertyName
175 // equal to 'value'
176 // In IE9, propertychange fires for most input events but is buggy and
177 // doesn't fire when text is deleted, but conveniently, selectionchange
178 // appears to fire in all of the remaining cases so we catch those and
179 // forward the event if the value has changed
180 // In either case, we don't want to call the event handler if the value
181 // is changed from JS so we redefine a setter for `.value` that updates
182 // our activeElementValue variable, allowing us to ignore those changes
183 //
184 // stopWatching() should be a noop here but we call it just in case we
185 // missed a blur event somehow.
186 stopWatchingForValueChange();
187 startWatchingForValueChange(target, targetInst);
188 } else if (topLevelType === 'topBlur') {
189 stopWatchingForValueChange();
190 }
191}
192
193// For IE8 and IE9.
194function getTargetInstForInputEventPolyfill(topLevelType, targetInst, nativeEvent) {
195 if (topLevelType === 'topSelectionChange' || topLevelType === 'topKeyUp' || topLevelType === 'topKeyDown') {
196 // On the selectionchange event, the target is just document which isn't
197 // helpful for us so just check activeElement instead.
198 //
199 // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
200 // propertychange on the first input event after setting `value` from a
201 // script and fires only keydown, keypress, keyup. Catching keyup usually
202 // gets it and catching keydown lets us fire an event for the first
203 // keystroke if user does a key repeat (it'll be a little delayed: right
204 // before the second keystroke). Other input methods (e.g., paste) seem to
205 // fire selectionchange normally.
206 return getInstIfValueChanged(activeElementInst, nativeEvent);
207 }
208}
209
210/**
211 * SECTION: handle `click` event
212 */
213function shouldUseClickEvent(elem) {
214 // Use the `click` event to detect changes to checkbox and radio inputs.
215 // This approach works across all browsers, whereas `change` does not fire
216 // until `blur` in IE8.
217 var nodeName = elem.nodeName;
218 return nodeName && nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio');
219}
220
221function getTargetInstForClickEvent(topLevelType, targetInst, nativeEvent) {
222 if (topLevelType === 'topClick') {
223 return getInstIfValueChanged(targetInst, nativeEvent);
224 }
225}
226
227function getTargetInstForInputOrChangeEvent(topLevelType, targetInst, nativeEvent) {
228 if (topLevelType === 'topInput' || topLevelType === 'topChange') {
229 return getInstIfValueChanged(targetInst, nativeEvent);
230 }
231}
232
233function handleControlledInputBlur(inst, node) {
234 // TODO: In IE, inst is occasionally null. Why?
235 if (inst == null) {
236 return;
237 }
238
239 // Fiber and ReactDOM keep wrapper state in separate places
240 var state = inst._wrapperState || node._wrapperState;
241
242 if (!state || !state.controlled || node.type !== 'number') {
243 return;
244 }
245
246 // If controlled, assign the value attribute to the current value on blur
247 var value = '' + node.value;
248 if (node.getAttribute('value') !== value) {
249 node.setAttribute('value', value);
250 }
251}
252
253/**
254 * This plugin creates an `onChange` event that normalizes change events
255 * across form elements. This event fires at a time when it's possible to
256 * change the element's value without seeing a flicker.
257 *
258 * Supported elements are:
259 * - input (see `isTextInputElement`)
260 * - textarea
261 * - select
262 */
263var ChangeEventPlugin = {
264 eventTypes: eventTypes,
265
266 _allowSimulatedPassThrough: true,
267 _isInputEventSupported: isInputEventSupported,
268
269 extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
270 var targetNode = targetInst ? ReactDOMComponentTree.getNodeFromInstance(targetInst) : window;
271
272 var getTargetInstFunc, handleEventFunc;
273 if (shouldUseChangeEvent(targetNode)) {
274 if (doesChangeEventBubble) {
275 getTargetInstFunc = getTargetInstForChangeEvent;
276 } else {
277 handleEventFunc = handleEventsForChangeEventIE8;
278 }
279 } else if (isTextInputElement(targetNode)) {
280 if (isInputEventSupported) {
281 getTargetInstFunc = getTargetInstForInputOrChangeEvent;
282 } else {
283 getTargetInstFunc = getTargetInstForInputEventPolyfill;
284 handleEventFunc = handleEventsForInputEventPolyfill;
285 }
286 } else if (shouldUseClickEvent(targetNode)) {
287 getTargetInstFunc = getTargetInstForClickEvent;
288 }
289
290 if (getTargetInstFunc) {
291 var inst = getTargetInstFunc(topLevelType, targetInst, nativeEvent);
292 if (inst) {
293 var event = createAndAccumulateChangeEvent(inst, nativeEvent, nativeEventTarget);
294 return event;
295 }
296 }
297
298 if (handleEventFunc) {
299 handleEventFunc(topLevelType, targetNode, targetInst);
300 }
301
302 // When blurring, set the value attribute for number inputs
303 if (topLevelType === 'topBlur') {
304 handleControlledInputBlur(targetInst, targetNode);
305 }
306 }
307};
308
309module.exports = ChangeEventPlugin;
\No newline at end of file