1
+/**
2
+ * Copyright 2013 Facebook, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ * @providesModule TextChangeEventPlugin
17
+ */
18
+
19
+"use strict";
20
+
21
+var AbstractEvent = require('AbstractEvent');
22
+var EventConstants = require('EventConstants');
23
+var EventPluginHub = require('EventPluginHub');
24
+var EventPropagators = require('EventPropagators');
25
+
26
+var isEventSupported = require('isEventSupported');
27
+var keyOf = require('keyOf');
28
+
29
+var topLevelTypes = EventConstants.topLevelTypes;
30
+
31
+var abstractEventTypes = {
32
+ textChange: {
33
+ phasedRegistrationNames: {
34
+ bubbled: keyOf({onTextChange: null}),
35
+ captured: keyOf({onTextChangeCapture: null})
36
+ }
37
+ }
38
+};
39
+
40
+// IE9 claims to support the input event but fails to trigger it when deleting
41
+// text, so we ignore its input events
42
+var isInputSupported = isEventSupported('input') && (
43
+ !("documentMode" in document) || document.documentMode > 9
44
+);
45
+
46
+var hasInputCapabilities = function(elem) {
47
+ // The HTML5 spec lists many more types than `text` and `password` on which
48
+ // the input event is triggered but none of them exist in old IE, so we don't
49
+ // check them here.
50
+ // TODO: <textarea> should be supported too but IE seems to reset the
51
+ // selection when changing textarea contents during a selectionchange event
52
+ // so it's not listed here for now.
53
+ return (
54
+ elem.nodeName === 'INPUT' &&
55
+ (elem.type === 'text' || elem.type === 'password')
56
+ );
57
+};
58
+
59
+var activeElement = null;
60
+var activeElementID = null;
61
+var activeElementValue = null;
62
+var activeElementValueProp = null;
63
+
64
+// Replacement getter/setter for the `value` property for old IE that gets set
65
+// on the active element
66
+var newValueProp = {
67
+ get: function() {
68
+ return activeElementValueProp.get.call(this);
69
+ },
70
+ set: function(val) {
71
+ activeElementValue = val;
72
+ activeElementValueProp.set.call(this, val);
73
+ }
74
+};
75
+
76
+var handlePropertyChange = function(nativeEvent) {
77
+ var value;
78
+ var abstractEvent;
79
+
80
+ if (nativeEvent.propertyName === "value") {
81
+ value = nativeEvent.srcElement.value;
82
+ if (value !== activeElementValue) {
83
+ activeElementValue = value;
84
+
85
+ abstractEvent = AbstractEvent.getPooled(
86
+ abstractEventTypes.textChange,
87
+ activeElementID,
88
+ nativeEvent
89
+ );
90
+ EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
91
+ EventPluginHub.enqueueAbstractEvents(abstractEvent);
92
+ EventPluginHub.processAbstractEventQueue();
93
+ }
94
+ }
95
+};
96
+
97
+/**
98
+ * @param {string} topLevelType Record from `EventConstants`.
99
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
100
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
101
+ * @param {object} nativeEvent Native browser event.
102
+ * @return {*} An accumulation of `AbstractEvent`s.
103
+ * @see {EventPluginHub.extractAbstractEvents}
104
+ */
105
+var extractAbstractEvents = function(
106
+ topLevelType,
107
+ topLevelTarget,
108
+ topLevelTargetID,
109
+ nativeEvent) {
110
+ var targetID;
111
+
112
+ if (isInputSupported && topLevelType === topLevelTypes.topInput) {
113
+ // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
114
+ // what we want so fall through here and trigger an abstract event...
115
+ if (topLevelTarget.nodeName === 'TEXTAREA') {
116
+ // ...unless it's a textarea, in which case we don't fire an event (so
117
+ // that we have consistency with our old-IE shim).
118
+ return;
119
+ }
120
+ targetID = topLevelTargetID;
121
+
122
+ } else if (!isInputSupported && topLevelType === topLevelTypes.topFocus) {
123
+ // In IE8, we can capture almost all .value changes by adding a
124
+ // propertychange handler and looking for events with propertyName 'value'
125
+ // In IE9, propertychange fires for most input events but is buggy and
126
+ // doesn't fire when text is deleted, but conveniently, selectionchange
127
+ // appears to fire in all of the remaining cases so we catch those and
128
+ // forward the event if the value has changed
129
+ // In either case, we don't want to call the event handler if the value is
130
+ // changed from JS so we redefine a setter for `.value` that updates our
131
+ // activeElementValue variable, allowing us to ignore those changes
132
+ if (hasInputCapabilities(topLevelTarget)) {
133
+ activeElement = topLevelTarget;
134
+ activeElementID = topLevelTargetID;
135
+ activeElementValue = topLevelTarget.value;
136
+ activeElementValueProp = Object.getOwnPropertyDescriptor(
137
+ topLevelTarget.constructor.prototype,
138
+ 'value'
139
+ );
140
+
141
+ Object.defineProperty(topLevelTarget, 'value', newValueProp);
142
+ topLevelTarget.attachEvent('onpropertychange', handlePropertyChange);
143
+ }
144
+ return;
145
+
146
+ } else if (!isInputSupported && topLevelType === topLevelTypes.topBlur) {
147
+ // If blur is triggered due to element removal, the target is set to the
148
+ // <html> element so ignore that and unbind from activeElement instead
149
+ if (activeElement) {
150
+ // delete restores the original property definition
151
+ delete activeElement.value;
152
+ activeElement.detachEvent('onpropertychange', handlePropertyChange);
153
+
154
+ activeElement = null;
155
+ activeElementID = null;
156
+ activeElementValue = null;
157
+ activeElementValueProp = null;
158
+ }
159
+ return;
160
+
161
+ } else if (!isInputSupported && (
162
+ topLevelType === topLevelTypes.topSelectionChange ||
163
+ topLevelType === topLevelTypes.topKeyUp ||
164
+ topLevelType === topLevelTypes.topKeyDown)) {
165
+ // On the selectionchange event, the target is just document which isn't
166
+ // helpful for us so just check activeElement instead.
167
+ //
168
+ // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
169
+ // propertychange on the first input event after setting `value` from a
170
+ // script and fires only keydown, keypress, keyup. Catching keyup usually
171
+ // gets it and catching keydown lets us fire an event for the first
172
+ // keystroke if user does a key repeat (it'll be a little delayed: right
173
+ // before the second keystroke). Other input methods (e.g., paste) seem to
174
+ // fire selectionchange normally.
175
+ if (!activeElement || activeElement.value === activeElementValue) {
176
+ return;
177
+ }
178
+ activeElementValue = activeElement.value;
179
+ targetID = activeElementID;
180
+
181
+ } else {
182
+ return;
183
+ }
184
+
185
+ // If we've made it to this point, some value change occurred
186
+ var abstractEvent = AbstractEvent.getPooled(
187
+ abstractEventTypes.textChange,
188
+ targetID,
189
+ nativeEvent
190
+ );
191
+ EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
192
+ return abstractEvent;
193
+};
194
+
195
+var TextChangeEventPlugin = {
196
+ abstractEventTypes: abstractEventTypes,
197
+ extractAbstractEvents: extractAbstractEvents
198
+};
199
+
200
+module.exports = TextChangeEventPlugin;