@symbiote-native/engine 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/accessibility-props.d.ts +0 -11
- package/build/accessibility-props.js +30 -68
- package/build/animated/graph.js +1 -1
- package/build/animated/leaf-lifecycle.js +2 -2
- package/build/asset-source-resolver.d.ts +2 -0
- package/build/asset-source-resolver.js +13 -0
- package/build/back-handler/index.d.ts +1 -5
- package/build/back-handler/index.js +0 -6
- package/build/debug.js +8 -22
- package/build/dispatch.js +3 -9
- package/build/events/delivery.d.ts +9 -0
- package/build/events/delivery.js +143 -0
- package/build/events/index.js +199 -660
- package/build/events/names.d.ts +24 -0
- package/build/events/names.js +80 -0
- package/build/events/press.d.ts +20 -0
- package/build/events/press.js +89 -0
- package/build/events/responder.d.ts +6 -0
- package/build/events/responder.js +124 -0
- package/build/fabric-props.js +74 -179
- package/build/fabric.d.ts +0 -13
- package/build/fabric.js +18 -38
- package/build/host-access.d.ts +1 -128
- package/build/host-access.js +96 -205
- package/build/host-behavior.d.ts +0 -100
- package/build/host-behavior.js +125 -311
- package/build/image-loader.js +10 -23
- package/build/image-source-resolver.js +3 -7
- package/build/image-source-write.d.ts +0 -11
- package/build/image-source-write.js +14 -34
- package/build/imperative.d.ts +2 -28
- package/build/imperative.js +60 -93
- package/build/index.d.ts +6 -2
- package/build/index.js +29 -39
- package/build/mutation-buffer.d.ts +3 -177
- package/build/mutation-buffer.js +162 -316
- package/build/native-engine.d.ts +6 -102
- package/build/native-engine.js +60 -141
- package/build/native-events.js +9 -18
- package/build/native-tree-host.d.ts +0 -21
- package/build/native-tree-host.js +15 -31
- package/build/node-events.d.ts +11 -0
- package/build/node-events.js +145 -0
- package/build/node-instance.d.ts +8 -0
- package/build/node-instance.js +168 -0
- package/build/node-props.d.ts +13 -0
- package/build/node-props.js +131 -0
- package/build/node-route.d.ts +2 -0
- package/build/node-route.js +151 -0
- package/build/node-style.d.ts +15 -0
- package/build/node-style.js +214 -0
- package/build/node-tree.d.ts +6 -0
- package/build/node-tree.js +159 -0
- package/build/node-types.d.ts +70 -0
- package/build/node-types.js +36 -0
- package/build/node.d.ts +7 -309
- package/build/node.js +9 -1564
- package/build/post-commit.js +3 -8
- package/build/process-aspect-ratio.js +3 -7
- package/build/process-background-longhands.js +10 -19
- package/build/process-filter.js +11 -19
- package/build/process-font-variant.js +3 -7
- package/build/registry.d.ts +0 -33
- package/build/registry.js +22 -57
- package/build/report-error.js +4 -18
- package/build/structured-style.d.ts +0 -9
- package/build/structured-style.js +16 -31
- package/build/styles.js +3 -6
- package/build/surface.d.ts +0 -26
- package/build/surface.js +29 -76
- package/build/text-input-state.js +4 -8
- package/build/touch-history.js +5 -11
- package/build/tree-host.d.ts +7 -270
- package/build/tree-host.js +63 -153
- package/build/view-config.js +17 -37
- package/cpp/SymbioteEngineBindings.cpp +19 -18
- package/cpp/SymbioteTree.cpp +81 -156
- package/cpp/SymbioteTree.h +6 -0
- package/package.json +2 -2
package/build/events/index.js
CHANGED
|
@@ -1,374 +1,217 @@
|
|
|
1
|
-
// Event normalization. Fabric delivers raw touch primitives to a single global
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// direct event in RN and is delivered only to its own target.
|
|
1
|
+
// Event normalization. Fabric delivers raw touch primitives to a single global handler with the
|
|
2
|
+
// `instanceHandle` (our `ISymbioteNode`) as the target, and there is no raw `press`: a tap is
|
|
3
|
+
// synthesized here from a touch sequence
|
|
4
|
+
// The pieces live in siblings, kept apart т.к. they answer different questions: `./names` what an
|
|
5
|
+
// event is called, `./delivery` who receives it, `./press` what a tap is made of, `./responder`
|
|
6
|
+
// who owns the gesture
|
|
8
7
|
import { dlog, isDebug } from '../debug.js';
|
|
9
8
|
import { runWrapped } from '../dispatch.js';
|
|
10
9
|
import { getSlot } from '../fabric.js';
|
|
11
|
-
import {
|
|
10
|
+
import { isSymbioteNode } from '../node.js';
|
|
12
11
|
import { registeredNativeEvent } from '../registry.js';
|
|
13
|
-
import { ancestorsOf, parentOf } from '../host-access.js';
|
|
14
|
-
import { setIsJSResponder } from '../imperative.js';
|
|
15
12
|
import { attachTouchHistory, recordTouchTrack, resetTouchHistory, touchHistory, } from '../touch-history.js';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
topFocus: 'focus',
|
|
22
|
-
topBlur: 'blur',
|
|
23
|
-
topChange: 'change',
|
|
24
|
-
topEndEditing: 'endEditing',
|
|
25
|
-
topSubmitEditing: 'submitEditing',
|
|
26
|
-
topKeyPress: 'keyPress',
|
|
27
|
-
};
|
|
28
|
-
const DIRECT_EVENTS = {
|
|
29
|
-
topLayout: 'layout',
|
|
30
|
-
topScroll: 'scroll',
|
|
31
|
-
topScrollBeginDrag: 'scrollBeginDrag',
|
|
32
|
-
topScrollEndDrag: 'scrollEndDrag',
|
|
33
|
-
topMomentumScrollBegin: 'momentumScrollBegin',
|
|
34
|
-
topMomentumScrollEnd: 'momentumScrollEnd',
|
|
35
|
-
topSelectionChange: 'selectionChange',
|
|
36
|
-
topContentSizeChange: 'contentSizeChange',
|
|
37
|
-
topLoadStart: 'loadStart',
|
|
38
|
-
topLoad: 'load',
|
|
39
|
-
topLoadEnd: 'loadEnd',
|
|
40
|
-
topError: 'error',
|
|
41
|
-
topProgress: 'progress',
|
|
42
|
-
topPartialLoad: 'partialLoad',
|
|
43
|
-
topRefresh: 'refresh',
|
|
44
|
-
topShow: 'show',
|
|
45
|
-
topRequestClose: 'requestClose',
|
|
46
|
-
topDismiss: 'dismiss',
|
|
47
|
-
topOrientationChange: 'orientationChange',
|
|
48
|
-
// Text glyph layout (onTextLayout) and the iOS status-bar-tap scroll-to-top.
|
|
49
|
-
topTextLayout: 'textLayout',
|
|
50
|
-
topScrollToTop: 'scrollToTop',
|
|
51
|
-
// Accessibility events from RN's base ViewConfig; any view can emit them.
|
|
52
|
-
// accessibilityAction fires on iOS + Android; the iOS-only three (accessibilityTap,
|
|
53
|
-
// magicTap, accessibilityEscape) have no Android producer, so they are inert there.
|
|
54
|
-
topAccessibilityAction: 'accessibilityAction',
|
|
55
|
-
topAccessibilityTap: 'accessibilityTap',
|
|
56
|
-
topMagicTap: 'magicTap',
|
|
57
|
-
topAccessibilityEscape: 'accessibilityEscape',
|
|
58
|
-
};
|
|
59
|
-
const TOUCH_START = 'topTouchStart';
|
|
60
|
-
const TOUCH_MOVE = 'topTouchMove';
|
|
61
|
-
const TOUCH_END = 'topTouchEnd';
|
|
62
|
-
const TOUCH_CANCEL = 'topTouchCancel';
|
|
63
|
-
const PRESS = 'press';
|
|
64
|
-
// Responder protocol (PanResponder / Touchable). RN's two-phase negotiation:
|
|
65
|
-
// every should-set is asked CAPTURE (root -> target) then BUBBLE (target -> root),
|
|
66
|
-
// and the first node returning true wins, on a touch START *and* on every MOVE,
|
|
67
|
-
// so a node can claim the responder mid-gesture. If someone already holds it, the
|
|
68
|
-
// incumbent is asked onResponderTerminationRequest; a true answer (or no listener)
|
|
69
|
-
// hands it over (terminate + grant), a false answer rejects the taker. Lifecycle
|
|
70
|
-
// events are direct (grant/start/move/end/release/terminate/reject). Listener names
|
|
71
|
-
// are post-`on` (onResponderMove -> 'responderMove').
|
|
72
|
-
const START_SHOULD_SET = 'startShouldSetResponder';
|
|
73
|
-
const START_SHOULD_SET_CAPTURE = 'startShouldSetResponderCapture';
|
|
74
|
-
const MOVE_SHOULD_SET = 'moveShouldSetResponder';
|
|
75
|
-
const MOVE_SHOULD_SET_CAPTURE = 'moveShouldSetResponderCapture';
|
|
76
|
-
const RESPONDER_GRANT = 'responderGrant';
|
|
77
|
-
const RESPONDER_REJECT = 'responderReject';
|
|
78
|
-
const RESPONDER_START = 'responderStart';
|
|
79
|
-
const RESPONDER_MOVE = 'responderMove';
|
|
80
|
-
const RESPONDER_END = 'responderEnd';
|
|
81
|
-
const RESPONDER_RELEASE = 'responderRelease';
|
|
82
|
-
const RESPONDER_TERMINATE = 'responderTerminate';
|
|
83
|
-
const RESPONDER_TERMINATION_REQUEST = 'responderTerminationRequest';
|
|
84
|
-
// Synthesized alongside press so Pressable can show pressed-state feedback: both
|
|
85
|
-
// fire on the node the touch STARTED on (the responder), pressOut on end/cancel.
|
|
86
|
-
const PRESS_IN = 'pressIn';
|
|
87
|
-
const PRESS_OUT = 'pressOut';
|
|
88
|
-
// Synthesized from a sustained hold so bare Text/View onLongPress fires without a
|
|
89
|
-
// native event (Pressable runs the same timer in JS). Default delay matches RN's
|
|
90
|
-
// Touchable (500ms); a fired long press suppresses the tap on release.
|
|
91
|
-
const LONG_PRESS = 'longPress';
|
|
92
|
-
const DEFAULT_LONG_PRESS_MS = 500;
|
|
93
|
-
// Pressability cancels the pending long press when the touch drifts past this many
|
|
94
|
-
// points from where it started (Pressability.DEFAULT_LONG_PRESS_DEACTIVATION_DISTANCE).
|
|
95
|
-
const LONG_PRESS_DEACTIVATION_DISTANCE = 10;
|
|
13
|
+
import { bubble, callOwnListener, deliverDirect, endsWithin, hasListenerInPath, } from './delivery.js';
|
|
14
|
+
import { BUBBLING_EVENTS, DEFAULT_LONG_PRESS_MS, DIRECT_EVENTS, LONG_PRESS, LONG_PRESS_DEACTIVATION_DISTANCE, PRESS, PRESS_IN, PRESS_OUT, RESPONDER_END, RESPONDER_MOVE, RESPONDER_RELEASE, RESPONDER_START, RESPONDER_TERMINATE, TOUCH_CANCEL, TOUCH_END, TOUCH_MOVE, TOUCH_START, } from './names.js';
|
|
15
|
+
import { activePresses, clearLongPress, findActivePress, hasRemainingTouchWithin, readTouchPoint, takeAllPresses, takeEndedPresses, } from './press.js';
|
|
16
|
+
import { clearResponder, heldResponder, negotiateResponder, releaseResponderToNative, } from './responder.js';
|
|
17
|
+
// Fabric calls one handler for every surface, so the registration happens once per runtime
|
|
96
18
|
let installed = false;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// undefined when nobody claimed it. Receives move and release/terminate.
|
|
102
|
-
let currentResponder;
|
|
103
|
-
function clearLongPress(press) {
|
|
104
|
-
if (press.longPressTimer !== undefined) {
|
|
105
|
-
clearTimeout(press.longPressTimer);
|
|
19
|
+
function armLongPress(press, nativeEvent) {
|
|
20
|
+
press.longPressTimer = setTimeout(() => {
|
|
21
|
+
if (!activePresses.has(press))
|
|
22
|
+
return;
|
|
106
23
|
press.longPressTimer = undefined;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
activePresses.clear();
|
|
112
|
-
for (const press of presses)
|
|
113
|
-
clearLongPress(press);
|
|
114
|
-
return presses;
|
|
24
|
+
press.longPressFired = true;
|
|
25
|
+
dlog('synthesized longPress -> dispatch');
|
|
26
|
+
runWrapped(() => bubble(press.owner, LONG_PRESS, nativeEvent));
|
|
27
|
+
}, DEFAULT_LONG_PRESS_MS);
|
|
115
28
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return false;
|
|
161
|
-
for (const touch of touches) {
|
|
162
|
-
if (!isRecord(touch))
|
|
163
|
-
continue;
|
|
164
|
-
const target = touch.target;
|
|
165
|
-
if (isSymbioteNode(target) && endsWithin(target, owner))
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
// Whether any node from `target` up to the root listens for `listenerName`, used to
|
|
171
|
-
// arm the long-press timer only when a handler would actually receive it.
|
|
172
|
-
function hasListenerInPath(target, listenerName) {
|
|
173
|
-
for (let node = target; node; node = parentOf(node)) {
|
|
174
|
-
if (node.listeners?.has(listenerName) === true)
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
// Invoke one node's own listener (no bubbling) and hand back its return value, so
|
|
180
|
-
// the responder negotiation can read the boolean from onStartShouldSetResponder.
|
|
181
|
-
function callOwnListener(node, listenerName, nativeEvent) {
|
|
182
|
-
const listener = node.listeners?.get(listenerName);
|
|
183
|
-
if (!listener)
|
|
184
|
-
return undefined;
|
|
185
|
-
return listener({
|
|
186
|
-
type: listenerName,
|
|
187
|
-
target: node,
|
|
188
|
-
currentTarget: node,
|
|
189
|
-
nativeEvent,
|
|
190
|
-
stopPropagation: () => { },
|
|
29
|
+
function onTouchStart(target, nativeEvent) {
|
|
30
|
+
if (isDebug()) {
|
|
31
|
+
dlog(`event ${TOUCH_START} on ${target.component}`);
|
|
32
|
+
// A responder surviving into a one-touch start lost its end or cancel. If it is an ancestor of
|
|
33
|
+
// the target, this whole tap goes to it and the release finally clears it
|
|
34
|
+
if (heldResponder() !== undefined)
|
|
35
|
+
dlog(`touchStart while ${heldResponder()?.component} still holds it`);
|
|
36
|
+
}
|
|
37
|
+
// Update the touch bank, then attach it so responder handlers read each touch's own
|
|
38
|
+
// previous-to-current delta. RN records before dispatch too
|
|
39
|
+
recordTouchTrack('start', nativeEvent);
|
|
40
|
+
attachTouchHistory(nativeEvent);
|
|
41
|
+
const canJoinExistingPress = Array.isArray(nativeEvent.touches) && nativeEvent.touches.length > 1;
|
|
42
|
+
const joinedPress = canJoinExistingPress
|
|
43
|
+
? findActivePress(target)
|
|
44
|
+
: undefined;
|
|
45
|
+
// A one-touch or identifier-less frame starts a new physical gesture, so any surviving press is
|
|
46
|
+
// stale (its end or cancel was lost) and gets released instead of suppressing this start
|
|
47
|
+
const stalePresses = canJoinExistingPress ? [] : takeAllPresses();
|
|
48
|
+
let startedPress;
|
|
49
|
+
if (joinedPress === undefined) {
|
|
50
|
+
startedPress = {
|
|
51
|
+
owner: target,
|
|
52
|
+
longPressTimer: undefined,
|
|
53
|
+
longPressFired: false,
|
|
54
|
+
longPressStart: readTouchPoint(nativeEvent),
|
|
55
|
+
};
|
|
56
|
+
activePresses.add(startedPress);
|
|
57
|
+
if (hasListenerInPath(target, LONG_PRESS))
|
|
58
|
+
armLongPress(startedPress, nativeEvent);
|
|
59
|
+
}
|
|
60
|
+
runWrapped(() => {
|
|
61
|
+
for (const stale of stalePresses)
|
|
62
|
+
bubble(stale.owner, PRESS_OUT, nativeEvent);
|
|
63
|
+
if (startedPress)
|
|
64
|
+
bubble(startedPress.owner, PRESS_IN, nativeEvent);
|
|
65
|
+
else
|
|
66
|
+
dlog('pressIn retained (another touch joined the active press)');
|
|
67
|
+
// A View can be both a Pressable and a PanResponder target, so the negotiation runs alongside
|
|
68
|
+
// press synthesis rather than instead of it
|
|
69
|
+
negotiateResponder(target, 'start', nativeEvent);
|
|
70
|
+
const responder = heldResponder();
|
|
71
|
+
if (responder)
|
|
72
|
+
callOwnListener(responder, RESPONDER_START, nativeEvent);
|
|
191
73
|
});
|
|
192
74
|
}
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
da--;
|
|
223
|
-
}
|
|
224
|
-
while (nb && db > da) {
|
|
225
|
-
nb = parentOf(nb);
|
|
226
|
-
db--;
|
|
227
|
-
}
|
|
228
|
-
while (na && nb) {
|
|
229
|
-
if (na === nb)
|
|
230
|
-
return na;
|
|
231
|
-
na = parentOf(na);
|
|
232
|
-
nb = parentOf(nb);
|
|
233
|
-
}
|
|
234
|
-
return undefined;
|
|
75
|
+
// Only the press owning this move may lose its long-press timer: movement on an unrelated
|
|
76
|
+
// simultaneously held Pressable must not disturb another target's clock
|
|
77
|
+
function cancelLongPressOnDrift(target, nativeEvent) {
|
|
78
|
+
const movedPress = findActivePress(target);
|
|
79
|
+
if (movedPress?.longPressTimer === undefined || !movedPress.longPressStart)
|
|
80
|
+
return;
|
|
81
|
+
const here = readTouchPoint(nativeEvent);
|
|
82
|
+
if (!here)
|
|
83
|
+
return;
|
|
84
|
+
const dx = here.x - movedPress.longPressStart.x;
|
|
85
|
+
const dy = here.y - movedPress.longPressStart.y;
|
|
86
|
+
if (Math.hypot(dx, dy) > LONG_PRESS_DEACTIVATION_DISTANCE) {
|
|
87
|
+
dlog('longPress cancelled (moved past deactivation distance)');
|
|
88
|
+
clearLongPress(movedPress);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function onTouchMove(target, nativeEvent) {
|
|
92
|
+
recordTouchTrack('move', nativeEvent);
|
|
93
|
+
attachTouchHistory(nativeEvent);
|
|
94
|
+
cancelLongPressOnDrift(target, nativeEvent);
|
|
95
|
+
runWrapped(() => {
|
|
96
|
+
// Re-negotiate first: a node can claim the responder mid-gesture through
|
|
97
|
+
// `onMoveShouldSetResponder`, and the responder itself is skipped
|
|
98
|
+
negotiateResponder(target, 'move', nativeEvent);
|
|
99
|
+
// The only consumer of a move is the responder; without one RN drops it too
|
|
100
|
+
const responder = heldResponder();
|
|
101
|
+
if (responder)
|
|
102
|
+
callOwnListener(responder, RESPONDER_MOVE, nativeEvent);
|
|
103
|
+
});
|
|
235
104
|
}
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
callOwnListener(path[i], captureName, nativeEvent) === true) {
|
|
246
|
-
return path[i];
|
|
105
|
+
// Each target completes independently. An honest tap ends inside that target, while `pressOut`
|
|
106
|
+
// always releases the pressed state, a drag-away end included
|
|
107
|
+
function completePress(press, target, nativeEvent) {
|
|
108
|
+
if (endsWithin(target, press.owner)) {
|
|
109
|
+
if (press.longPressFired)
|
|
110
|
+
dlog('press suppressed (longPress already fired)');
|
|
111
|
+
else {
|
|
112
|
+
dlog('event press -> dispatch');
|
|
113
|
+
bubble(press.owner, PRESS, nativeEvent);
|
|
247
114
|
}
|
|
248
115
|
}
|
|
249
|
-
|
|
250
|
-
if (node !== skip &&
|
|
251
|
-
callOwnListener(node, bubbleName, nativeEvent) === true)
|
|
252
|
-
return node;
|
|
253
|
-
}
|
|
254
|
-
return undefined;
|
|
116
|
+
bubble(press.owner, PRESS_OUT, nativeEvent);
|
|
255
117
|
}
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// `onStartShouldSetResponder` returns true, the native UIScrollView never learns the gesture was
|
|
262
|
-
// claimed, and every subsequent move arrives as `topScroll` instead of `topTouchMove` — so the
|
|
263
|
-
// negotiation never even gets a move to grant on. Device-diagnosed 2026-09-08 on a PanResponder
|
|
264
|
-
// drag box inside the canary's ScrollView: `startShouldSet -> true` followed by
|
|
265
|
-
// `topScrollBeginDrag` and twenty `topScroll`, with no grant and no move.
|
|
266
|
-
//
|
|
267
|
-
// `blockNativeResponder` is the taker's own `onResponderGrant` return, exactly as RN reads it.
|
|
268
|
-
function handOverNativeResponder(from, to, blockNativeResponder) {
|
|
269
|
-
if (isDebug()) {
|
|
270
|
-
dlog(`setIsJSResponder from=${from === undefined ? 'none' : 'yes'} ` +
|
|
271
|
-
`to=${to === undefined ? 'none' : 'yes'} block=${blockNativeResponder}`);
|
|
272
|
-
}
|
|
273
|
-
if (from !== undefined)
|
|
274
|
-
setIsJSResponder(from, false, blockNativeResponder);
|
|
275
|
-
if (to !== undefined)
|
|
276
|
-
setIsJSResponder(to, true, blockNativeResponder);
|
|
277
|
-
}
|
|
278
|
-
// Whether the taker asked native to stand down. RN reads this off the grant dispatch's return;
|
|
279
|
-
// an absent listener means no claim, which is RN's `blockNativeResponder || false`.
|
|
280
|
-
function blocksNative(result) {
|
|
281
|
-
return result === true;
|
|
282
|
-
}
|
|
283
|
-
// Negotiate (or re-negotiate) the responder for a touch start/move. If nobody holds
|
|
284
|
-
// it, the winner is granted. If someone does, the incumbent is asked to relinquish
|
|
285
|
-
// via onResponderTerminationRequest (absent listener = implicit yes); on yes it is
|
|
286
|
-
// terminated and the taker granted, on no the taker is rejected.
|
|
287
|
-
function negotiateResponder(target, phase, nativeEvent) {
|
|
288
|
-
// With no responder, ask the full path from the touch target. With one, RN scopes
|
|
289
|
-
// the walk to the lowest common ancestor of responder+target upward (never below
|
|
290
|
-
// the responder) and skips the deepest node when it IS the responder (Responder-
|
|
291
|
-
// EventPlugin.setResponderAndExtractTransfer). At touch start currentResponder is
|
|
292
|
-
// cleared, so this collapses to the plain target->root start walk.
|
|
293
|
-
//
|
|
294
|
-
// ONE WALK, and the common case never needs a second. `pathToRoot` crosses the host boundary per
|
|
295
|
-
// level, and the previous shape paid `lowestCommonAncestor` — `depthOf(a)` plus `depthOf(b)` plus
|
|
296
|
-
// a lockstep climb — BEFORE walking again from the answer. Measured on a depth-8 chain: 19
|
|
297
|
-
// crossings per move, of which 18 were the ancestor search, on a path that runs 60 times a second
|
|
298
|
-
// for the whole of a drag (`__tests__/touch-crossing-cost.test.ts`).
|
|
299
|
-
//
|
|
300
|
-
// The scope RN computes is the lowest common ancestor of responder and target, and while a finger
|
|
301
|
-
// stays inside the view that claimed it — which is what a drag IS — the responder is an ANCESTOR
|
|
302
|
-
// of the target, so the LCA is the responder itself and it is already in the target's own path.
|
|
303
|
-
// An index lookup answers it with nothing crossed, and the scoped path is that path's suffix.
|
|
304
|
-
// The genuine cross-subtree case (the finger left the responder's subtree) still falls back to
|
|
305
|
-
// the full search, and it is a rare frame rather than every frame.
|
|
306
|
-
const targetPath = pathToRoot(target);
|
|
307
|
-
const heldAt = currentResponder === undefined ? -1 : targetPath.indexOf(currentResponder);
|
|
308
|
-
const from = currentResponder === undefined
|
|
309
|
-
? target
|
|
310
|
-
: heldAt >= 0
|
|
311
|
-
? currentResponder
|
|
312
|
-
: lowestCommonAncestor(currentResponder, target);
|
|
313
|
-
if (!from)
|
|
118
|
+
// `responderEnd` fires on every finger leaving; the FINAL name (release on an end, terminate on a
|
|
119
|
+
// cancel) only once no touch remains inside the responder
|
|
120
|
+
function finishResponder(responder, finalName, isFinal, nativeEvent) {
|
|
121
|
+
callOwnListener(responder, RESPONDER_END, nativeEvent);
|
|
122
|
+
if (!isFinal)
|
|
314
123
|
return;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
124
|
+
callOwnListener(responder, finalName, nativeEvent);
|
|
125
|
+
releaseResponderToNative(responder);
|
|
126
|
+
}
|
|
127
|
+
function onTouchEnd(target, nativeEvent) {
|
|
128
|
+
recordTouchTrack('end', nativeEvent);
|
|
129
|
+
attachTouchHistory(nativeEvent);
|
|
130
|
+
const hadActivePress = activePresses.size > 0;
|
|
131
|
+
const completedPresses = takeEndedPresses(nativeEvent);
|
|
132
|
+
const responder = heldResponder();
|
|
133
|
+
// RN releases the responder only when no remaining touch still down started inside it, so
|
|
134
|
+
// lifting ONE finger of a multi-touch gesture must not release. `onResponderEnd` still fires
|
|
135
|
+
const releases = responder !== undefined && !hasRemainingTouchWithin(responder, nativeEvent);
|
|
136
|
+
if (releases)
|
|
137
|
+
clearResponder();
|
|
138
|
+
runWrapped(() => {
|
|
139
|
+
for (const press of completedPresses)
|
|
140
|
+
completePress(press, target, nativeEvent);
|
|
141
|
+
if (!hadActivePress) {
|
|
142
|
+
if (isDebug())
|
|
143
|
+
dlog(`event ${TOUCH_END} ignored (no matching start)`);
|
|
329
144
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
145
|
+
else if (completedPresses.length === 0)
|
|
146
|
+
dlog('press retained (another touch remains inside its owner)');
|
|
147
|
+
if (!responder)
|
|
148
|
+
return;
|
|
149
|
+
finishResponder(responder, RESPONDER_RELEASE, releases, nativeEvent);
|
|
150
|
+
if (!releases)
|
|
151
|
+
dlog('responderEnd without release (touches remain inside responder)');
|
|
152
|
+
});
|
|
153
|
+
// Once no touch is down, clear the bank so the next gesture starts clean
|
|
154
|
+
if (touchHistory.numberActiveTouches === 0)
|
|
155
|
+
resetTouchHistory();
|
|
156
|
+
}
|
|
157
|
+
// Android: a ScrollView whose scroller has not finished (fling tail, spring-back) intercepts the
|
|
158
|
+
// next down natively, and the tap reaches JS as start plus cancel with no press
|
|
159
|
+
function onTouchCancel(target, nativeEvent) {
|
|
160
|
+
if (isDebug())
|
|
161
|
+
dlog(`event ${TOUCH_CANCEL} on ${target.component}`);
|
|
162
|
+
recordTouchTrack('end', nativeEvent);
|
|
163
|
+
attachTouchHistory(nativeEvent);
|
|
164
|
+
// A cancel is scoped to the fingers removed from `touches`, just like an end, so an unrelated
|
|
165
|
+
// Pressable, or another finger under the same owner, keeps its press and long-press state
|
|
166
|
+
const cancelledPresses = takeEndedPresses(nativeEvent);
|
|
167
|
+
const responder = heldResponder();
|
|
168
|
+
const terminates = responder !== undefined && !hasRemainingTouchWithin(responder, nativeEvent);
|
|
169
|
+
if (terminates)
|
|
170
|
+
clearResponder();
|
|
171
|
+
runWrapped(() => {
|
|
172
|
+
for (const press of cancelledPresses)
|
|
173
|
+
bubble(press.owner, PRESS_OUT, nativeEvent);
|
|
174
|
+
if (!responder)
|
|
175
|
+
return;
|
|
176
|
+
finishResponder(responder, RESPONDER_TERMINATE, terminates, nativeEvent);
|
|
177
|
+
});
|
|
178
|
+
if (touchHistory.numberActiveTouches === 0)
|
|
179
|
+
resetTouchHistory();
|
|
180
|
+
}
|
|
181
|
+
const TOUCH_HANDLERS = {
|
|
182
|
+
[TOUCH_START]: onTouchStart,
|
|
183
|
+
[TOUCH_MOVE]: onTouchMove,
|
|
184
|
+
[TOUCH_END]: onTouchEnd,
|
|
185
|
+
[TOUCH_CANCEL]: onTouchCancel,
|
|
186
|
+
};
|
|
187
|
+
// A third-party Fabric view declares its own events, which the built-in tables do not know, so the
|
|
188
|
+
// registry answers keyed by the node's own component
|
|
189
|
+
function deliverNonTouch(target, topLevelType, nativeEvent) {
|
|
190
|
+
const direct = DIRECT_EVENTS[topLevelType];
|
|
191
|
+
if (direct !== undefined) {
|
|
339
192
|
if (isDebug())
|
|
340
|
-
dlog(`
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return;
|
|
193
|
+
dlog(`event ${topLevelType} -> ${direct} (direct)`);
|
|
194
|
+
runWrapped(() => deliverDirect(target, direct, nativeEvent));
|
|
195
|
+
return true;
|
|
344
196
|
}
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
// only an explicit non-true answer keeps the incumbent and rejects the taker.
|
|
348
|
-
const guarded = incumbent.listeners?.has(RESPONDER_TERMINATION_REQUEST) === true;
|
|
349
|
-
const allowed = !guarded ||
|
|
350
|
-
callOwnListener(incumbent, RESPONDER_TERMINATION_REQUEST, nativeEvent) ===
|
|
351
|
-
true;
|
|
352
|
-
if (allowed) {
|
|
353
|
-
// RN's transfer order (setResponderAndExtractTransfer): grant the TAKER first, then
|
|
354
|
-
// terminate the incumbent. RN dispatches grant ahead of the terminationRequest too,
|
|
355
|
-
// purely to read the taker's block-native return; we have no native surface to
|
|
356
|
-
// block, so on the REJECT path firing a grant the taker never keeps would be a
|
|
357
|
-
// visible no-op event with no behavioral counterpart. We therefore fire grant
|
|
358
|
-
// before terminate on the consent path (matching RN's grant<terminate ordering) and
|
|
359
|
-
// omit it on reject; the consent OUTCOME is unchanged either way.
|
|
197
|
+
const bubbling = BUBBLING_EVENTS[topLevelType];
|
|
198
|
+
if (bubbling !== undefined) {
|
|
360
199
|
if (isDebug())
|
|
361
|
-
dlog(`
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
currentResponder = wants;
|
|
365
|
-
handOverNativeResponder(incumbent, wants, blocksNative(granted));
|
|
200
|
+
dlog(`event ${topLevelType} -> ${bubbling} (bubble)`);
|
|
201
|
+
runWrapped(() => bubble(target, bubbling, nativeEvent));
|
|
202
|
+
return true;
|
|
366
203
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
204
|
+
const registered = registeredNativeEvent(target.component, topLevelType);
|
|
205
|
+
if (registered === undefined)
|
|
206
|
+
return false;
|
|
207
|
+
if (isDebug()) {
|
|
208
|
+
const phase = registered.direct ? 'direct' : 'bubble';
|
|
209
|
+
dlog(`event ${topLevelType} -> ${registered.listener} (${phase}, registered)`);
|
|
371
210
|
}
|
|
211
|
+
runWrapped(() => registered.direct
|
|
212
|
+
? deliverDirect(target, registered.listener, nativeEvent)
|
|
213
|
+
: bubble(target, registered.listener, nativeEvent));
|
|
214
|
+
return true;
|
|
372
215
|
}
|
|
373
216
|
export function installEventHandler() {
|
|
374
217
|
if (installed)
|
|
@@ -377,321 +220,17 @@ export function installEventHandler() {
|
|
|
377
220
|
getSlot().registerEventHandler((instanceHandle, topLevelType, nativeEvent) => {
|
|
378
221
|
if (!isSymbioteNode(instanceHandle))
|
|
379
222
|
return;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (isDebug()) {
|
|
384
|
-
dlog(`event ${TOUCH_START} on ${instanceHandle.component}`);
|
|
385
|
-
// A responder surviving into a one-touch start lost its end/cancel. If it is an ancestor
|
|
386
|
-
// of the target, this whole tap goes to it and the release finally clears it.
|
|
387
|
-
if (currentResponder !== undefined)
|
|
388
|
-
dlog(`touchStart while ${currentResponder.component} still holds the responder`);
|
|
389
|
-
}
|
|
390
|
-
// Update the touch bank, then attach it so responder handlers (PanResponder)
|
|
391
|
-
// read each touch's own previous->current delta; RN records before dispatch.
|
|
392
|
-
recordTouchTrack('start', nativeEvent);
|
|
393
|
-
attachTouchHistory(nativeEvent);
|
|
394
|
-
const canJoinExistingPress = Array.isArray(nativeEvent.touches) && nativeEvent.touches.length > 1;
|
|
395
|
-
const joinedPress = canJoinExistingPress
|
|
396
|
-
? findActivePress(instanceHandle)
|
|
397
|
-
: undefined;
|
|
398
|
-
// A one-touch or identifier-less frame starts a new physical gesture. Any surviving press
|
|
399
|
-
// is stale (its end/cancel was lost), so release it instead of letting it suppress this start.
|
|
400
|
-
const stalePresses = canJoinExistingPress ? [] : takeAllPresses();
|
|
401
|
-
let startedPress;
|
|
402
|
-
if (joinedPress === undefined) {
|
|
403
|
-
startedPress = {
|
|
404
|
-
owner: instanceHandle,
|
|
405
|
-
longPressTimer: undefined,
|
|
406
|
-
longPressFired: false,
|
|
407
|
-
longPressStart: readTouchPoint(nativeEvent),
|
|
408
|
-
};
|
|
409
|
-
activePresses.add(startedPress);
|
|
410
|
-
if (hasListenerInPath(instanceHandle, LONG_PRESS)) {
|
|
411
|
-
const press = startedPress;
|
|
412
|
-
press.longPressTimer = setTimeout(() => {
|
|
413
|
-
if (!activePresses.has(press))
|
|
414
|
-
return;
|
|
415
|
-
press.longPressTimer = undefined;
|
|
416
|
-
press.longPressFired = true;
|
|
417
|
-
dlog('synthesized longPress -> dispatch');
|
|
418
|
-
runWrapped(() => bubble(press.owner, LONG_PRESS, nativeEvent));
|
|
419
|
-
}, DEFAULT_LONG_PRESS_MS);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
runWrapped(() => {
|
|
423
|
-
for (const stale of stalePresses)
|
|
424
|
-
bubble(stale.owner, PRESS_OUT, nativeEvent);
|
|
425
|
-
if (startedPress)
|
|
426
|
-
bubble(startedPress.owner, PRESS_IN, nativeEvent);
|
|
427
|
-
else
|
|
428
|
-
dlog('pressIn retained (another touch joined the active press)');
|
|
429
|
-
// Responder negotiation runs alongside press synthesis: a View can be both
|
|
430
|
-
// a Pressable (press) and a PanResponder target (responder).
|
|
431
|
-
negotiateResponder(instanceHandle, 'start', nativeEvent);
|
|
432
|
-
// onResponderStart is a direct event to whoever now holds the responder.
|
|
433
|
-
if (currentResponder)
|
|
434
|
-
callOwnListener(currentResponder, RESPONDER_START, nativeEvent);
|
|
435
|
-
});
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
if (topLevelType === TOUCH_MOVE) {
|
|
439
|
-
recordTouchTrack('move', nativeEvent);
|
|
440
|
-
attachTouchHistory(nativeEvent);
|
|
441
|
-
// Only the press owning this move can lose its long-press timer; movement on an unrelated
|
|
442
|
-
// simultaneously held Pressable must not disturb another target's clock.
|
|
443
|
-
const movedPress = findActivePress(instanceHandle);
|
|
444
|
-
if (movedPress?.longPressTimer !== undefined &&
|
|
445
|
-
movedPress.longPressStart) {
|
|
446
|
-
const here = readTouchPoint(nativeEvent);
|
|
447
|
-
if (here) {
|
|
448
|
-
const dx = here.x - movedPress.longPressStart.x;
|
|
449
|
-
const dy = here.y - movedPress.longPressStart.y;
|
|
450
|
-
if (Math.hypot(dx, dy) > LONG_PRESS_DEACTIVATION_DISTANCE) {
|
|
451
|
-
dlog('longPress cancelled (moved past deactivation distance)');
|
|
452
|
-
clearLongPress(movedPress);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
runWrapped(() => {
|
|
457
|
-
// Re-negotiate first: a node can claim the responder mid-gesture via
|
|
458
|
-
// onMoveShouldSetResponder (the responder itself is skipped, see negotiate).
|
|
459
|
-
negotiateResponder(instanceHandle, 'move', nativeEvent);
|
|
460
|
-
// The only consumer of a move is the responder; without one, RN drops it too.
|
|
461
|
-
if (currentResponder)
|
|
462
|
-
callOwnListener(currentResponder, RESPONDER_MOVE, nativeEvent);
|
|
463
|
-
});
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
if (topLevelType === TOUCH_END) {
|
|
467
|
-
recordTouchTrack('end', nativeEvent);
|
|
468
|
-
attachTouchHistory(nativeEvent);
|
|
469
|
-
const hadActivePress = activePresses.size > 0;
|
|
470
|
-
const completedPresses = [];
|
|
471
|
-
for (const press of activePresses) {
|
|
472
|
-
if (hasRemainingTouchWithin(press.owner, nativeEvent))
|
|
473
|
-
continue;
|
|
474
|
-
activePresses.delete(press);
|
|
475
|
-
clearLongPress(press);
|
|
476
|
-
completedPresses.push(press);
|
|
477
|
-
}
|
|
478
|
-
const responder = currentResponder;
|
|
479
|
-
// RN releases (and clears) the responder only when no remaining touch still down
|
|
480
|
-
// started inside it; lifting ONE finger in a multi-touch gesture must NOT release.
|
|
481
|
-
// onResponderEnd still fires on every finger-up. (ResponderEventPlugin: responderEnd
|
|
482
|
-
// is unconditional, responderRelease is gated on noResponderTouches.)
|
|
483
|
-
const releases = responder !== undefined &&
|
|
484
|
-
!hasRemainingTouchWithin(responder, nativeEvent);
|
|
485
|
-
if (releases)
|
|
486
|
-
currentResponder = undefined;
|
|
487
|
-
runWrapped(() => {
|
|
488
|
-
for (const press of completedPresses) {
|
|
489
|
-
// Each target completes independently. An honest tap ends inside that target; pressOut
|
|
490
|
-
// always releases its pressed state, including a drag-away end.
|
|
491
|
-
if (endsWithin(instanceHandle, press.owner)) {
|
|
492
|
-
if (press.longPressFired) {
|
|
493
|
-
dlog('press suppressed (longPress already fired)');
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
dlog('event press -> dispatch');
|
|
497
|
-
bubble(press.owner, PRESS, nativeEvent);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
bubble(press.owner, PRESS_OUT, nativeEvent);
|
|
501
|
-
}
|
|
502
|
-
if (!hadActivePress) {
|
|
503
|
-
if (isDebug())
|
|
504
|
-
dlog(`event ${TOUCH_END} ignored (no matching start)`);
|
|
505
|
-
}
|
|
506
|
-
else if (completedPresses.length === 0) {
|
|
507
|
-
dlog('press retained (another touch remains inside its owner)');
|
|
508
|
-
}
|
|
509
|
-
// onResponderEnd fires on every finger-up; onResponderRelease (the final
|
|
510
|
-
// release) only when the last responder touch lifted.
|
|
511
|
-
if (responder) {
|
|
512
|
-
callOwnListener(responder, RESPONDER_END, nativeEvent);
|
|
513
|
-
if (releases) {
|
|
514
|
-
callOwnListener(responder, RESPONDER_RELEASE, nativeEvent);
|
|
515
|
-
handOverNativeResponder(responder, undefined, false);
|
|
516
|
-
}
|
|
517
|
-
else
|
|
518
|
-
dlog('responderEnd without release (touches remain inside responder)');
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
// Once no touch is down, clear the bank so the next gesture starts clean.
|
|
522
|
-
if (touchHistory.numberActiveTouches === 0)
|
|
523
|
-
resetTouchHistory();
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
if (topLevelType === TOUCH_CANCEL) {
|
|
527
|
-
// Android: a ScrollView whose scroller has not finished (fling tail, spring-back) intercepts
|
|
528
|
-
// the next down natively, and the tap reaches JS as start + cancel with no press.
|
|
529
|
-
if (isDebug())
|
|
530
|
-
dlog(`event ${TOUCH_CANCEL} on ${instanceHandle.component}`);
|
|
531
|
-
recordTouchTrack('end', nativeEvent);
|
|
532
|
-
attachTouchHistory(nativeEvent);
|
|
533
|
-
// A cancel is scoped to the finger(s) removed from `touches`, just like an end. An unrelated
|
|
534
|
-
// Pressable—or another finger under the same owner—must keep its press and long-press state.
|
|
535
|
-
const cancelledPresses = [];
|
|
536
|
-
for (const press of activePresses) {
|
|
537
|
-
if (hasRemainingTouchWithin(press.owner, nativeEvent))
|
|
538
|
-
continue;
|
|
539
|
-
activePresses.delete(press);
|
|
540
|
-
clearLongPress(press);
|
|
541
|
-
cancelledPresses.push(press);
|
|
542
|
-
}
|
|
543
|
-
const responder = currentResponder;
|
|
544
|
-
const terminatesResponder = responder !== undefined &&
|
|
545
|
-
!hasRemainingTouchWithin(responder, nativeEvent);
|
|
546
|
-
if (terminatesResponder)
|
|
547
|
-
currentResponder = undefined;
|
|
548
|
-
runWrapped(() => {
|
|
549
|
-
for (const press of cancelledPresses)
|
|
550
|
-
bubble(press.owner, PRESS_OUT, nativeEvent);
|
|
551
|
-
if (responder) {
|
|
552
|
-
// Like touch-end, every finger leaving emits responderEnd. Termination is final only
|
|
553
|
-
// when no touch remains inside the responder.
|
|
554
|
-
callOwnListener(responder, RESPONDER_END, nativeEvent);
|
|
555
|
-
if (terminatesResponder) {
|
|
556
|
-
callOwnListener(responder, RESPONDER_TERMINATE, nativeEvent);
|
|
557
|
-
handOverNativeResponder(responder, undefined, false);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
if (touchHistory.numberActiveTouches === 0)
|
|
562
|
-
resetTouchHistory();
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
const direct = DIRECT_EVENTS[topLevelType];
|
|
566
|
-
if (direct !== undefined) {
|
|
567
|
-
if (isDebug()) {
|
|
568
|
-
dlog(`event ${topLevelType} -> ${direct} (direct)`);
|
|
569
|
-
}
|
|
570
|
-
runWrapped(() => deliverDirect(instanceHandle, direct, nativeEvent));
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
const bubbling = BUBBLING_EVENTS[topLevelType];
|
|
574
|
-
if (bubbling !== undefined) {
|
|
575
|
-
if (isDebug()) {
|
|
576
|
-
dlog(`event ${topLevelType} -> ${bubbling} (bubble)`);
|
|
577
|
-
}
|
|
578
|
-
runWrapped(() => bubble(instanceHandle, bubbling, nativeEvent));
|
|
223
|
+
const touch = TOUCH_HANDLERS[topLevelType];
|
|
224
|
+
if (touch !== undefined) {
|
|
225
|
+
touch(instanceHandle, nativeEvent);
|
|
579
226
|
return;
|
|
580
227
|
}
|
|
581
|
-
|
|
582
|
-
// built-in tables above don't know them, so fall back to the registry, keyed by
|
|
583
|
-
// the node's own component. `direct` events fire only on their target, the rest
|
|
584
|
-
// bubble: same split as the built-ins.
|
|
585
|
-
const registered = registeredNativeEvent(instanceHandle.component, topLevelType);
|
|
586
|
-
if (registered !== undefined) {
|
|
587
|
-
if (isDebug()) {
|
|
588
|
-
const phase = registered.direct ? 'direct' : 'bubble';
|
|
589
|
-
dlog(`event ${topLevelType} -> ${registered.listener} (${phase}, registered)`);
|
|
590
|
-
}
|
|
591
|
-
runWrapped(() => registered.direct
|
|
592
|
-
? deliverDirect(instanceHandle, registered.listener, nativeEvent)
|
|
593
|
-
: bubble(instanceHandle, registered.listener, nativeEvent));
|
|
228
|
+
if (deliverNonTouch(instanceHandle, topLevelType, nativeEvent))
|
|
594
229
|
return;
|
|
595
|
-
|
|
596
|
-
//
|
|
597
|
-
// config. A permanent diagnostic seam: if a native view fires something we drop
|
|
598
|
-
// on the floor (an event the ViewConfig didn't surface, or a name mismatch),
|
|
599
|
-
// this is where it shows up. Keeps "the handler silently did nothing" debuggable.
|
|
230
|
+
// Nothing claimed this event, neither a built-in table nor the view's derived config. A
|
|
231
|
+
// permanent seam: a native view firing something we drop on the floor shows up here
|
|
600
232
|
if (isDebug()) {
|
|
601
233
|
dlog(`event ${topLevelType} UNMATCHED on ${instanceHandle.component} (dropped)`);
|
|
602
234
|
}
|
|
603
235
|
});
|
|
604
236
|
}
|
|
605
|
-
// A press is honest only if the touch ends on the node it started on, or a
|
|
606
|
-
// descendant of it: walk parent pointers up from the end target looking for the
|
|
607
|
-
// start target. The start node may have been unmounted mid-touch (parent pointer
|
|
608
|
-
// cleared); the walk simply runs out and returns false, no throw.
|
|
609
|
-
function endsWithin(endTarget, start) {
|
|
610
|
-
let node = endTarget;
|
|
611
|
-
while (node) {
|
|
612
|
-
if (node === start)
|
|
613
|
-
return true;
|
|
614
|
-
node = parentOf(node);
|
|
615
|
-
}
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
// Two-phase delivery, mirroring RN's accumulateTwoPhaseDispatches (legacy-events/
|
|
619
|
-
// EventPropagators): CAPTURE root -> target first, invoking each node's
|
|
620
|
-
// `<EventName>Capture` listener, then BUBBLE target -> root invoking the plain
|
|
621
|
-
// listener. The same event object semantics apply to both passes; a stopPropagation
|
|
622
|
-
// in capture halts before bubble ever runs. `target` stays the original node;
|
|
623
|
-
// `currentTarget` tracks whose listener runs.
|
|
624
|
-
function bubble(target, listenerName, nativeEvent) {
|
|
625
|
-
let stopped = false;
|
|
626
|
-
const stopPropagation = () => {
|
|
627
|
-
stopped = true;
|
|
628
|
-
};
|
|
629
|
-
// Capture phase: root -> target. RN gathers captured listeners first (the
|
|
630
|
-
// `<EventName>Capture` registration), so on*Capture handlers fire ahead of the
|
|
631
|
-
// bubble pass. The path is built target -> root, then walked in reverse to get
|
|
632
|
-
// root -> target without a second allocation.
|
|
633
|
-
const captureName = `${listenerName}Capture`;
|
|
634
|
-
const path = pathToRoot(target);
|
|
635
|
-
for (let i = path.length - 1; i >= 0; i--) {
|
|
636
|
-
const node = path[i];
|
|
637
|
-
// Anchors (Angular's #anchor component hosts) never paint and have no native view. A
|
|
638
|
-
// listener registered on one only exists because a framework's own output-binding
|
|
639
|
-
// machinery (already delivered directly, e.g. Angular's EventEmitter.subscribe) also
|
|
640
|
-
// registered it through Renderer2.listen. Bubbling into it would refire that same
|
|
641
|
-
// callback a second time, so anchors are transparent to listener lookup, not just paint.
|
|
642
|
-
const listener = isAnchor(node)
|
|
643
|
-
? undefined
|
|
644
|
-
: node.listeners?.get(captureName);
|
|
645
|
-
if (listener) {
|
|
646
|
-
if (isDebug())
|
|
647
|
-
dlog(`event ${listenerName} capture on ${node.component}`);
|
|
648
|
-
listener({
|
|
649
|
-
type: listenerName,
|
|
650
|
-
target,
|
|
651
|
-
currentTarget: node,
|
|
652
|
-
nativeEvent,
|
|
653
|
-
stopPropagation,
|
|
654
|
-
});
|
|
655
|
-
if (stopped)
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
// Bubble phase: target -> root, invoking each ancestor's plain listener. Anchors are
|
|
660
|
-
// transparent here too, same reason (see the capture-phase comment above).
|
|
661
|
-
//
|
|
662
|
-
// THE SAME ARRAY, read forward. This used to re-walk `parentOf` from the target, which is a
|
|
663
|
-
// CROSSING of the host boundary per level — so one event asked the host for its ancestors twice,
|
|
664
|
-
// measured at 18 questions on a depth-8 chain where 9 is the floor. The comment above already
|
|
665
|
-
// says the path is built once "without a second allocation"; the second WALK is the expensive
|
|
666
|
-
// half, and it was the one left in. Every touch and every frame of every drag pays it.
|
|
667
|
-
for (const node of path) {
|
|
668
|
-
const listener = isAnchor(node)
|
|
669
|
-
? undefined
|
|
670
|
-
: node.listeners?.get(listenerName);
|
|
671
|
-
if (listener) {
|
|
672
|
-
const event = {
|
|
673
|
-
type: listenerName,
|
|
674
|
-
target,
|
|
675
|
-
currentTarget: node,
|
|
676
|
-
nativeEvent,
|
|
677
|
-
stopPropagation,
|
|
678
|
-
};
|
|
679
|
-
listener(event);
|
|
680
|
-
if (stopped)
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
// Direct (non-bubbling) delivery: only the target's own listener fires.
|
|
686
|
-
function deliverDirect(target, listenerName, nativeEvent) {
|
|
687
|
-
const listener = target.listeners?.get(listenerName);
|
|
688
|
-
if (!listener)
|
|
689
|
-
return;
|
|
690
|
-
listener({
|
|
691
|
-
type: listenerName,
|
|
692
|
-
target,
|
|
693
|
-
currentTarget: target,
|
|
694
|
-
nativeEvent,
|
|
695
|
-
stopPropagation: () => { },
|
|
696
|
-
});
|
|
697
|
-
}
|