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
+ * @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
+ const listenerFnWrapper = function(syntheticEvent, ...args) {
121
+ const eventInst = new CustomEvent(mangledImperativeRegistrationName, {
122
+ detail: syntheticEvent.nativeEvent,
123
+ });
124
+ eventInst.isTrusted = true;
125
+ // setSyntheticEvent is present on the React Native Event shim.
126
+ // It is used to forward method calls on Event to the underlying SyntheticEvent.
127
+ // $FlowFixMe
128
+ eventInst.setSyntheticEvent(syntheticEvent);
129
+
130
+ listenerObj.listener(eventInst, ...args);
131
+ };
132
+
133
+ // Only call once?
134
+ // If so, we ensure that it's only called once by setting a flag
135
+ // and by removing it from eventListeners once it is called (but only
136
+ // when it's actually been executed).
137
+ if (listenerObj.options.once) {
138
+ listeners.push(function(...args) {
139
+ // Remove from the event listener once it's been called
140
+ stateNode.canonical.removeEventListener_unstable(
141
+ mangledImperativeRegistrationName,
142
+ listenerObj.listener,
143
+ listenerObj.capture,
144
+ );
145
+
146
+ // Guard against function being called more than once in
147
+ // case there are somehow multiple in-flight references to
148
+ // it being processed
149
+ if (!listenerObj.invalidated) {
150
+ listenerObj.invalidated = true;
151
+ listenerObj.listener(...args);
152
+ }
153
+ });
154
+ } else {
155
+ listeners.push(listenerFnWrapper);
156
+ }
157
+ });
158
+ }
159
+
160
+ if (listeners.length === 0) {
161
+ return null;
162
+ }
163
+ if (listeners.length === 1) {
164
+ return listeners[0];
165
+ }
166
+
167
+ return listeners;
168
+}