1
+/**
2
+ * Copyright (c) Facebook, Inc. and its 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 'legacy-events/PluginModuleType';
11
+import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
12
+import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
13
+import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
14
+
15
+import {enableFlareAPI} from 'shared/ReactFeatureFlags';
16
+import {
17
+ unstable_scheduleCallback as scheduleCallback,
18
+ unstable_NormalPriority as NormalPriority,
19
+} from 'scheduler';
20
+import {
21
+ attemptToDispatchEvent,
22
+ trapEventForResponderEventSystem,
23
+} from './ReactDOMEventListener';
24
+import {
25
+ getListeningSetForElement,
26
+ listenToTopLevel,
27
+} from './ReactBrowserEventEmitter';
28
+import {unsafeCastDOMTopLevelTypeToString} from 'legacy-events/TopLevelEventTypes';
29
+
30
+// TODO: Upgrade this definition once we're on a newer version of Flow that
31
+// has this definition built-in.
32
+type PointerEvent = Event & {
33
+ pointerId: number,
34
+ relatedTarget: EventTarget | null,
35
+};
36
+
37
+import {
38
+ TOP_MOUSE_DOWN,
39
+ TOP_MOUSE_UP,
40
+ TOP_TOUCH_CANCEL,
41
+ TOP_TOUCH_END,
42
+ TOP_TOUCH_START,
43
+ TOP_AUX_CLICK,
44
+ TOP_DOUBLE_CLICK,
45
+ TOP_POINTER_CANCEL,
46
+ TOP_POINTER_DOWN,
47
+ TOP_POINTER_UP,
48
+ TOP_DRAG_END,
49
+ TOP_DRAG_START,
50
+ TOP_DROP,
51
+ TOP_COMPOSITION_END,
52
+ TOP_COMPOSITION_START,
53
+ TOP_KEY_DOWN,
54
+ TOP_KEY_PRESS,
55
+ TOP_KEY_UP,
56
+ TOP_INPUT,
57
+ TOP_TEXT_INPUT,
58
+ TOP_CLOSE,
59
+ TOP_CANCEL,
60
+ TOP_COPY,
61
+ TOP_CUT,
62
+ TOP_PASTE,
63
+ TOP_CLICK,
64
+ TOP_CHANGE,
65
+ TOP_CONTEXT_MENU,
66
+ TOP_RESET,
67
+ TOP_SUBMIT,
68
+ TOP_DRAG_ENTER,
69
+ TOP_DRAG_LEAVE,
70
+ TOP_MOUSE_OVER,
71
+ TOP_MOUSE_OUT,
72
+ TOP_POINTER_OVER,
73
+ TOP_POINTER_OUT,
74
+ TOP_GOT_POINTER_CAPTURE,
75
+ TOP_LOST_POINTER_CAPTURE,
76
+ TOP_FOCUS,
77
+ TOP_BLUR,
78
+} from './DOMTopLevelEventTypes';
79
+import {IS_REPLAYED} from 'legacy-events/EventSystemFlags';
80
+
81
+type QueuedReplayableEvent = {|
82
+ blockedOn: null | Container | SuspenseInstance,
83
+ topLevelType: DOMTopLevelEventType,
84
+ eventSystemFlags: EventSystemFlags,
85
+ nativeEvent: AnyNativeEvent,
86
+|};
87
+
88
+let hasScheduledReplayAttempt = false;
89
+
90
+// The queue of discrete events to be replayed.
91
+let queuedDiscreteEvents: Array<QueuedReplayableEvent> = [];
92
+
93
+// Indicates if any continuous event targets are non-null for early bailout.
94
+let hasAnyQueuedContinuousEvents: boolean = false;
95
+// The last of each continuous event type. We only need to replay the last one
96
+// if the last target was dehydrated.
97
+let queuedFocus: null | QueuedReplayableEvent = null;
98
+let queuedDrag: null | QueuedReplayableEvent = null;
99
+let queuedMouse: null | QueuedReplayableEvent = null;
100
+// For pointer events there can be one latest event per pointerId.
101
+let queuedPointers: Map<number, QueuedReplayableEvent> = new Map();
102
+let queuedPointerCaptures: Map<number, QueuedReplayableEvent> = new Map();
103
+// We could consider replaying selectionchange and touchmoves too.
104
+
105
+export function hasQueuedDiscreteEvents(): boolean {
106
+ return queuedDiscreteEvents.length > 0;
107
+}
108
+
109
+export function hasQueuedContinuousEvents(): boolean {
110
+ return hasAnyQueuedContinuousEvents;
111
+}
112
+
113
+const discreteReplayableEvents = [
114
+ TOP_MOUSE_DOWN,
115
+ TOP_MOUSE_UP,
116
+ TOP_TOUCH_CANCEL,
117
+ TOP_TOUCH_END,
118
+ TOP_TOUCH_START,
119
+ TOP_AUX_CLICK,
120
+ TOP_DOUBLE_CLICK,
121
+ TOP_POINTER_CANCEL,
122
+ TOP_POINTER_DOWN,
123
+ TOP_POINTER_UP,
124
+ TOP_DRAG_END,
125
+ TOP_DRAG_START,
126
+ TOP_DROP,
127
+ TOP_COMPOSITION_END,
128
+ TOP_COMPOSITION_START,
129
+ TOP_KEY_DOWN,
130
+ TOP_KEY_PRESS,
131
+ TOP_KEY_UP,
132
+ TOP_INPUT,
133
+ TOP_TEXT_INPUT,
134
+ TOP_CLOSE,
135
+ TOP_CANCEL,
136
+ TOP_COPY,
137
+ TOP_CUT,
138
+ TOP_PASTE,
139
+ TOP_CLICK,
140
+ TOP_CHANGE,
141
+ TOP_CONTEXT_MENU,
142
+ TOP_RESET,
143
+ TOP_SUBMIT,
144
+];
145
+
146
+const continuousReplayableEvents = [
147
+ TOP_FOCUS,
148
+ TOP_BLUR,
149
+ TOP_DRAG_ENTER,
150
+ TOP_DRAG_LEAVE,
151
+ TOP_MOUSE_OVER,
152
+ TOP_MOUSE_OUT,
153
+ TOP_POINTER_OVER,
154
+ TOP_POINTER_OUT,
155
+ TOP_GOT_POINTER_CAPTURE,
156
+ TOP_LOST_POINTER_CAPTURE,
157
+];
158
+
159
+export function isReplayableDiscreteEvent(
160
+ eventType: DOMTopLevelEventType,
161
+): boolean {
162
+ return discreteReplayableEvents.indexOf(eventType) > -1;
163
+}
164
+
165
+function trapReplayableEvent(
166
+ topLevelType: DOMTopLevelEventType,
167
+ document: Document,
168
+ listeningSet: Set<DOMTopLevelEventType | string>,
169
+) {
170
+ listenToTopLevel(topLevelType, document, listeningSet);
171
+ if (enableFlareAPI) {
172
+ // Trap events for the responder system.
173
+ const passiveEventKey =
174
+ unsafeCastDOMTopLevelTypeToString(topLevelType) + '_passive';
175
+ if (!listeningSet.has(passiveEventKey)) {
176
+ trapEventForResponderEventSystem(document, topLevelType, true);
177
+ listeningSet.add(passiveEventKey);
178
+ }
179
+ // TODO: This listens to all events as active which might have
180
+ // undesirable effects. It's also unnecessary to have both
181
+ // passive and active listeners. Instead, we could start with
182
+ // a passive and upgrade it to an active one if needed.
183
+ // For replaying purposes the active is never needed since we
184
+ // currently don't preventDefault.
185
+ const activeEventKey =
186
+ unsafeCastDOMTopLevelTypeToString(topLevelType) + '_active';
187
+ if (!listeningSet.has(activeEventKey)) {
188
+ trapEventForResponderEventSystem(document, topLevelType, false);
189
+ listeningSet.add(activeEventKey);
190
+ }
191
+ }
192
+}
193
+
194
+export function eagerlyTrapReplayableEvents(document: Document) {
195
+ const listeningSet = getListeningSetForElement(document);
196
+ // Discrete
197
+ discreteReplayableEvents.forEach(topLevelType => {
198
+ trapReplayableEvent(topLevelType, document, listeningSet);
199
+ });
200
+ // Continuous
201
+ continuousReplayableEvents.forEach(topLevelType => {
202
+ trapReplayableEvent(topLevelType, document, listeningSet);
203
+ });
204
+}
205
+
206
+function createQueuedReplayableEvent(
207
+ blockedOn: null | Container | SuspenseInstance,
208
+ topLevelType: DOMTopLevelEventType,
209
+ eventSystemFlags: EventSystemFlags,
210
+ nativeEvent: AnyNativeEvent,
211
+): QueuedReplayableEvent {
212
+ return {
213
+ blockedOn,
214
+ topLevelType,
215
+ eventSystemFlags: eventSystemFlags | IS_REPLAYED,
216
+ nativeEvent,
217
+ };
218
+}
219
+
220
+export function queueDiscreteEvent(
221
+ blockedOn: null | Container | SuspenseInstance,
222
+ topLevelType: DOMTopLevelEventType,
223
+ eventSystemFlags: EventSystemFlags,
224
+ nativeEvent: AnyNativeEvent,
225
+): void {
226
+ queuedDiscreteEvents.push(
227
+ createQueuedReplayableEvent(
228
+ blockedOn,
229
+ topLevelType,
230
+ eventSystemFlags,
231
+ nativeEvent,
232
+ ),
233
+ );
234
+ if (blockedOn === null && queuedDiscreteEvents.length === 1) {
235
+ // This probably shouldn't happen but some defensive coding might
236
+ // help us get unblocked if we have a bug.
237
+ replayUnblockedEvents();
238
+ }
239
+}
240
+
241
+// Resets the replaying for this type of continuous event to no event.
242
+export function clearIfContinuousEvent(
243
+ topLevelType: DOMTopLevelEventType,
244
+ nativeEvent: AnyNativeEvent,
245
+): void {
246
+ switch (topLevelType) {
247
+ case TOP_FOCUS:
248
+ case TOP_BLUR:
249
+ queuedFocus = null;
250
+ break;
251
+ case TOP_DRAG_ENTER:
252
+ case TOP_DRAG_LEAVE:
253
+ queuedDrag = null;
254
+ break;
255
+ case TOP_MOUSE_OVER:
256
+ case TOP_MOUSE_OUT:
257
+ queuedMouse = null;
258
+ break;
259
+ case TOP_POINTER_OVER:
260
+ case TOP_POINTER_OUT: {
261
+ let pointerId = ((nativeEvent: any): PointerEvent).pointerId;
262
+ queuedPointers.delete(pointerId);
263
+ break;
264
+ }
265
+ case TOP_GOT_POINTER_CAPTURE:
266
+ case TOP_LOST_POINTER_CAPTURE: {
267
+ let pointerId = ((nativeEvent: any): PointerEvent).pointerId;
268
+ queuedPointerCaptures.delete(pointerId);
269
+ break;
270
+ }
271
+ }
272
+}
273
+
274
+function accumulateOrCreateQueuedReplayableEvent(
275
+ existingQueuedEvent: null | QueuedReplayableEvent,
276
+ blockedOn: null | Container | SuspenseInstance,
277
+ topLevelType: DOMTopLevelEventType,
278
+ eventSystemFlags: EventSystemFlags,
279
+ nativeEvent: AnyNativeEvent,
280
+): QueuedReplayableEvent {
281
+ if (
282
+ existingQueuedEvent === null ||
283
+ existingQueuedEvent.nativeEvent !== nativeEvent
284
+ ) {
285
+ return createQueuedReplayableEvent(
286
+ blockedOn,
287
+ topLevelType,
288
+ eventSystemFlags,
289
+ nativeEvent,
290
+ );
291
+ }
292
+ // If we have already queued this exact event, then it's because
293
+ // the different event systems have different DOM event listeners.
294
+ // We can accumulate the flags and store a single event to be
295
+ // replayed.
296
+ existingQueuedEvent.eventSystemFlags |= eventSystemFlags;
297
+ return existingQueuedEvent;
298
+}
299
+
300
+export function queueIfContinuousEvent(
301
+ blockedOn: null | Container | SuspenseInstance,
302
+ topLevelType: DOMTopLevelEventType,
303
+ eventSystemFlags: EventSystemFlags,
304
+ nativeEvent: AnyNativeEvent,
305
+): boolean {
306
+ // These set relatedTarget to null because the replayed event will be treated as if we
307
+ // moved from outside the window (no target) onto the target once it hydrates.
308
+ // Instead of mutating we could clone the event.
309
+ switch (topLevelType) {
310
+ case TOP_FOCUS: {
311
+ const focusEvent = ((nativeEvent: any): FocusEvent);
312
+ queuedFocus = accumulateOrCreateQueuedReplayableEvent(
313
+ queuedFocus,
314
+ blockedOn,
315
+ topLevelType,
316
+ eventSystemFlags,
317
+ focusEvent,
318
+ );
319
+ return true;
320
+ }
321
+ case TOP_DRAG_ENTER: {
322
+ const dragEvent = ((nativeEvent: any): DragEvent);
323
+ queuedDrag = accumulateOrCreateQueuedReplayableEvent(
324
+ queuedDrag,
325
+ blockedOn,
326
+ topLevelType,
327
+ eventSystemFlags,
328
+ dragEvent,
329
+ );
330
+ return true;
331
+ }
332
+ case TOP_MOUSE_OVER: {
333
+ const mouseEvent = ((nativeEvent: any): MouseEvent);
334
+ queuedMouse = accumulateOrCreateQueuedReplayableEvent(
335
+ queuedMouse,
336
+ blockedOn,
337
+ topLevelType,
338
+ eventSystemFlags,
339
+ mouseEvent,
340
+ );
341
+ return true;
342
+ }
343
+ case TOP_POINTER_OVER: {
344
+ const pointerEvent = ((nativeEvent: any): PointerEvent);
345
+ const pointerId = pointerEvent.pointerId;
346
+ queuedPointers.set(
347
+ pointerId,
348
+ accumulateOrCreateQueuedReplayableEvent(
349
+ queuedPointers.get(pointerId) || null,
350
+ blockedOn,
351
+ topLevelType,
352
+ eventSystemFlags,
353
+ pointerEvent,
354
+ ),
355
+ );
356
+ return true;
357
+ }
358
+ case TOP_GOT_POINTER_CAPTURE: {
359
+ const pointerEvent = ((nativeEvent: any): PointerEvent);
360
+ const pointerId = pointerEvent.pointerId;
361
+ queuedPointerCaptures.set(
362
+ pointerId,
363
+ accumulateOrCreateQueuedReplayableEvent(
364
+ queuedPointerCaptures.get(pointerId) || null,
365
+ blockedOn,
366
+ topLevelType,
367
+ eventSystemFlags,
368
+ pointerEvent,
369
+ ),
370
+ );
371
+ return true;
372
+ }
373
+ }
374
+ return false;
375
+}
376
+
377
+function attemptReplayQueuedEvent(queuedEvent: QueuedReplayableEvent): boolean {
378
+ if (queuedEvent.blockedOn !== null) {
379
+ return false;
380
+ }
381
+ let nextBlockedOn = attemptToDispatchEvent(
382
+ queuedEvent.topLevelType,
383
+ queuedEvent.eventSystemFlags,
384
+ queuedEvent.nativeEvent,
385
+ );
386
+ if (nextBlockedOn !== null) {
387
+ // We're still blocked. Try again later.
388
+ queuedEvent.blockedOn = nextBlockedOn;
389
+ return false;
390
+ }
391
+ return true;
392
+}
393
+
394
+function attemptReplayQueuedEventInMap(
395
+ queuedEvent: QueuedReplayableEvent,
396
+ key: number,
397
+ map: Map<number, QueuedReplayableEvent>,
398
+): void {
399
+ if (attemptReplayQueuedEvent(queuedEvent)) {
400
+ map.delete(key);
401
+ }
402
+}
403
+
404
+function replayUnblockedEvents() {
405
+ hasScheduledReplayAttempt = false;
406
+ // First replay discrete events.
407
+ while (queuedDiscreteEvents.length > 0) {
408
+ let nextDiscreteEvent = queuedDiscreteEvents[0];
409
+ if (nextDiscreteEvent.blockedOn !== null) {
410
+ // We're still blocked.
411
+ break;
412
+ }
413
+ let nextBlockedOn = attemptToDispatchEvent(
414
+ nextDiscreteEvent.topLevelType,
415
+ nextDiscreteEvent.eventSystemFlags,
416
+ nextDiscreteEvent.nativeEvent,
417
+ );
418
+ if (nextBlockedOn !== null) {
419
+ // We're still blocked. Try again later.
420
+ nextDiscreteEvent.blockedOn = nextBlockedOn;
421
+ } else {
422
+ // We've successfully replayed the first event. Let's try the next one.
423
+ queuedDiscreteEvents.shift();
424
+ }
425
+ }
426
+ // Next replay any continuous events.
427
+ if (queuedFocus !== null && attemptReplayQueuedEvent(queuedFocus)) {
428
+ queuedFocus = null;
429
+ }
430
+ if (queuedDrag !== null && attemptReplayQueuedEvent(queuedDrag)) {
431
+ queuedDrag = null;
432
+ }
433
+ if (queuedMouse !== null && attemptReplayQueuedEvent(queuedMouse)) {
434
+ queuedMouse = null;
435
+ }
436
+ queuedPointers.forEach(attemptReplayQueuedEventInMap);
437
+ queuedPointerCaptures.forEach(attemptReplayQueuedEventInMap);
438
+}
439
+
440
+function scheduleCallbackIfUnblocked(
441
+ queuedEvent: QueuedReplayableEvent,
442
+ unblocked: Container | SuspenseInstance,
443
+) {
444
+ if (queuedEvent.blockedOn === unblocked) {
445
+ queuedEvent.blockedOn = null;
446
+ if (!hasScheduledReplayAttempt) {
447
+ hasScheduledReplayAttempt = true;
448
+ // Schedule a callback to attempt replaying as many events as are
449
+ // now unblocked. This first might not actually be unblocked yet.
450
+ // We could check it early to avoid scheduling an unnecessary callback.
451
+ scheduleCallback(NormalPriority, replayUnblockedEvents);
452
+ }
453
+ }
454
+}
455
+
456
+export function retryIfBlockedOn(
457
+ unblocked: Container | SuspenseInstance,
458
+): void {
459
+ // Mark anything that was blocked on this as no longer blocked
460
+ // and eligible for a replay.
461
+ if (queuedDiscreteEvents.length > 0) {
462
+ scheduleCallbackIfUnblocked(queuedDiscreteEvents[0], unblocked);
463
+ // This is a exponential search for each boundary that commits. I think it's
464
+ // worth it because we expect very few discrete events to queue up and once
465
+ // we are actually fully unblocked it will be fast to replay them.
466
+ for (let i = 1; i < queuedDiscreteEvents.length; i++) {
467
+ let queuedEvent = queuedDiscreteEvents[i];
468
+ if (queuedEvent.blockedOn === unblocked) {
469
+ queuedEvent.blockedOn = null;
470
+ }
471
+ }
472
+ }
473
+
474
+ if (queuedFocus !== null) {
475
+ scheduleCallbackIfUnblocked(queuedFocus, unblocked);
476
+ }
477
+ if (queuedDrag !== null) {
478
+ scheduleCallbackIfUnblocked(queuedDrag, unblocked);
479
+ }
480
+ if (queuedMouse !== null) {
481
+ scheduleCallbackIfUnblocked(queuedMouse, unblocked);
482
+ }
483
+ const unblock = queuedEvent =>
484
+ scheduleCallbackIfUnblocked(queuedEvent, unblocked);
485
+ queuedPointers.forEach(unblock);
486
+ queuedPointerCaptures.forEach(unblock);
487
+}