UNPKG

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