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
- * @flow
7
- */
8
-
9
-import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
10
-import type {PropagationPhases} from './legacy-events/PropagationPhases';
11
-
12
-import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils';
13
-import {CustomEvent} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
14
-
15
-/**
16
- * Get a list of listeners for a specific event, in-order.
17
- * For React Native we treat the props-based function handlers
18
- * as the first-class citizens, and they are always executed first
19
- * for both capture and bubbling phase.
20
- *
21
- * We need "phase" propagated to this point to support the HostComponent
22
- * EventEmitter API, which does not mutate the name of the handler based
23
- * on phase (whereas prop handlers are registered as `onMyEvent` and `onMyEvent_Capture`).
24
- *
25
- * Native system events emitted into React Native
26
- * will be emitted both to the prop handler function and to imperative event
27
- * listeners.
28
- *
29
- * This will either return null, a single Function without an array, or
30
- * an array of 2+ items.
31
- */
32
-export default function getListeners(
33
- inst: Fiber,
34
- registrationName: string,
35
- phase: PropagationPhases,
36
- dispatchToImperativeListeners: boolean,
37
-): null | Function | Array<Function> {
38
- const stateNode = inst.stateNode;
39
-
40
- if (stateNode === null) {
41
- return null;
42
- }
43
-
44
- // If null: Work in progress (ex: onload events in incremental mode).
45
- const props = getFiberCurrentPropsFromNode(stateNode);
46
- if (props === null) {
47
- // Work in progress.
48
- return null;
49
- }
50
-
51
- const listener = props[registrationName];
52
-
53
- if (listener && typeof listener !== 'function') {
54
- throw new Error(
55
- `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`,
56
- );
57
- }
58
-
59
- // If there are no imperative listeners, early exit.
60
- if (
61
- !(
62
- dispatchToImperativeListeners &&
63
- stateNode.canonical &&
64
- stateNode.canonical._eventListeners
65
- )
66
- ) {
67
- return listener;
68
- }
69
-
70
- // Below this is the de-optimized path.
71
- // If you are using _eventListeners, we do not (yet)
72
- // expect this to be as performant as the props-only path.
73
- // If/when this becomes a bottleneck, it can be refactored
74
- // to avoid unnecessary closures and array allocations.
75
- //
76
- // Previously, there was only one possible listener for an event:
77
- // the onEventName property in props.
78
- // Now, it is also possible to have N listeners
79
- // for a specific event on a node. Thus, we accumulate all of the listeners,
80
- // including the props listener, and return a function that calls them all in
81
- // order, starting with the handler prop and then the listeners in order.
82
- // We return either a non-empty array or null.
83
- const listeners = [];
84
- if (listener) {
85
- listeners.push(listener);
86
- }
87
-
88
- // TODO: for now, all of these events get an `rn:` prefix to enforce
89
- // that the user knows they're only getting non-W3C-compliant events
90
- // through this imperative event API.
91
- // Events might not necessarily be noncompliant, but we currently have
92
- // no verification that /any/ events are compliant.
93
- // Thus, we prefix to ensure no collision with W3C event names.
94
- const requestedPhaseIsCapture = phase === 'captured';
95
- const mangledImperativeRegistrationName = requestedPhaseIsCapture
96
- ? 'rn:' + registrationName.replace(/Capture$/, '')
97
- : 'rn:' + registrationName;
98
-
99
- // Get imperative event listeners for this event
100
- if (
101
- stateNode.canonical._eventListeners[mangledImperativeRegistrationName] &&
102
- stateNode.canonical._eventListeners[mangledImperativeRegistrationName]
103
- .length > 0
104
- ) {
105
- const eventListeners =
106
- stateNode.canonical._eventListeners[mangledImperativeRegistrationName];
107
-
108
- eventListeners.forEach(listenerObj => {
109
- // Make sure phase of listener matches requested phase
110
- const isCaptureEvent =
111
- listenerObj.options.capture != null && listenerObj.options.capture;
112
- if (isCaptureEvent !== requestedPhaseIsCapture) {
113
- return;
114
- }
115
-
116
- // For now (this is an area of future optimization) we must wrap
117
- // all imperative event listeners in a function to unwrap the SyntheticEvent
118
- // and pass them an Event.
119
- // When this API is more stable and used more frequently, we can revisit.
120
- // $FlowFixMe[missing-local-annot]
121
- const listenerFnWrapper = function (syntheticEvent, ...args) {
122
- const eventInst = new CustomEvent(mangledImperativeRegistrationName, {
123
- detail: syntheticEvent.nativeEvent,
124
- });
125
- eventInst.isTrusted = true;
126
- eventInst.setSyntheticEvent(syntheticEvent);
127
-
128
- listenerObj.listener(eventInst, ...args);
129
- };
130
-
131
- // Only call once?
132
- // If so, we ensure that it's only called once by setting a flag
133
- // and by removing it from eventListeners once it is called (but only
134
- // when it's actually been executed).
135
- if (listenerObj.options.once) {
136
- listeners.push(function (...args) {
137
- // Remove from the event listener once it's been called
138
- stateNode.canonical.removeEventListener_unstable(
139
- mangledImperativeRegistrationName,
140
- listenerObj.listener,
141
- listenerObj.capture,
142
- );
143
-
144
- // Guard against function being called more than once in
145
- // case there are somehow multiple in-flight references to
146
- // it being processed
147
- if (!listenerObj.invalidated) {
148
- listenerObj.invalidated = true;
149
- listenerObj.listener(...args);
150
- }
151
- });
152
- } else {
153
- listeners.push(listenerFnWrapper);
154
- }
155
- });
156
- }
157
-
158
- if (listeners.length === 0) {
159
- return null;
160
- }
161
- if (listeners.length === 1) {
162
- return listeners[0];
163
- }
164
-
165
- return listeners;
166
-}