@symbiote-native/engine 0.1.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/LICENSE +21 -0
- package/README.md +124 -0
- package/build/accessibility-info/index.android.d.ts +3 -0
- package/build/accessibility-info/index.android.js +166 -0
- package/build/accessibility-info/index.d.ts +1 -0
- package/build/accessibility-info/index.ios.d.ts +3 -0
- package/build/accessibility-info/index.ios.js +219 -0
- package/build/accessibility-info/index.js +5 -0
- package/build/accessibility-info/shared.d.ts +34 -0
- package/build/accessibility-info/shared.js +13 -0
- package/build/action-sheet-ios/index.d.ts +36 -0
- package/build/action-sheet-ios/index.js +74 -0
- package/build/alert/index.android.d.ts +5 -0
- package/build/alert/index.android.js +117 -0
- package/build/alert/index.d.ts +1 -0
- package/build/alert/index.ios.d.ts +7 -0
- package/build/alert/index.ios.js +83 -0
- package/build/alert/index.js +8 -0
- package/build/alert/shared.d.ts +19 -0
- package/build/alert/shared.js +17 -0
- package/build/animated/animated-component-shared.d.ts +5 -0
- package/build/animated/animated-component-shared.js +54 -0
- package/build/animated/animation.d.ts +9 -0
- package/build/animated/animation.js +6 -0
- package/build/animated/animations/base.d.ts +27 -0
- package/build/animated/animations/base.js +90 -0
- package/build/animated/animations/composition.d.ts +38 -0
- package/build/animated/animations/composition.js +236 -0
- package/build/animated/animations/decay.d.ts +22 -0
- package/build/animated/animations/decay.js +65 -0
- package/build/animated/animations/raf.d.ts +5 -0
- package/build/animated/animations/raf.js +39 -0
- package/build/animated/animations/spring-config.d.ts +6 -0
- package/build/animated/animations/spring-config.js +55 -0
- package/build/animated/animations/spring.d.ts +50 -0
- package/build/animated/animations/spring.js +207 -0
- package/build/animated/animations/timing.d.ts +27 -0
- package/build/animated/animations/timing.js +101 -0
- package/build/animated/animations/tracking.d.ts +14 -0
- package/build/animated/animations/tracking.js +43 -0
- package/build/animated/bezier.d.ts +1 -0
- package/build/animated/bezier.js +101 -0
- package/build/animated/color.d.ts +37 -0
- package/build/animated/color.js +183 -0
- package/build/animated/easing.d.ts +20 -0
- package/build/animated/easing.js +96 -0
- package/build/animated/event.d.ts +36 -0
- package/build/animated/event.js +252 -0
- package/build/animated/graph.d.ts +38 -0
- package/build/animated/graph.js +227 -0
- package/build/animated/index.d.ts +20 -0
- package/build/animated/index.js +28 -0
- package/build/animated/interpolation-node.d.ts +16 -0
- package/build/animated/interpolation-node.js +57 -0
- package/build/animated/interpolation.d.ts +22 -0
- package/build/animated/interpolation.js +199 -0
- package/build/animated/mock.d.ts +56 -0
- package/build/animated/mock.js +127 -0
- package/build/animated/native/native-animated.d.ts +43 -0
- package/build/animated/native/native-animated.js +146 -0
- package/build/animated/operators.d.ts +80 -0
- package/build/animated/operators.js +266 -0
- package/build/animated/props.d.ts +20 -0
- package/build/animated/props.js +187 -0
- package/build/animated/style.d.ts +26 -0
- package/build/animated/style.js +187 -0
- package/build/animated/value-xy.d.ts +35 -0
- package/build/animated/value-xy.js +106 -0
- package/build/animated/value.d.ts +36 -0
- package/build/animated/value.js +185 -0
- package/build/app-registry/index.d.ts +40 -0
- package/build/app-registry/index.js +144 -0
- package/build/app-state/index.d.ts +16 -0
- package/build/app-state/index.js +105 -0
- package/build/appearance/index.d.ts +12 -0
- package/build/appearance/index.js +84 -0
- package/build/back-handler/index.d.ts +14 -0
- package/build/back-handler/index.js +106 -0
- package/build/commit.d.ts +16 -0
- package/build/commit.js +678 -0
- package/build/debug.d.ts +5 -0
- package/build/debug.js +18 -0
- package/build/dimensions/index.d.ts +28 -0
- package/build/dimensions/index.js +148 -0
- package/build/dispatch.d.ts +2 -0
- package/build/dispatch.js +18 -0
- package/build/events/index.d.ts +1 -0
- package/build/events/index.js +691 -0
- package/build/fabric.d.ts +32 -0
- package/build/fabric.js +59 -0
- package/build/host-instance/index.d.ts +11 -0
- package/build/host-instance/index.js +49 -0
- package/build/i18n-manager/index.d.ts +13 -0
- package/build/i18n-manager/index.js +91 -0
- package/build/index.d.ts +80 -0
- package/build/index.js +72 -0
- package/build/interaction-manager/index.d.ts +45 -0
- package/build/interaction-manager/index.js +222 -0
- package/build/keyboard/index.d.ts +31 -0
- package/build/keyboard/index.js +142 -0
- package/build/layout-animation/index.d.ts +66 -0
- package/build/layout-animation/index.js +183 -0
- package/build/linking/index.android.d.ts +2 -0
- package/build/linking/index.android.js +18 -0
- package/build/linking/index.d.ts +1 -0
- package/build/linking/index.ios.d.ts +2 -0
- package/build/linking/index.ios.js +9 -0
- package/build/linking/index.js +6 -0
- package/build/linking/shared.d.ts +32 -0
- package/build/linking/shared.js +98 -0
- package/build/native-events.d.ts +24 -0
- package/build/native-events.js +129 -0
- package/build/native-modules.d.ts +6 -0
- package/build/native-modules.js +57 -0
- package/build/node.d.ts +36 -0
- package/build/node.js +194 -0
- package/build/pan-responder/index.d.ts +53 -0
- package/build/pan-responder/index.js +353 -0
- package/build/permissions-android/index.d.ts +115 -0
- package/build/permissions-android/index.js +185 -0
- package/build/pixel-ratio/index.d.ts +8 -0
- package/build/pixel-ratio/index.js +27 -0
- package/build/platform/index.android.d.ts +22 -0
- package/build/platform/index.android.js +60 -0
- package/build/platform/index.d.ts +1 -0
- package/build/platform/index.ios.d.ts +18 -0
- package/build/platform/index.ios.js +62 -0
- package/build/platform/index.js +5 -0
- package/build/platform/shared.d.ts +25 -0
- package/build/platform/shared.js +41 -0
- package/build/platform-color.d.ts +19 -0
- package/build/platform-color.js +25 -0
- package/build/post-commit.d.ts +4 -0
- package/build/post-commit.js +16 -0
- package/build/process-aspect-ratio.d.ts +1 -0
- package/build/process-aspect-ratio.js +34 -0
- package/build/process-background-image/index.d.ts +28 -0
- package/build/process-background-image/index.js +557 -0
- package/build/process-box-shadow/index.d.ts +11 -0
- package/build/process-box-shadow/index.js +193 -0
- package/build/process-filter.d.ts +31 -0
- package/build/process-filter.js +304 -0
- package/build/process-font-variant.d.ts +1 -0
- package/build/process-font-variant.js +17 -0
- package/build/process-transform/index.d.ts +5 -0
- package/build/process-transform/index.js +120 -0
- package/build/process-transform-origin/index.d.ts +3 -0
- package/build/process-transform-origin/index.js +108 -0
- package/build/registry.d.ts +31 -0
- package/build/registry.js +145 -0
- package/build/settings/index.d.ts +8 -0
- package/build/settings/index.js +126 -0
- package/build/share/index.android.d.ts +3 -0
- package/build/share/index.android.js +56 -0
- package/build/share/index.d.ts +1 -0
- package/build/share/index.ios.d.ts +3 -0
- package/build/share/index.ios.js +47 -0
- package/build/share/index.js +6 -0
- package/build/share/shared.d.ts +32 -0
- package/build/share/shared.js +32 -0
- package/build/status-bar/index.android.d.ts +5 -0
- package/build/status-bar/index.android.js +83 -0
- package/build/status-bar/index.d.ts +1 -0
- package/build/status-bar/index.ios.d.ts +5 -0
- package/build/status-bar/index.ios.js +66 -0
- package/build/status-bar/index.js +4 -0
- package/build/status-bar/shared.d.ts +22 -0
- package/build/status-bar/shared.js +22 -0
- package/build/style/index.d.ts +1 -0
- package/build/style/index.js +30 -0
- package/build/style-registry/index.d.ts +11 -0
- package/build/style-registry/index.js +165 -0
- package/build/style-sheet/index.d.ts +20 -0
- package/build/style-sheet/index.js +121 -0
- package/build/styles.d.ts +220 -0
- package/build/styles.js +7 -0
- package/build/surface.d.ts +16 -0
- package/build/surface.js +67 -0
- package/build/tags.d.ts +1 -0
- package/build/tags.js +10 -0
- package/build/text-input-state.d.ts +5 -0
- package/build/text-input-state.js +29 -0
- package/build/toast-android/index.d.ts +10 -0
- package/build/toast-android/index.js +108 -0
- package/build/vibration/index.android.d.ts +2 -0
- package/build/vibration/index.android.js +18 -0
- package/build/vibration/index.d.ts +1 -0
- package/build/vibration/index.ios.d.ts +2 -0
- package/build/vibration/index.ios.js +54 -0
- package/build/vibration/index.js +6 -0
- package/build/vibration/shared.d.ts +15 -0
- package/build/vibration/shared.js +68 -0
- package/build/view-config.d.ts +1 -0
- package/build/view-config.js +114 -0
- package/package.json +41 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
// Event normalization. Fabric delivers raw touch primitives to a single global
|
|
2
|
+
// handler, with the instanceHandle (our SymbioteNode) as the target. There is no
|
|
3
|
+
// raw `press` event; a tap is synthesized from a touch sequence: the start and
|
|
4
|
+
// end targets are correlated so a press fires only when the touch ends on the
|
|
5
|
+
// node it started on (or a descendant). Bubbling events walk target -> root,
|
|
6
|
+
// invoking each ancestor's listener until one calls stopPropagation. Layout is a
|
|
7
|
+
// direct event in RN and is delivered only to its own target.
|
|
8
|
+
import { dlog } from '../debug';
|
|
9
|
+
import { runWrapped } from '../dispatch';
|
|
10
|
+
import { getSlot } from '../fabric';
|
|
11
|
+
import { isAnchor, isSymbioteNode } from '../node';
|
|
12
|
+
import { registeredNativeEvent } from '../registry';
|
|
13
|
+
// Raw Fabric event name -> listener name. Generic bubbling events live here; press
|
|
14
|
+
// is synthesized from a touch sequence and layout is direct, so both are handled
|
|
15
|
+
// outside this table.
|
|
16
|
+
// Raw Fabric event -> listener name, split by dispatch phase. Press is synthesized
|
|
17
|
+
// from a touch sequence (handled separately below); everything else is table-driven.
|
|
18
|
+
// Bubbling events walk target -> root; direct events fire only on the target.
|
|
19
|
+
const BUBBLING_EVENTS = {
|
|
20
|
+
topFocus: 'focus',
|
|
21
|
+
topBlur: 'blur',
|
|
22
|
+
topChange: 'change',
|
|
23
|
+
topEndEditing: 'endEditing',
|
|
24
|
+
topSubmitEditing: 'submitEditing',
|
|
25
|
+
topKeyPress: 'keyPress',
|
|
26
|
+
};
|
|
27
|
+
const DIRECT_EVENTS = {
|
|
28
|
+
topLayout: 'layout',
|
|
29
|
+
topScroll: 'scroll',
|
|
30
|
+
topScrollBeginDrag: 'scrollBeginDrag',
|
|
31
|
+
topScrollEndDrag: 'scrollEndDrag',
|
|
32
|
+
topMomentumScrollBegin: 'momentumScrollBegin',
|
|
33
|
+
topMomentumScrollEnd: 'momentumScrollEnd',
|
|
34
|
+
topSelectionChange: 'selectionChange',
|
|
35
|
+
topContentSizeChange: 'contentSizeChange',
|
|
36
|
+
topLoadStart: 'loadStart',
|
|
37
|
+
topLoad: 'load',
|
|
38
|
+
topLoadEnd: 'loadEnd',
|
|
39
|
+
topError: 'error',
|
|
40
|
+
topProgress: 'progress',
|
|
41
|
+
topPartialLoad: 'partialLoad',
|
|
42
|
+
topRefresh: 'refresh',
|
|
43
|
+
topShow: 'show',
|
|
44
|
+
topRequestClose: 'requestClose',
|
|
45
|
+
topDismiss: 'dismiss',
|
|
46
|
+
topOrientationChange: 'orientationChange',
|
|
47
|
+
// Text glyph layout (onTextLayout) and the iOS status-bar-tap scroll-to-top.
|
|
48
|
+
topTextLayout: 'textLayout',
|
|
49
|
+
topScrollToTop: 'scrollToTop',
|
|
50
|
+
// Accessibility events from RN's base ViewConfig; any view can emit them.
|
|
51
|
+
// accessibilityAction fires on iOS + Android; the iOS-only three (accessibilityTap,
|
|
52
|
+
// magicTap, accessibilityEscape) have no Android producer, so they are inert there.
|
|
53
|
+
topAccessibilityAction: 'accessibilityAction',
|
|
54
|
+
topAccessibilityTap: 'accessibilityTap',
|
|
55
|
+
topMagicTap: 'magicTap',
|
|
56
|
+
topAccessibilityEscape: 'accessibilityEscape',
|
|
57
|
+
};
|
|
58
|
+
const TOUCH_START = 'topTouchStart';
|
|
59
|
+
const TOUCH_MOVE = 'topTouchMove';
|
|
60
|
+
const TOUCH_END = 'topTouchEnd';
|
|
61
|
+
const TOUCH_CANCEL = 'topTouchCancel';
|
|
62
|
+
const PRESS = 'press';
|
|
63
|
+
// Responder protocol (PanResponder / Touchable). RN's two-phase negotiation:
|
|
64
|
+
// every should-set is asked CAPTURE (root -> target) then BUBBLE (target -> root),
|
|
65
|
+
// and the first node returning true wins, on a touch START *and* on every MOVE,
|
|
66
|
+
// so a node can claim the responder mid-gesture. If someone already holds it, the
|
|
67
|
+
// incumbent is asked onResponderTerminationRequest; a true answer (or no listener)
|
|
68
|
+
// hands it over (terminate + grant), a false answer rejects the taker. Lifecycle
|
|
69
|
+
// events are direct (grant/start/move/end/release/terminate/reject). Listener names
|
|
70
|
+
// are post-`on` (onResponderMove -> 'responderMove').
|
|
71
|
+
const START_SHOULD_SET = 'startShouldSetResponder';
|
|
72
|
+
const START_SHOULD_SET_CAPTURE = 'startShouldSetResponderCapture';
|
|
73
|
+
const MOVE_SHOULD_SET = 'moveShouldSetResponder';
|
|
74
|
+
const MOVE_SHOULD_SET_CAPTURE = 'moveShouldSetResponderCapture';
|
|
75
|
+
const RESPONDER_GRANT = 'responderGrant';
|
|
76
|
+
const RESPONDER_REJECT = 'responderReject';
|
|
77
|
+
const RESPONDER_START = 'responderStart';
|
|
78
|
+
const RESPONDER_MOVE = 'responderMove';
|
|
79
|
+
const RESPONDER_END = 'responderEnd';
|
|
80
|
+
const RESPONDER_RELEASE = 'responderRelease';
|
|
81
|
+
const RESPONDER_TERMINATE = 'responderTerminate';
|
|
82
|
+
const RESPONDER_TERMINATION_REQUEST = 'responderTerminationRequest';
|
|
83
|
+
// Synthesized alongside press so Pressable can show pressed-state feedback: both
|
|
84
|
+
// fire on the node the touch STARTED on (the responder), pressOut on end/cancel.
|
|
85
|
+
const PRESS_IN = 'pressIn';
|
|
86
|
+
const PRESS_OUT = 'pressOut';
|
|
87
|
+
// Synthesized from a sustained hold so bare Text/View onLongPress fires without a
|
|
88
|
+
// native event (Pressable runs the same timer in JS). Default delay matches RN's
|
|
89
|
+
// Touchable (500ms); a fired long press suppresses the tap on release.
|
|
90
|
+
const LONG_PRESS = 'longPress';
|
|
91
|
+
const DEFAULT_LONG_PRESS_MS = 500;
|
|
92
|
+
// Pressability cancels the pending long press when the touch drifts past this many
|
|
93
|
+
// points from where it started (Pressability.DEFAULT_LONG_PRESS_DEACTIVATION_DISTANCE).
|
|
94
|
+
const LONG_PRESS_DEACTIVATION_DISTANCE = 10;
|
|
95
|
+
// RN's bank is indexed by touch identifier and warns above 20; we never warn (headless
|
|
96
|
+
// events may carry larger or absent ids), we just skip anything out of a sane range.
|
|
97
|
+
const MAX_TOUCH_BANK = 20;
|
|
98
|
+
const touchBank = [];
|
|
99
|
+
const touchHistory = {
|
|
100
|
+
touchBank,
|
|
101
|
+
numberActiveTouches: 0,
|
|
102
|
+
indexOfSingleActiveTouch: -1,
|
|
103
|
+
mostRecentTimeStamp: 0,
|
|
104
|
+
};
|
|
105
|
+
function toFiniteNumber(value) {
|
|
106
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
107
|
+
}
|
|
108
|
+
// Pull a recordable touch out of an untyped entry, or undefined when it lacks a usable
|
|
109
|
+
// identifier or coordinates. RN's getTouchIdentifier throws on a null id; we skip
|
|
110
|
+
// instead, so events without touch geometry leave the bank untouched.
|
|
111
|
+
function normalizeTouch(raw) {
|
|
112
|
+
if (!isRecord(raw))
|
|
113
|
+
return undefined;
|
|
114
|
+
const identifier = toFiniteNumber(raw.identifier);
|
|
115
|
+
const pageX = toFiniteNumber(raw.pageX);
|
|
116
|
+
const pageY = toFiniteNumber(raw.pageY);
|
|
117
|
+
if (identifier === undefined || pageX === undefined || pageY === undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
if (identifier < 0 || identifier > MAX_TOUCH_BANK)
|
|
120
|
+
return undefined;
|
|
121
|
+
return { identifier, pageX, pageY, timestamp: toFiniteNumber(raw.timestamp) ?? 0 };
|
|
122
|
+
}
|
|
123
|
+
// The changed touches for this frame (start/move/end), defensively read.
|
|
124
|
+
function changedTouchesOf(nativeEvent) {
|
|
125
|
+
const raw = nativeEvent.changedTouches;
|
|
126
|
+
if (!Array.isArray(raw))
|
|
127
|
+
return [];
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const entry of raw) {
|
|
130
|
+
const touch = normalizeTouch(entry);
|
|
131
|
+
if (touch !== undefined)
|
|
132
|
+
out.push(touch);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
// Count of all touches still down (RN reads nativeEvent.touches.length directly).
|
|
137
|
+
function activeTouchCount(nativeEvent) {
|
|
138
|
+
const raw = nativeEvent.touches;
|
|
139
|
+
return Array.isArray(raw) ? raw.length : 0;
|
|
140
|
+
}
|
|
141
|
+
function recordTouchStart(touch) {
|
|
142
|
+
const record = touchBank[touch.identifier];
|
|
143
|
+
if (record) {
|
|
144
|
+
record.touchActive = true;
|
|
145
|
+
record.startPageX = touch.pageX;
|
|
146
|
+
record.startPageY = touch.pageY;
|
|
147
|
+
record.startTimeStamp = touch.timestamp;
|
|
148
|
+
record.currentPageX = touch.pageX;
|
|
149
|
+
record.currentPageY = touch.pageY;
|
|
150
|
+
record.currentTimeStamp = touch.timestamp;
|
|
151
|
+
record.previousPageX = touch.pageX;
|
|
152
|
+
record.previousPageY = touch.pageY;
|
|
153
|
+
record.previousTimeStamp = touch.timestamp;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
touchBank[touch.identifier] = {
|
|
157
|
+
touchActive: true,
|
|
158
|
+
startPageX: touch.pageX,
|
|
159
|
+
startPageY: touch.pageY,
|
|
160
|
+
startTimeStamp: touch.timestamp,
|
|
161
|
+
currentPageX: touch.pageX,
|
|
162
|
+
currentPageY: touch.pageY,
|
|
163
|
+
currentTimeStamp: touch.timestamp,
|
|
164
|
+
previousPageX: touch.pageX,
|
|
165
|
+
previousPageY: touch.pageY,
|
|
166
|
+
previousTimeStamp: touch.timestamp,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
touchHistory.mostRecentTimeStamp = touch.timestamp;
|
|
170
|
+
}
|
|
171
|
+
// Move and end share the previous<-current shift; only `touchActive` differs.
|
|
172
|
+
function shiftTouchRecord(touch, active) {
|
|
173
|
+
const record = touchBank[touch.identifier];
|
|
174
|
+
if (!record)
|
|
175
|
+
return;
|
|
176
|
+
record.touchActive = active;
|
|
177
|
+
record.previousPageX = record.currentPageX;
|
|
178
|
+
record.previousPageY = record.currentPageY;
|
|
179
|
+
record.previousTimeStamp = record.currentTimeStamp;
|
|
180
|
+
record.currentPageX = touch.pageX;
|
|
181
|
+
record.currentPageY = touch.pageY;
|
|
182
|
+
record.currentTimeStamp = touch.timestamp;
|
|
183
|
+
touchHistory.mostRecentTimeStamp = touch.timestamp;
|
|
184
|
+
}
|
|
185
|
+
// Maintain the bank as a touch frame flows. Mirrors RN's recordTouchTrack: moveish
|
|
186
|
+
// shifts records, startish records + recomputes numberActiveTouches, endish marks the
|
|
187
|
+
// record inactive + rescans for the single remaining touch. `kind` is the touch phase.
|
|
188
|
+
function recordTouchTrack(kind, nativeEvent) {
|
|
189
|
+
if (kind === 'move') {
|
|
190
|
+
for (const touch of changedTouchesOf(nativeEvent))
|
|
191
|
+
shiftTouchRecord(touch, true);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (kind === 'start') {
|
|
195
|
+
for (const touch of changedTouchesOf(nativeEvent))
|
|
196
|
+
recordTouchStart(touch);
|
|
197
|
+
touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
|
|
198
|
+
if (touchHistory.numberActiveTouches === 1) {
|
|
199
|
+
const first = normalizeTouch(arrayFirst(nativeEvent.touches));
|
|
200
|
+
touchHistory.indexOfSingleActiveTouch = first?.identifier ?? -1;
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
for (const touch of changedTouchesOf(nativeEvent))
|
|
205
|
+
shiftTouchRecord(touch, false);
|
|
206
|
+
touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
|
|
207
|
+
if (touchHistory.numberActiveTouches === 1) {
|
|
208
|
+
for (let i = 0; i < touchBank.length; i++) {
|
|
209
|
+
const record = touchBank[i];
|
|
210
|
+
if (record !== undefined && record.touchActive) {
|
|
211
|
+
touchHistory.indexOfSingleActiveTouch = i;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function arrayFirst(value) {
|
|
218
|
+
return Array.isArray(value) ? value[0] : undefined;
|
|
219
|
+
}
|
|
220
|
+
// Drop all touch state. Called on a fully-released / cancelled gesture so a stale bank
|
|
221
|
+
// never leaks geometry into the next gesture's first frame.
|
|
222
|
+
function resetTouchHistory() {
|
|
223
|
+
touchBank.length = 0;
|
|
224
|
+
touchHistory.numberActiveTouches = 0;
|
|
225
|
+
touchHistory.indexOfSingleActiveTouch = -1;
|
|
226
|
+
touchHistory.mostRecentTimeStamp = 0;
|
|
227
|
+
}
|
|
228
|
+
// Attach the live touch history onto the event the responder handlers receive, matching
|
|
229
|
+
// ResponderEventPlugin.js (`grantEvent.touchHistory = ...`, etc.). PanResponder reads
|
|
230
|
+
// it for the per-touch dx/vx math; handlers that ignore it are unaffected.
|
|
231
|
+
function attachTouchHistory(nativeEvent) {
|
|
232
|
+
nativeEvent.touchHistory = touchHistory;
|
|
233
|
+
}
|
|
234
|
+
// #endregion
|
|
235
|
+
let installed = false;
|
|
236
|
+
// Target of the in-flight touch, remembered at topTouchStart and consumed (or
|
|
237
|
+
// cleared) at topTouchEnd / topTouchCancel.
|
|
238
|
+
let pressStart;
|
|
239
|
+
// The node that claimed the responder for the in-flight touch (PanResponder), or
|
|
240
|
+
// undefined when nobody claimed it. Receives move and release/terminate.
|
|
241
|
+
let currentResponder;
|
|
242
|
+
// Long-press synthesis: armed at touch start when some node in the press path listens
|
|
243
|
+
// for it, fired once after the hold delay, disarmed on end/cancel; the same arm/clear
|
|
244
|
+
// lifecycle Pressable runs in JS. Pressability ALSO cancels the timer when the touch
|
|
245
|
+
// drifts past LONG_PRESS_DEACTIVATION_DISTANCE, so we record the start point at touch
|
|
246
|
+
// start and clear the timer on a move that exceeds it.
|
|
247
|
+
let longPressTimer;
|
|
248
|
+
let longPressFired = false;
|
|
249
|
+
// Touch coordinate at touch start (pageX/pageY), or undefined when the native event
|
|
250
|
+
// carried no coords; then the move-distance cancel is simply skipped.
|
|
251
|
+
let longPressStart;
|
|
252
|
+
function clearLongPress() {
|
|
253
|
+
if (longPressTimer !== undefined) {
|
|
254
|
+
clearTimeout(longPressTimer);
|
|
255
|
+
longPressTimer = undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Read the gesture's page coordinate from a raw native touch event, defensively: RN
|
|
259
|
+
// puts pageX/pageY on the event for a single touch, or on the first entry of a
|
|
260
|
+
// `touches` array for multi-touch. Returns undefined when neither shape carries
|
|
261
|
+
// numbers, so callers skip any coordinate-dependent logic rather than guess.
|
|
262
|
+
function readTouchPoint(nativeEvent) {
|
|
263
|
+
const fromPair = (source) => {
|
|
264
|
+
if (!source)
|
|
265
|
+
return undefined;
|
|
266
|
+
const { pageX, pageY } = source;
|
|
267
|
+
if (typeof pageX === 'number' && typeof pageY === 'number')
|
|
268
|
+
return { x: pageX, y: pageY };
|
|
269
|
+
return undefined;
|
|
270
|
+
};
|
|
271
|
+
const direct = fromPair(nativeEvent);
|
|
272
|
+
if (direct)
|
|
273
|
+
return direct;
|
|
274
|
+
const touches = nativeEvent.touches;
|
|
275
|
+
if (Array.isArray(touches)) {
|
|
276
|
+
const first = touches[0];
|
|
277
|
+
if (isRecord(first))
|
|
278
|
+
return fromPair(first);
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
// Narrow an unknown to a plain object so its properties can be read without a cast.
|
|
283
|
+
function isRecord(value) {
|
|
284
|
+
return typeof value === 'object' && value !== null;
|
|
285
|
+
}
|
|
286
|
+
// Whether any touch still down started inside the responder (its target IS the
|
|
287
|
+
// responder or a descendant). RN's noResponderTouches walks nativeEvent.touches and
|
|
288
|
+
// returns false the moment one is found; a release fires only when none remain. The
|
|
289
|
+
// headless smokes fire with an empty `{}` event (no `touches`) → no remaining touch →
|
|
290
|
+
// release fires, preserving single-touch behavior. (ResponderEventPlugin.noResponder-
|
|
291
|
+
// Touches + isAncestor.)
|
|
292
|
+
function hasRemainingResponderTouch(responder, nativeEvent) {
|
|
293
|
+
const touches = nativeEvent.touches;
|
|
294
|
+
if (!Array.isArray(touches))
|
|
295
|
+
return false;
|
|
296
|
+
for (const touch of touches) {
|
|
297
|
+
if (!isRecord(touch))
|
|
298
|
+
continue;
|
|
299
|
+
const target = touch.target;
|
|
300
|
+
if (isSymbioteNode(target) && endsWithin(target, responder))
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
// Whether any node from `target` up to the root listens for `listenerName`, used to
|
|
306
|
+
// arm the long-press timer only when a handler would actually receive it.
|
|
307
|
+
function hasListenerInPath(target, listenerName) {
|
|
308
|
+
for (let node = target; node; node = node.parent) {
|
|
309
|
+
if (node.listeners?.has(listenerName) === true)
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
// Invoke one node's own listener (no bubbling) and hand back its return value, so
|
|
315
|
+
// the responder negotiation can read the boolean from onStartShouldSetResponder.
|
|
316
|
+
function callOwnListener(node, listenerName, nativeEvent) {
|
|
317
|
+
const listener = node.listeners?.get(listenerName);
|
|
318
|
+
if (!listener)
|
|
319
|
+
return undefined;
|
|
320
|
+
return listener({
|
|
321
|
+
type: listenerName,
|
|
322
|
+
target: node,
|
|
323
|
+
currentTarget: node,
|
|
324
|
+
nativeEvent,
|
|
325
|
+
stopPropagation: () => { },
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
// The node chain from `from` up to the root, deepest first. The single allocation
|
|
329
|
+
// the two-phase walk indexes both ways (capture reads it reversed).
|
|
330
|
+
function pathToRoot(from) {
|
|
331
|
+
const path = [];
|
|
332
|
+
for (let node = from; node; node = node.parent)
|
|
333
|
+
path.push(node);
|
|
334
|
+
return path;
|
|
335
|
+
}
|
|
336
|
+
// Depth of a node below the root (root = 0). Aligns two nodes before the lockstep
|
|
337
|
+
// climb to their lowest common ancestor.
|
|
338
|
+
function depthOf(node) {
|
|
339
|
+
let depth = 0;
|
|
340
|
+
for (let n = node.parent; n; n = n.parent)
|
|
341
|
+
depth++;
|
|
342
|
+
return depth;
|
|
343
|
+
}
|
|
344
|
+
// RN's getLowestCommonAncestor over our parent pointers: lift the deeper node to the
|
|
345
|
+
// shallower one's depth, then climb both in lockstep until they meet (ResponderEvent-
|
|
346
|
+
// Plugin.getLowestCommonAncestor). Used to scope the move re-negotiation.
|
|
347
|
+
function lowestCommonAncestor(a, b) {
|
|
348
|
+
let da = depthOf(a);
|
|
349
|
+
let db = depthOf(b);
|
|
350
|
+
let na = a;
|
|
351
|
+
let nb = b;
|
|
352
|
+
while (na && da > db) {
|
|
353
|
+
na = na.parent;
|
|
354
|
+
da--;
|
|
355
|
+
}
|
|
356
|
+
while (nb && db > da) {
|
|
357
|
+
nb = nb.parent;
|
|
358
|
+
db--;
|
|
359
|
+
}
|
|
360
|
+
while (na && nb) {
|
|
361
|
+
if (na === nb)
|
|
362
|
+
return na;
|
|
363
|
+
na = na.parent;
|
|
364
|
+
nb = nb.parent;
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
// RN's two-phase should-set walk: CAPTURE root -> deepest, then BUBBLE deepest -> root;
|
|
369
|
+
// the first node returning true wins. `skip` is excluded from both passes; RN skips
|
|
370
|
+
// the deepest node when it IS the current responder (you don't ask the holder to
|
|
371
|
+
// re-claim), so its should-set callback never consumes the gesture frame out from under
|
|
372
|
+
// its own onResponderMove (PanResponder folds geometry in the should-set-capture
|
|
373
|
+
// handler, so asking the responder again would zero its move).
|
|
374
|
+
function findWantsResponder(path, captureName, bubbleName, nativeEvent, skip) {
|
|
375
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
376
|
+
if (path[i] !== skip && callOwnListener(path[i], captureName, nativeEvent) === true) {
|
|
377
|
+
return path[i];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const node of path) {
|
|
381
|
+
if (node !== skip && callOwnListener(node, bubbleName, nativeEvent) === true)
|
|
382
|
+
return node;
|
|
383
|
+
}
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
// Negotiate (or re-negotiate) the responder for a touch start/move. If nobody holds
|
|
387
|
+
// it, the winner is granted. If someone does, the incumbent is asked to relinquish
|
|
388
|
+
// via onResponderTerminationRequest (absent listener = implicit yes); on yes it is
|
|
389
|
+
// terminated and the taker granted, on no the taker is rejected.
|
|
390
|
+
function negotiateResponder(target, phase, nativeEvent) {
|
|
391
|
+
// With no responder, ask the full path from the touch target. With one, RN scopes
|
|
392
|
+
// the walk to the lowest common ancestor of responder+target upward (never below
|
|
393
|
+
// the responder) and skips the deepest node when it IS the responder (Responder-
|
|
394
|
+
// EventPlugin.setResponderAndExtractTransfer). At touch start currentResponder is
|
|
395
|
+
// cleared, so this collapses to the plain target->root start walk.
|
|
396
|
+
const from = currentResponder === undefined ? target : lowestCommonAncestor(currentResponder, target);
|
|
397
|
+
if (!from)
|
|
398
|
+
return;
|
|
399
|
+
const path = pathToRoot(from);
|
|
400
|
+
const skip = from === currentResponder ? from : undefined;
|
|
401
|
+
const wants = phase === 'start'
|
|
402
|
+
? findWantsResponder(path, START_SHOULD_SET_CAPTURE, START_SHOULD_SET, nativeEvent, skip)
|
|
403
|
+
: findWantsResponder(path, MOVE_SHOULD_SET_CAPTURE, MOVE_SHOULD_SET, nativeEvent, skip);
|
|
404
|
+
if (!wants || wants === currentResponder)
|
|
405
|
+
return;
|
|
406
|
+
if (currentResponder === undefined) {
|
|
407
|
+
currentResponder = wants;
|
|
408
|
+
dlog(`responder granted to ${wants.component}`);
|
|
409
|
+
callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const incumbent = currentResponder;
|
|
413
|
+
// A missing termination-request listener means implicit consent (RN default true);
|
|
414
|
+
// only an explicit non-true answer keeps the incumbent and rejects the taker.
|
|
415
|
+
const guarded = incumbent.listeners?.has(RESPONDER_TERMINATION_REQUEST) === true;
|
|
416
|
+
const allowed = !guarded || callOwnListener(incumbent, RESPONDER_TERMINATION_REQUEST, nativeEvent) === true;
|
|
417
|
+
if (allowed) {
|
|
418
|
+
// RN's transfer order (setResponderAndExtractTransfer): grant the TAKER first, then
|
|
419
|
+
// terminate the incumbent. RN dispatches grant ahead of the terminationRequest too,
|
|
420
|
+
// purely to read the taker's block-native return; we have no native surface to
|
|
421
|
+
// block, so on the REJECT path firing a grant the taker never keeps would be a
|
|
422
|
+
// visible no-op event with no behavioral counterpart. We therefore fire grant
|
|
423
|
+
// before terminate on the consent path (matching RN's grant<terminate ordering) and
|
|
424
|
+
// omit it on reject; the consent OUTCOME is unchanged either way.
|
|
425
|
+
dlog(`responder transferred ${incumbent.component} -> ${wants.component}`);
|
|
426
|
+
callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
|
|
427
|
+
callOwnListener(incumbent, RESPONDER_TERMINATE, nativeEvent);
|
|
428
|
+
currentResponder = wants;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
dlog(`responder takeover of ${incumbent.component} rejected`);
|
|
432
|
+
callOwnListener(wants, RESPONDER_REJECT, nativeEvent);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
export function installEventHandler() {
|
|
436
|
+
if (installed)
|
|
437
|
+
return;
|
|
438
|
+
installed = true;
|
|
439
|
+
getSlot().registerEventHandler((instanceHandle, topLevelType, nativeEvent) => {
|
|
440
|
+
if (!isSymbioteNode(instanceHandle))
|
|
441
|
+
return;
|
|
442
|
+
if (topLevelType === TOUCH_START) {
|
|
443
|
+
dlog(`event ${TOUCH_START}`);
|
|
444
|
+
// Update the touch bank, then attach it so responder handlers (PanResponder)
|
|
445
|
+
// read each touch's own previous->current delta; RN records before dispatch.
|
|
446
|
+
recordTouchTrack('start', nativeEvent);
|
|
447
|
+
attachTouchHistory(nativeEvent);
|
|
448
|
+
pressStart = instanceHandle;
|
|
449
|
+
// Arm long-press synthesis: only when a listener exists in the path, fired once
|
|
450
|
+
// after the hold delay, then suppresses the tap (longPressFired) on release.
|
|
451
|
+
longPressFired = false;
|
|
452
|
+
clearLongPress();
|
|
453
|
+
longPressStart = readTouchPoint(nativeEvent);
|
|
454
|
+
if (hasListenerInPath(instanceHandle, LONG_PRESS)) {
|
|
455
|
+
const longPressTarget = instanceHandle;
|
|
456
|
+
longPressTimer = setTimeout(() => {
|
|
457
|
+
longPressTimer = undefined;
|
|
458
|
+
longPressFired = true;
|
|
459
|
+
dlog('synthesized longPress -> dispatch');
|
|
460
|
+
runWrapped(() => bubble(longPressTarget, LONG_PRESS, nativeEvent));
|
|
461
|
+
}, DEFAULT_LONG_PRESS_MS);
|
|
462
|
+
}
|
|
463
|
+
runWrapped(() => {
|
|
464
|
+
bubble(instanceHandle, PRESS_IN, nativeEvent);
|
|
465
|
+
// Responder negotiation runs alongside press synthesis: a View can be both
|
|
466
|
+
// a Pressable (press) and a PanResponder target (responder).
|
|
467
|
+
negotiateResponder(instanceHandle, 'start', nativeEvent);
|
|
468
|
+
// onResponderStart is a direct event to whoever now holds the responder.
|
|
469
|
+
if (currentResponder)
|
|
470
|
+
callOwnListener(currentResponder, RESPONDER_START, nativeEvent);
|
|
471
|
+
});
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (topLevelType === TOUCH_MOVE) {
|
|
475
|
+
recordTouchTrack('move', nativeEvent);
|
|
476
|
+
attachTouchHistory(nativeEvent);
|
|
477
|
+
// Cancel the pending long press if the touch drifted too far (Pressability's
|
|
478
|
+
// deactivation-distance check). Skipped when either coord is unknown.
|
|
479
|
+
if (longPressTimer !== undefined && longPressStart) {
|
|
480
|
+
const here = readTouchPoint(nativeEvent);
|
|
481
|
+
if (here) {
|
|
482
|
+
const dx = here.x - longPressStart.x;
|
|
483
|
+
const dy = here.y - longPressStart.y;
|
|
484
|
+
if (Math.hypot(dx, dy) > LONG_PRESS_DEACTIVATION_DISTANCE) {
|
|
485
|
+
dlog('longPress cancelled (moved past deactivation distance)');
|
|
486
|
+
clearLongPress();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
runWrapped(() => {
|
|
491
|
+
// Re-negotiate first: a node can claim the responder mid-gesture via
|
|
492
|
+
// onMoveShouldSetResponder (the responder itself is skipped, see negotiate).
|
|
493
|
+
negotiateResponder(instanceHandle, 'move', nativeEvent);
|
|
494
|
+
// The only consumer of a move is the responder; without one, RN drops it too.
|
|
495
|
+
if (currentResponder)
|
|
496
|
+
callOwnListener(currentResponder, RESPONDER_MOVE, nativeEvent);
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (topLevelType === TOUCH_END) {
|
|
501
|
+
recordTouchTrack('end', nativeEvent);
|
|
502
|
+
attachTouchHistory(nativeEvent);
|
|
503
|
+
const start = pressStart;
|
|
504
|
+
pressStart = undefined;
|
|
505
|
+
const responder = currentResponder;
|
|
506
|
+
// RN releases (and clears) the responder only when no remaining touch still down
|
|
507
|
+
// started inside it; lifting ONE finger in a multi-touch gesture must NOT release.
|
|
508
|
+
// onResponderEnd still fires on every finger-up. (ResponderEventPlugin: responderEnd
|
|
509
|
+
// is unconditional, responderRelease is gated on noResponderTouches.)
|
|
510
|
+
const releases = responder !== undefined && !hasRemainingResponderTouch(responder, nativeEvent);
|
|
511
|
+
if (releases)
|
|
512
|
+
currentResponder = undefined;
|
|
513
|
+
// A completed long press eats the tap (RN), but pressOut still fires below.
|
|
514
|
+
const wasLongPress = longPressFired;
|
|
515
|
+
longPressFired = false;
|
|
516
|
+
longPressStart = undefined;
|
|
517
|
+
clearLongPress();
|
|
518
|
+
runWrapped(() => {
|
|
519
|
+
if (start) {
|
|
520
|
+
// press fires only on an honest tap (ended within the responder); pressOut
|
|
521
|
+
// always fires on the responder so its pressed-state can release.
|
|
522
|
+
if (endsWithin(instanceHandle, start)) {
|
|
523
|
+
if (wasLongPress) {
|
|
524
|
+
dlog('press suppressed (longPress already fired)');
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
dlog('event press -> dispatch');
|
|
528
|
+
bubble(start, PRESS, nativeEvent);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
bubble(start, PRESS_OUT, nativeEvent);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
dlog(`event ${TOUCH_END} ignored (no matching start)`);
|
|
535
|
+
}
|
|
536
|
+
// onResponderEnd fires on every finger-up; onResponderRelease (the final
|
|
537
|
+
// release) only when the last responder touch lifted.
|
|
538
|
+
if (responder) {
|
|
539
|
+
callOwnListener(responder, RESPONDER_END, nativeEvent);
|
|
540
|
+
if (releases)
|
|
541
|
+
callOwnListener(responder, RESPONDER_RELEASE, nativeEvent);
|
|
542
|
+
else
|
|
543
|
+
dlog('responderEnd without release (touches remain inside responder)');
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
// Once no touch is down, clear the bank so the next gesture starts clean.
|
|
547
|
+
if (touchHistory.numberActiveTouches === 0)
|
|
548
|
+
resetTouchHistory();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (topLevelType === TOUCH_CANCEL) {
|
|
552
|
+
recordTouchTrack('end', nativeEvent);
|
|
553
|
+
attachTouchHistory(nativeEvent);
|
|
554
|
+
const start = pressStart;
|
|
555
|
+
pressStart = undefined;
|
|
556
|
+
const responder = currentResponder;
|
|
557
|
+
currentResponder = undefined;
|
|
558
|
+
longPressFired = false;
|
|
559
|
+
longPressStart = undefined;
|
|
560
|
+
clearLongPress();
|
|
561
|
+
runWrapped(() => {
|
|
562
|
+
if (start)
|
|
563
|
+
bubble(start, PRESS_OUT, nativeEvent);
|
|
564
|
+
// A cancelled gesture ends then terminates (the responder was taken away).
|
|
565
|
+
if (responder) {
|
|
566
|
+
callOwnListener(responder, RESPONDER_END, nativeEvent);
|
|
567
|
+
callOwnListener(responder, RESPONDER_TERMINATE, nativeEvent);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
if (touchHistory.numberActiveTouches === 0)
|
|
571
|
+
resetTouchHistory();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const direct = DIRECT_EVENTS[topLevelType];
|
|
575
|
+
if (direct !== undefined) {
|
|
576
|
+
dlog(`event ${topLevelType} -> ${direct} (direct)`);
|
|
577
|
+
runWrapped(() => deliverDirect(instanceHandle, direct, nativeEvent));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const bubbling = BUBBLING_EVENTS[topLevelType];
|
|
581
|
+
if (bubbling !== undefined) {
|
|
582
|
+
dlog(`event ${topLevelType} -> ${bubbling} (bubble)`);
|
|
583
|
+
runWrapped(() => bubble(instanceHandle, bubbling, nativeEvent));
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
// Third-party Fabric views (registerComponent) declare their own events; the
|
|
587
|
+
// built-in tables above don't know them, so fall back to the registry, keyed by
|
|
588
|
+
// the node's own component. `direct` events fire only on their target, the rest
|
|
589
|
+
// bubble: same split as the built-ins.
|
|
590
|
+
const registered = registeredNativeEvent(instanceHandle.component, topLevelType);
|
|
591
|
+
if (registered !== undefined) {
|
|
592
|
+
const phase = registered.direct ? 'direct' : 'bubble';
|
|
593
|
+
dlog(`event ${topLevelType} -> ${registered.listener} (${phase}, registered)`);
|
|
594
|
+
runWrapped(() => registered.direct
|
|
595
|
+
? deliverDirect(instanceHandle, registered.listener, nativeEvent)
|
|
596
|
+
: bubble(instanceHandle, registered.listener, nativeEvent));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// Nothing claimed this event: neither a built-in table nor the view's derived
|
|
600
|
+
// config. A permanent diagnostic seam: if a native view fires something we drop
|
|
601
|
+
// on the floor (an event the ViewConfig didn't surface, or a name mismatch),
|
|
602
|
+
// this is where it shows up. Keeps "the handler silently did nothing" debuggable.
|
|
603
|
+
dlog(`event ${topLevelType} UNMATCHED on ${instanceHandle.component} (dropped)`);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
// A press is honest only if the touch ends on the node it started on, or a
|
|
607
|
+
// descendant of it: walk parent pointers up from the end target looking for the
|
|
608
|
+
// start target. The start node may have been unmounted mid-touch (parent pointer
|
|
609
|
+
// cleared); the walk simply runs out and returns false, no throw.
|
|
610
|
+
function endsWithin(endTarget, start) {
|
|
611
|
+
let node = endTarget;
|
|
612
|
+
while (node) {
|
|
613
|
+
if (node === start)
|
|
614
|
+
return true;
|
|
615
|
+
node = node.parent;
|
|
616
|
+
}
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
// Two-phase delivery, mirroring RN's accumulateTwoPhaseDispatches (legacy-events/
|
|
620
|
+
// EventPropagators): CAPTURE root -> target first, invoking each node's
|
|
621
|
+
// `<EventName>Capture` listener, then BUBBLE target -> root invoking the plain
|
|
622
|
+
// listener. The same event object semantics apply to both passes; a stopPropagation
|
|
623
|
+
// in capture halts before bubble ever runs. `target` stays the original node;
|
|
624
|
+
// `currentTarget` tracks whose listener runs.
|
|
625
|
+
function bubble(target, listenerName, nativeEvent) {
|
|
626
|
+
let stopped = false;
|
|
627
|
+
const stopPropagation = () => {
|
|
628
|
+
stopped = true;
|
|
629
|
+
};
|
|
630
|
+
// Capture phase: root -> target. RN gathers captured listeners first (the
|
|
631
|
+
// `<EventName>Capture` registration), so on*Capture handlers fire ahead of the
|
|
632
|
+
// bubble pass. The path is built target -> root, then walked in reverse to get
|
|
633
|
+
// root -> target without a second allocation.
|
|
634
|
+
const captureName = `${listenerName}Capture`;
|
|
635
|
+
const path = pathToRoot(target);
|
|
636
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
637
|
+
const node = path[i];
|
|
638
|
+
// Anchors (Angular's #anchor component hosts) never paint and have no native view — a
|
|
639
|
+
// listener registered on one only exists because a framework's own output-binding
|
|
640
|
+
// machinery (already delivered directly, e.g. Angular's EventEmitter.subscribe) also
|
|
641
|
+
// registered it through Renderer2.listen. Bubbling into it would refire that same
|
|
642
|
+
// callback a second time, so anchors are transparent to listener lookup, not just paint.
|
|
643
|
+
const listener = isAnchor(node) ? undefined : node.listeners?.get(captureName);
|
|
644
|
+
if (listener) {
|
|
645
|
+
dlog(`event ${listenerName} capture on ${node.component}`);
|
|
646
|
+
listener({
|
|
647
|
+
type: listenerName,
|
|
648
|
+
target,
|
|
649
|
+
currentTarget: node,
|
|
650
|
+
nativeEvent,
|
|
651
|
+
stopPropagation,
|
|
652
|
+
});
|
|
653
|
+
if (stopped)
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Bubble phase: target -> root, invoking each ancestor's plain listener. Anchors are
|
|
658
|
+
// transparent here too (see the capture-phase comment above) — same event, same reason.
|
|
659
|
+
let node = target;
|
|
660
|
+
while (node) {
|
|
661
|
+
const listener = isAnchor(node) ? undefined : node.listeners?.get(listenerName);
|
|
662
|
+
if (listener) {
|
|
663
|
+
// engine owner adds currentTarget + stopPropagation to SymbioteEvent
|
|
664
|
+
const event = {
|
|
665
|
+
type: listenerName,
|
|
666
|
+
target,
|
|
667
|
+
currentTarget: node,
|
|
668
|
+
nativeEvent,
|
|
669
|
+
stopPropagation,
|
|
670
|
+
};
|
|
671
|
+
listener(event);
|
|
672
|
+
if (stopped)
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
node = node.parent;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// Direct (non-bubbling) delivery: only the target's own listener fires.
|
|
679
|
+
function deliverDirect(target, listenerName, nativeEvent) {
|
|
680
|
+
const listener = target.listeners?.get(listenerName);
|
|
681
|
+
if (!listener)
|
|
682
|
+
return;
|
|
683
|
+
// engine owner adds currentTarget + stopPropagation to SymbioteEvent
|
|
684
|
+
listener({
|
|
685
|
+
type: listenerName,
|
|
686
|
+
target,
|
|
687
|
+
currentTarget: target,
|
|
688
|
+
nativeEvent,
|
|
689
|
+
stopPropagation: () => { },
|
|
690
|
+
});
|
|
691
|
+
}
|