UNPKG

5.93 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 EventPropagators = require('./EventPropagators');
12var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
13var ReactDOMComponentTree = require('./ReactDOMComponentTree');
14var ReactInputSelection = require('./ReactInputSelection');
15var SyntheticEvent = require('./SyntheticEvent');
16
17var getActiveElement = require('fbjs/lib/getActiveElement');
18var isTextInputElement = require('./isTextInputElement');
19var shallowEqual = require('fbjs/lib/shallowEqual');
20
21var skipSelectionChangeEvent = ExecutionEnvironment.canUseDOM && 'documentMode' in document && document.documentMode <= 11;
22
23var eventTypes = {
24 select: {
25 phasedRegistrationNames: {
26 bubbled: 'onSelect',
27 captured: 'onSelectCapture'
28 },
29 dependencies: ['topBlur', 'topContextMenu', 'topFocus', 'topKeyDown', 'topKeyUp', 'topMouseDown', 'topMouseUp', 'topSelectionChange']
30 }
31};
32
33var activeElement = null;
34var activeElementInst = null;
35var lastSelection = null;
36var mouseDown = false;
37
38// Track whether a listener exists for this plugin. If none exist, we do
39// not extract events. See #3639.
40var hasListener = false;
41
42/**
43 * Get an object which is a unique representation of the current selection.
44 *
45 * The return value will not be consistent across nodes or browsers, but
46 * two identical selections on the same node will return identical objects.
47 *
48 * @param {DOMElement} node
49 * @return {object}
50 */
51function getSelection(node) {
52 if ('selectionStart' in node && ReactInputSelection.hasSelectionCapabilities(node)) {
53 return {
54 start: node.selectionStart,
55 end: node.selectionEnd
56 };
57 } else if (window.getSelection) {
58 var selection = window.getSelection();
59 return {
60 anchorNode: selection.anchorNode,
61 anchorOffset: selection.anchorOffset,
62 focusNode: selection.focusNode,
63 focusOffset: selection.focusOffset
64 };
65 } else if (document.selection) {
66 var range = document.selection.createRange();
67 return {
68 parentElement: range.parentElement(),
69 text: range.text,
70 top: range.boundingTop,
71 left: range.boundingLeft
72 };
73 }
74}
75
76/**
77 * Poll selection to see whether it's changed.
78 *
79 * @param {object} nativeEvent
80 * @return {?SyntheticEvent}
81 */
82function constructSelectEvent(nativeEvent, nativeEventTarget) {
83 // Ensure we have the right element, and that the user is not dragging a
84 // selection (this matches native `select` event behavior). In HTML5, select
85 // fires only on input and textarea thus if there's no focused element we
86 // won't dispatch.
87 if (mouseDown || activeElement == null || activeElement !== getActiveElement()) {
88 return null;
89 }
90
91 // Only fire when selection has actually changed.
92 var currentSelection = getSelection(activeElement);
93 if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
94 lastSelection = currentSelection;
95
96 var syntheticEvent = SyntheticEvent.getPooled(eventTypes.select, activeElementInst, nativeEvent, nativeEventTarget);
97
98 syntheticEvent.type = 'select';
99 syntheticEvent.target = activeElement;
100
101 EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent);
102
103 return syntheticEvent;
104 }
105
106 return null;
107}
108
109/**
110 * This plugin creates an `onSelect` event that normalizes select events
111 * across form elements.
112 *
113 * Supported elements are:
114 * - input (see `isTextInputElement`)
115 * - textarea
116 * - contentEditable
117 *
118 * This differs from native browser implementations in the following ways:
119 * - Fires on contentEditable fields as well as inputs.
120 * - Fires for collapsed selection.
121 * - Fires after user input.
122 */
123var SelectEventPlugin = {
124 eventTypes: eventTypes,
125
126 extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
127 if (!hasListener) {
128 return null;
129 }
130
131 var targetNode = targetInst ? ReactDOMComponentTree.getNodeFromInstance(targetInst) : window;
132
133 switch (topLevelType) {
134 // Track the input node that has focus.
135 case 'topFocus':
136 if (isTextInputElement(targetNode) || targetNode.contentEditable === 'true') {
137 activeElement = targetNode;
138 activeElementInst = targetInst;
139 lastSelection = null;
140 }
141 break;
142 case 'topBlur':
143 activeElement = null;
144 activeElementInst = null;
145 lastSelection = null;
146 break;
147 // Don't fire the event while the user is dragging. This matches the
148 // semantics of the native select event.
149 case 'topMouseDown':
150 mouseDown = true;
151 break;
152 case 'topContextMenu':
153 case 'topMouseUp':
154 mouseDown = false;
155 return constructSelectEvent(nativeEvent, nativeEventTarget);
156 // Chrome and IE fire non-standard event when selection is changed (and
157 // sometimes when it hasn't). IE's event fires out of order with respect
158 // to key and input events on deletion, so we discard it.
159 //
160 // Firefox doesn't support selectionchange, so check selection status
161 // after each key entry. The selection changes after keydown and before
162 // keyup, but we check on keydown as well in the case of holding down a
163 // key, when multiple keydown events are fired but only one keyup is.
164 // This is also our approach for IE handling, for the reason above.
165 case 'topSelectionChange':
166 if (skipSelectionChangeEvent) {
167 break;
168 }
169 // falls through
170 case 'topKeyDown':
171 case 'topKeyUp':
172 return constructSelectEvent(nativeEvent, nativeEventTarget);
173 }
174
175 return null;
176 },
177
178 didPutListener: function (inst, registrationName, listener) {
179 if (registrationName === 'onSelect') {
180 hasListener = true;
181 }
182 }
183};
184
185module.exports = SelectEventPlugin;
\No newline at end of file