main
js 209 lines 6.27 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
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 * @flow
8 */
9
10 import type {AnyNativeEvent} from '../PluginModuleType';
11 import type {DOMEventName} from '../DOMEventNames';
12 import type {DispatchQueue} from '../DOMPluginEventSystem';
13 import type {EventSystemFlags} from '../EventSystemFlags';
14 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
15 import type {ReactSyntheticEvent} from '../ReactSyntheticEventType';
16
17 import {canUseDOM} from 'shared/ExecutionEnvironment';
18 import {SyntheticEvent} from '../../events/SyntheticEvent';
19 import isTextInputElement from '../isTextInputElement';
20 import shallowEqual from 'shared/shallowEqual';
21
22 import {registerTwoPhaseEvent} from '../EventRegistry';
23 import getActiveElement from '../../client/getActiveElement';
24 import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
25 import {hasSelectionCapabilities} from '../../client/ReactInputSelection';
26 import {DOCUMENT_NODE} from '../../client/HTMLNodeType';
27 import {accumulateTwoPhaseListeners} from '../DOMPluginEventSystem';
28
29 const skipSelectionChangeEvent =
30 canUseDOM && 'documentMode' in document && document.documentMode <= 11;
31
32 function registerEvents() {
33 registerTwoPhaseEvent('onSelect', [
34 'focusout',
35 'contextmenu',
36 'dragend',
37 'focusin',
38 'keydown',
39 'keyup',
40 'mousedown',
41 'mouseup',
42 'selectionchange',
43 ]);
44 }
45
46 let activeElement = null;
47 let activeElementInst = null;
48 let lastSelection = null;
49 let mouseDown = false;
50
51 /**
52 * Get an object which is a unique representation of the current selection.
53 *
54 * The return value will not be consistent across nodes or browsers, but
55 * two identical selections on the same node will return identical objects.
56 */
57 function getSelection(node: any) {
58 if ('selectionStart' in node && hasSelectionCapabilities(node)) {
59 return {
60 start: node.selectionStart,
61 end: node.selectionEnd,
62 };
63 } else {
64 const win =
65 (node.ownerDocument && node.ownerDocument.defaultView) || window;
66 const selection = win.getSelection();
67 return {
68 anchorNode: selection.anchorNode,
69 anchorOffset: selection.anchorOffset,
70 focusNode: selection.focusNode,
71 focusOffset: selection.focusOffset,
72 };
73 }
74 }
75
76 /**
77 * Get document associated with the event target.
78 */
79 function getEventTargetDocument(eventTarget: any) {
80 return eventTarget.window === eventTarget
81 ? eventTarget.document
82 : eventTarget.nodeType === DOCUMENT_NODE
83 ? eventTarget
84 : eventTarget.ownerDocument;
85 }
86
87 /**
88 * Poll selection to see whether it's changed.
89 *
90 * @param {object} nativeEvent
91 * @param {object} nativeEventTarget
92 * @return {?SyntheticEvent}
93 */
94 function constructSelectEvent(
95 dispatchQueue: DispatchQueue,
96 nativeEvent: AnyNativeEvent,
97 nativeEventTarget: null | EventTarget,
98 ) {
99 // Ensure we have the right element, and that the user is not dragging a
100 // selection (this matches native `select` event behavior). In HTML5, select
101 // fires only on input and textarea thus if there's no focused element we
102 // won't dispatch.
103 const doc = getEventTargetDocument(nativeEventTarget);
104
105 if (
106 mouseDown ||
107 activeElement == null ||
108 activeElement !== getActiveElement(doc)
109 ) {
110 return;
111 }
112
113 // Only fire when selection has actually changed.
114 const currentSelection = getSelection(activeElement);
115 if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
116 lastSelection = currentSelection;
117
118 const listeners = accumulateTwoPhaseListeners(
119 activeElementInst,
120 'onSelect',
121 );
122 if (listeners.length > 0) {
123 const event: ReactSyntheticEvent = new SyntheticEvent(
124 'onSelect',
125 'select',
126 null,
127 nativeEvent,
128 nativeEventTarget,
129 );
130 dispatchQueue.push({event, listeners});
131 event.target = activeElement;
132 }
133 }
134 }
135
136 /**
137 * This plugin creates an `onSelect` event that normalizes select events
138 * across form elements.
139 *
140 * Supported elements are:
141 * - input (see `isTextInputElement`)
142 * - textarea
143 * - contentEditable
144 *
145 * This differs from native browser implementations in the following ways:
146 * - Fires on contentEditable fields as well as inputs.
147 * - Fires for collapsed selection.
148 * - Fires after user input.
149 */
150 function extractEvents(
151 dispatchQueue: DispatchQueue,
152 domEventName: DOMEventName,
153 targetInst: null | Fiber,
154 nativeEvent: AnyNativeEvent,
155 nativeEventTarget: null | EventTarget,
156 eventSystemFlags: EventSystemFlags,
157 targetContainer: EventTarget,
158 ) {
159 const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
160
161 switch (domEventName) {
162 // Track the input node that has focus.
163 case 'focusin':
164 if (
165 isTextInputElement(targetNode as any) ||
166 targetNode.contentEditable === 'true'
167 ) {
168 activeElement = targetNode;
169 activeElementInst = targetInst;
170 lastSelection = null;
171 }
172 break;
173 case 'focusout':
174 activeElement = null;
175 activeElementInst = null;
176 lastSelection = null;
177 break;
178 // Don't fire the event while the user is dragging. This matches the
179 // semantics of the native select event.
180 case 'mousedown':
181 mouseDown = true;
182 break;
183 case 'contextmenu':
184 case 'mouseup':
185 case 'dragend':
186 mouseDown = false;
187 constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget);
188 break;
189 // Chrome and IE fire non-standard event when selection is changed (and
190 // sometimes when it hasn't). IE's event fires out of order with respect
191 // to key and input events on deletion, so we discard it.
192 //
193 // Firefox doesn't support selectionchange, so check selection status
194 // after each key entry. The selection changes after keydown and before
195 // keyup, but we check on keydown as well in the case of holding down a
196 // key, when multiple keydown events are fired but only one keyup is.
197 // This is also our approach for IE handling, for the reason above.
198 case 'selectionchange':
199 if (skipSelectionChangeEvent) {
200 break;
201 }
202 // falls through
203 case 'keydown':
204 case 'keyup':
205 constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget);
206 }
207 }
208
209 export {registerEvents, extractEvents};