Add select event plugin
Polyfill 'onSelect' behavior for React. Use non-standard 'selectionchange' event rather than 'select' event. This allows us to fire the event when user moves the cursor via arrow keys, and not just when they select multiple chars. Add methods to ReactDOMSelection to make getting current selection easier, so we can do a fast check for change without having to calculate char offsets for selection start and end.
Josh Duck committed
Oct 7, 2013 at 14:54 UTC
2b7a7599bb06f5cbcf01803fe1ae264b2b07d425
8 files changed
+284
-43
src/core/ReactComponentWithPureRenderMixin.js
+2
-26
@@ -18,6 +18,8 @@
18
19
"use strict";
20
21
+var shallowEqual = require('shallowEqual');
22
+
23
/**
24
* If your React component's render function is "pure", e.g. it will render the
25
* same result given the same props and state, provide this Mixin for a
@@ -49,30 +51,4 @@ var ReactComponentWithPureRenderMixin = {
51
}
52
};
53
52
-/**
53
- * Performs equality by iterating through keys on an object and returning
54
- * false when any key has values which are not strictly equal between
55
- * objA and objB. Returns true when the values of all keys are strictly equal.
56
- */
57
-function shallowEqual(objA, objB) {
58
- if (objA === objB) {
59
- return true;
60
- }
61
- var key;
62
- // Test for A's keys different from B.
63
- for (key in objA) {
64
- if (objA.hasOwnProperty(key) &&
65
- (!objB.hasOwnProperty(key) || objA[key] !== objB[key])) {
66
- return false;
67
- }
68
- }
69
- // Test for B'a keys missing from A.
70
- for (key in objB) {
71
- if (objB.hasOwnProperty(key) && !objA.hasOwnProperty(key)) {
72
- return false;
73
- }
74
- }
75
- return true;
76
-}
77
-
54
module.exports = ReactComponentWithPureRenderMixin;
src/core/ReactDOMSelection.js
+12
-10
@@ -33,8 +33,9 @@ var getTextContentAccessor = require('getTextContentAccessor');
33
* differ between the two APIs.
34
*
35
* @param {DOMElement} node
36
+ * @return {object}
37
*/
37
-function getIESelection(node) {
38
+function getIEOffsets(node) {
39
var selection = document.selection;
40
var selectedRange = selection.createRange();
41
var selectedLength = selectedRange.text.length;
@@ -55,8 +56,9 @@ function getIESelection(node) {
56
57
/**
58
* @param {DOMElement} node
59
+ * @return {object}
60
*/
59
-function getModernSelection(node) {
61
+function getModernOffsets(node) {
62
var selection = window.getSelection();
63
var anchorNode = selection.anchorNode;
64
var anchorOffset = selection.anchorOffset;
@@ -90,7 +92,7 @@ function getModernSelection(node) {
92
* @param {DOMElement|DOMTextNode} node
93
* @param {object} offsets
94
*/
93
-function setIESelection(node, offsets) {
95
+function setIEOffsets(node, offsets) {
96
var range = document.selection.createRange().duplicate();
97
var start, end;
98
@@ -124,7 +126,7 @@ function setIESelection(node, offsets) {
126
* @param {DOMElement|DOMTextNode} node
127
* @param {object} offsets
128
*/
127
-function setModernSelection(node, offsets) {
129
+function setModernOffsets(node, offsets) {
130
var selection = window.getSelection();
131
132
var length = node[getTextContentAccessor()].length;
@@ -149,18 +151,18 @@ var ReactDOMSelection = {
151
/**
152
* @param {DOMElement} node
153
*/
152
- get: function(node) {
153
- var getSelection = document.selection ? getIESelection : getModernSelection;
154
- return getSelection(node);
154
+ getOffsets: function(node) {
155
+ var getOffsets = document.selection ? getIEOffsets : getModernOffsets;
156
+ return getOffsets(node);
157
},
158
159
/**
160
* @param {DOMElement|DOMTextNode} node
161
* @param {object} offsets
162
*/
161
- set: function(node, offsets) {
162
- var setSelection = document.selection ? setIESelection : setModernSelection;
163
- setSelection(node, offsets);
163
+ setOffsets: function(node, offsets) {
164
+ var setOffsets = document.selection ? setIEOffsets : setModernOffsets;
165
+ setOffsets(node, offsets);
166
}
167
};
168
src/core/ReactDefaultInjection.js
+6
-4
@@ -32,14 +32,15 @@ var ReactPerf = require('ReactPerf');
32
var DefaultDOMPropertyConfig = require('DefaultDOMPropertyConfig');
33
var DOMProperty = require('DOMProperty');
34
35
-var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
36
-var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
35
var ChangeEventPlugin = require('ChangeEventPlugin');
36
var CompositionEventPlugin = require('CompositionEventPlugin');
37
+var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
38
+var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
39
var EventPluginHub = require('EventPluginHub');
40
+var MobileSafariClickEventPlugin = require('MobileSafariClickEventPlugin');
41
var ReactInstanceHandles = require('ReactInstanceHandles');
42
+var SelectEventPlugin = require('SelectEventPlugin');
43
var SimpleEventPlugin = require('SimpleEventPlugin');
42
-var MobileSafariClickEventPlugin = require('MobileSafariClickEventPlugin');
44
45
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
46
var ReactUpdates = require('ReactUpdates');
@@ -61,7 +62,8 @@ function inject() {
62
'EnterLeaveEventPlugin': EnterLeaveEventPlugin,
63
'ChangeEventPlugin': ChangeEventPlugin,
64
'CompositionEventPlugin': CompositionEventPlugin,
64
- 'MobileSafariClickEventPlugin': MobileSafariClickEventPlugin
65
+ 'MobileSafariClickEventPlugin': MobileSafariClickEventPlugin,
66
+ 'SelectEventPlugin': SelectEventPlugin
67
});
68
69
ReactDOM.injection.injectComponentClasses({
src/core/ReactInputSelection.js
+2
-2
@@ -111,7 +111,7 @@ var ReactInputSelection = {
111
}
112
} else {
113
// Content editable or old IE textarea.
114
- selection = ReactDOMSelection.get(input);
114
+ selection = ReactDOMSelection.getOffsets(input);
115
}
116
117
return selection || {start: 0, end: 0};
@@ -140,7 +140,7 @@ var ReactInputSelection = {
140
range.moveEnd('character', end - start);
141
range.select();
142
} else {
143
- ReactDOMSelection.set(input, offsets);
143
+ ReactDOMSelection.setOffsets(input, offsets);
144
}
145
}
146
};
src/eventPlugins/ChangeEventPlugin.js
-1
@@ -46,7 +46,6 @@ var activeElementID = null;
46
var activeElementValue = null;
47
var activeElementValueProp = null;
48
49
-
49
/**
50
* SECTION: handle `change` event
51
*/
src/eventPlugins/DefaultEventPluginOrder.js
+1
@@ -35,6 +35,7 @@ var DefaultEventPluginOrder = [
35
keyOf({TapEventPlugin: null}),
36
keyOf({EnterLeaveEventPlugin: null}),
37
keyOf({ChangeEventPlugin: null}),
38
+ keyOf({SelectEventPlugin: null}),
39
keyOf({CompositionEventPlugin: null}),
40
keyOf({AnalyticsEventPlugin: null}),
41
keyOf({MobileSafariClickEventPlugin: null})
src/eventPlugins/SelectEventPlugin.js
new
+212
@@ -0,0 +1,212 @@
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 SelectEventPlugin
17
+ */
18
+
19
+"use strict";
20
+
21
+var EventConstants = require('EventConstants');
22
+var EventPluginHub = require('EventPluginHub');
23
+var EventPropagators = require('EventPropagators');
24
+var ExecutionEnvironment = require('ExecutionEnvironment');
25
+var SyntheticEvent = require('SyntheticEvent');
26
+
27
+var getActiveElement = require('getActiveElement');
28
+var isEventSupported = require('isEventSupported');
29
+var keyOf = require('keyOf');
30
+var shallowEqual = require('shallowEqual');
31
+
32
+var topLevelTypes = EventConstants.topLevelTypes;
33
+
34
+var eventTypes = {
35
+ select: {
36
+ phasedRegistrationNames: {
37
+ bubbled: keyOf({onSelect: null}),
38
+ captured: keyOf({onSelectCapture: null})
39
+ }
40
+ }
41
+};
42
+
43
+var useSelectionChange = false;
44
+var useSelect = false;
45
+
46
+if (ExecutionEnvironment.canUseDOM) {
47
+ useSelectionChange = 'onselectionchange' in document;
48
+ useSelect = isEventSupported('select');
49
+}
50
+
51
+var activeElement = null;
52
+var activeElementID = null;
53
+var activeNativeEvent = null;
54
+var lastSelection = null;
55
+var mouseDown = false;
56
+
57
+/**
58
+ * Get an object which is a unique representation of the current selection.
59
+ *
60
+ * The return value will not be consistent across nodes or browsers, but
61
+ * two identical selections on the same node will return identical objects.
62
+ *
63
+ * @param {DOMElement} node
64
+ * @param {object}
65
+ */
66
+function getSelection(node) {
67
+ if ('selectionStart' in node) {
68
+ return {
69
+ start: node.selectionStart,
70
+ end: node.selectionEnd
71
+ };
72
+ } else if (document.selection) {
73
+ var range = document.selection.createRange();
74
+ return {
75
+ parentElement: range.parentElement(),
76
+ text: range.text,
77
+ top: range.boundingTop,
78
+ left: range.boundingLeft
79
+ };
80
+ } else {
81
+ var selection = window.getSelection();
82
+ return {
83
+ anchorNode: selection.anchorNode,
84
+ anchorOffset: selection.anchorOffset,
85
+ focusNode: selection.focusNode,
86
+ focusOffset: selection.focusOffset
87
+ };
88
+ }
89
+}
90
+
91
+/**
92
+ * Poll selection to see whether it's changed.
93
+ *
94
+ * @param {object} nativeEvent
95
+ * @return {?SyntheticEvent}
96
+ */
97
+function constructSelectEvent(nativeEvent) {
98
+ // Ensure we have the right element, and that the user is not dragging a
99
+ // selection (this matches native `select` event behavior).
100
+ if (mouseDown || activeElement != getActiveElement()) {
101
+ return;
102
+ }
103
+
104
+ // Only fire when selection has actually changed.
105
+ var currentSelection = getSelection(activeElement);
106
+ if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
107
+ lastSelection = currentSelection;
108
+
109
+ return SyntheticEvent.getPooled(
110
+ eventTypes.select,
111
+ activeElementID,
112
+ nativeEvent
113
+ );
114
+ }
115
+}
116
+
117
+/**
118
+ * Handle deferred event. And manually dispatch synthetic events.
119
+ */
120
+function handleDeferredEvent() {
121
+ if (!activeNativeEvent) {
122
+ return;
123
+ }
124
+
125
+ var event = constructSelectEvent(activeNativeEvent);
126
+ activeNativeEvent = null;
127
+
128
+ if (event) {
129
+ EventPropagators.accumulateTwoPhaseDispatches(event);
130
+
131
+ // Enqueue and process the abstract event manually.
132
+ EventPluginHub.enqueueEvents(event);
133
+ EventPluginHub.processEventQueue();
134
+ }
135
+}
136
+
137
+/**
138
+ * This plugin creates an `onSelect` event that normalizes select events
139
+ * across form elements.
140
+ *
141
+ * Supported elements are:
142
+ * - input (see `supportedInputTypes`)
143
+ * - TEXTAREA
144
+ * - contentEditable
145
+ *
146
+ * This differs from native browser implementations in the following ways:
147
+ * - Fires on contentEditable fields as well as inputs.
148
+ * - Fires for collapsed selection.
149
+ * - Fires after user input.
150
+ */
151
+var SelectEventPlugin = {
152
+
153
+ eventTypes: eventTypes,
154
+
155
+ /**
156
+ * @param {string} topLevelType Record from `EventConstants`.
157
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
158
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
159
+ * @param {object} nativeEvent Native browser event.
160
+ * @return {*} An accumulation of synthetic events.
161
+ * @see {EventPluginHub.extractEvents}
162
+ */
163
+ extractEvents: function(
164
+ topLevelType,
165
+ topLevelTarget,
166
+ topLevelTargetID,
167
+ nativeEvent) {
168
+
169
+ switch (topLevelType) {
170
+ case topLevelTypes.topFocus:
171
+ // Track the input node that has focus.
172
+ activeElement = topLevelTarget;
173
+ activeElementID = topLevelTargetID;
174
+ lastSelection = null;
175
+ mouseDown = false;
176
+ break;
177
+ case topLevelTypes.topBlur:
178
+ activeElement = null;
179
+ activeElementID = null;
180
+ lastSelection = null;
181
+ mouseDown = false;
182
+ break;
183
+ case topLevelTypes.topSelectionChange:
184
+ // Chrome and IE fire non-standard event whenever selection is
185
+ // changed (and sometimes when it hasn't).
186
+ var event = constructSelectEvent(nativeEvent);
187
+ if (event) {
188
+ EventPropagators.accumulateTwoPhaseDispatches(event);
189
+ return event;
190
+ }
191
+ break;
192
+ case topLevelTypes.topMouseDown:
193
+ // Don't fire the event while the user is dragging.
194
+ mouseDown = true;
195
+ break;
196
+ case topLevelTypes.topMouseUp:
197
+ mouseDown = false;
198
+ activeNativeEvent = nativeEvent;
199
+ handleDeferredEvent.defer();
200
+ break;
201
+ case topLevelTypes.topKeyDown:
202
+ // For Firefox we check seleciton after each key.
203
+ if (!useSelectionChange) {
204
+ activeNativeEvent = nativeEvent;
205
+ handleDeferredEvent.defer();
206
+ }
207
+ break;
208
+ }
209
+ }
210
+};
211
+
212
+module.exports = SelectEventPlugin;
src/utils/shallowEqual.js
new
+49
@@ -0,0 +1,49 @@
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 shallowEqual
17
+ */
18
+
19
+"use strict";
20
+
21
+/**
22
+ * Performs equality by iterating through keys on an object and returning
23
+ * false when any key has values which are not strictly equal between
24
+ * objA and objB. Returns true when the values of all keys are strictly equal.
25
+ *
26
+ * @return {boolean}
27
+ */
28
+function shallowEqual(objA, objB) {
29
+ if (objA === objB) {
30
+ return true;
31
+ }
32
+ var key;
33
+ // Test for A's keys different from B.
34
+ for (key in objA) {
35
+ if (objA.hasOwnProperty(key) &&
36
+ (!objB.hasOwnProperty(key) || objA[key] !== objB[key])) {
37
+ return false;
38
+ }
39
+ }
40
+ // Test for B'a keys missing from A.
41
+ for (key in objB) {
42
+ if (objB.hasOwnProperty(key) && !objA.hasOwnProperty(key)) {
43
+ return false;
44
+ }
45
+ }
46
+ return true;
47
+}
48
+
49
+module.exports = shallowEqual;