@symbiote-native/angular 0.6.2 → 0.8.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/README.md +3 -3
- package/bin/symbiote-angular-dev.cjs +20 -19
- package/build/angular/anchor-host-registry.js +2 -2
- package/build/angular/anchor-host-registry.js.map +1 -1
- package/build/angular/bootstrap.js +1 -1
- package/build/angular/bootstrap.js.map +1 -1
- package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.android.js +12 -4
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.ios.js +8 -3
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
- package/build/angular/components/activity-indicator/shared.d.ts +1 -1
- package/build/angular/components/activity-indicator/shared.js +4 -4
- package/build/angular/components/activity-indicator/shared.js.map +1 -1
- package/build/angular/components/button.d.ts +4 -2
- package/build/angular/components/button.js +41 -22
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +6 -4
- package/build/angular/components/flat-list/index.js +80 -50
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.d.ts +8 -3
- package/build/angular/components/image/index.android.js +42 -10
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.d.ts +8 -3
- package/build/angular/components/image/index.ios.js +42 -10
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -1
- package/build/angular/components/image/shared.js +51 -8
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +2 -1
- package/build/angular/components/image-background.js +26 -10
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +9 -4
- package/build/angular/components/input-accessory-view/index.js +49 -10
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +44 -12
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +11 -8
- package/build/angular/components/modal/index.js +51 -29
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +12 -6
- package/build/angular/components/pressable/index.js +66 -13
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +8 -4
- package/build/angular/components/refresh-control/index.js +54 -16
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +9 -4
- package/build/angular/components/safe-area-view/index.js +48 -9
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.d.ts +2 -3
- package/build/angular/components/scroll-view/index.android.js +88 -61
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
- package/build/angular/components/scroll-view/index.ios.js +24 -18
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +35 -0
- package/build/angular/components/scroll-view/projection.js +279 -53
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.d.ts +14 -5
- package/build/angular/components/scroll-view/shared.js +225 -75
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +21 -22
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +7 -1
- package/build/angular/components/section-list/index.js +91 -54
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.d.ts +7 -3
- package/build/angular/components/switch/index.android.js +52 -10
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.d.ts +7 -3
- package/build/angular/components/switch/index.ios.js +52 -10
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +13 -4
- package/build/angular/components/switch/shared.js +71 -7
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +11 -3
- package/build/angular/components/text-input.js +68 -8
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +40 -9
- package/build/angular/components/touchable/index.js +303 -67
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
- package/build/angular/components/touchable-native-feedback/index.js +63 -27
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +22 -18
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +15 -4
- package/build/angular/components/virtualized-list/index.js +326 -94
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js +12 -12
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.d.ts +19 -1
- package/build/angular/components/virtualized-section-list/index.js +93 -22
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +2 -2
- package/build/angular/components.js +5 -0
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +34 -36
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js +19 -22
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +6 -4
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/diagnostics.d.ts +33 -0
- package/build/angular/diagnostics.js +82 -0
- package/build/angular/diagnostics.js.map +1 -0
- package/build/angular/host-instance/index.js +1 -1
- package/build/angular/host-instance/index.js.map +1 -1
- package/build/angular/index.d.ts +9 -4
- package/build/angular/index.js +17 -3
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
- package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
- package/build/angular/modules/animated/create-animated-component.js +106 -55
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/status-bar/index.js +4 -4
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +20 -3
- package/build/angular/primitives/index.js +84 -48
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +48 -2
- package/build/angular/primitives/shared.js +158 -11
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +69 -38
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +1 -0
- package/build/angular/renderer/index.js +37 -2
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/services/color-scheme.service.js +3 -3
- package/build/angular/services/color-scheme.service.js.map +1 -1
- package/build/angular/services/window-dimensions.service.js +4 -4
- package/build/angular/services/window-dimensions.service.js.map +1 -1
- package/package.json +10 -8
- package/src/anchor-host-registry.ts +2 -2
- package/src/bootstrap.ts +4 -1
- package/src/components/activity-indicator/index.android.ts +8 -1
- package/src/components/activity-indicator/index.ios.ts +4 -0
- package/src/components/activity-indicator/shared.ts +32 -13
- package/src/components/button.ts +46 -19
- package/src/components/flat-list/index.ts +122 -62
- package/src/components/image/index.android.ts +58 -7
- package/src/components/image/index.ios.ts +58 -7
- package/src/components/image/shared.ts +92 -21
- package/src/components/image-background.ts +29 -9
- package/src/components/input-accessory-view/index.ts +67 -12
- package/src/components/keyboard-avoiding-view/index.ts +61 -15
- package/src/components/modal/index.ts +72 -33
- package/src/components/pressable/index.ts +117 -28
- package/src/components/refresh-control/index.ts +74 -18
- package/src/components/safe-area-view/index.ts +64 -10
- package/src/components/scroll-view/index.android.ts +62 -47
- package/src/components/scroll-view/index.ios.ts +19 -17
- package/src/components/scroll-view/projection.ts +363 -64
- package/src/components/scroll-view/shared.ts +312 -90
- package/src/components/scroll-view/sticky-header.ts +36 -24
- package/src/components/section-list/index.ts +86 -53
- package/src/components/switch/index.android.ts +60 -8
- package/src/components/switch/index.ios.ts +60 -8
- package/src/components/switch/shared.ts +93 -6
- package/src/components/text-input.ts +95 -13
- package/src/components/touchable/index.ts +348 -64
- package/src/components/touchable-native-feedback/index.ts +94 -31
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +380 -85
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +146 -33
- package/src/components.ts +9 -1
- package/src/create-portal/index.ts +31 -31
- package/src/create-tunnel/index.ts +16 -17
- package/src/descriptor-to-angular/index.ts +29 -8
- package/src/diagnostics.ts +138 -0
- package/src/host-instance/index.ts +9 -3
- package/src/index.ts +61 -3
- package/src/modules/animated/animated-leaf-binder.ts +28 -77
- package/src/modules/animated/create-animated-component.ts +115 -47
- package/src/modules/app-registry/index.ts +9 -9
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +47 -2
- package/src/primitives/shared.ts +170 -10
- package/src/render/index.ts +77 -39
- package/src/renderer/index.ts +76 -14
- package/src/services/color-scheme.service.ts +3 -1
- package/src/services/window-dimensions.service.ts +11 -4
- package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
- package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
- package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
- package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
AnimatedProps,
|
|
3
3
|
type AnimatedValue,
|
|
4
4
|
dlog,
|
|
5
|
+
isDebug,
|
|
5
6
|
isAnchor,
|
|
6
7
|
reduceProps,
|
|
7
8
|
routeProp,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
type ISymbioteEvent,
|
|
15
16
|
type ISymbioteNode,
|
|
16
17
|
} from '@symbiote-native/engine';
|
|
18
|
+
import { countAngular } from '../../diagnostics';
|
|
17
19
|
import {
|
|
18
20
|
createInitialStickyState,
|
|
19
21
|
readLayoutNumber,
|
|
@@ -26,11 +28,45 @@ import {
|
|
|
26
28
|
import { descriptorFor } from '@symbiote-native/components';
|
|
27
29
|
import { Platform } from '@symbiote-native/engine';
|
|
28
30
|
|
|
29
|
-
type IInsert = (
|
|
31
|
+
type IInsert = (
|
|
32
|
+
parent: ISymbioteNode,
|
|
33
|
+
child: ISymbioteNode,
|
|
34
|
+
beforeChild?: ISymbioteNode,
|
|
35
|
+
) => void;
|
|
30
36
|
type IRemove = (parent: ISymbioteNode, child: ISymbioteNode) => void;
|
|
31
37
|
|
|
32
|
-
const contentProjection = new WeakMap<
|
|
33
|
-
|
|
38
|
+
const contentProjection = new WeakMap<
|
|
39
|
+
ISymbioteNode,
|
|
40
|
+
ScrollViewProjectionController
|
|
41
|
+
>();
|
|
42
|
+
// Which controller owns a projected child. Keyed by EVERY projected child, not only the wrapped
|
|
43
|
+
// ones: sticky indices are positional, so a child removed behind the controller's back leaves a dead
|
|
44
|
+
// record that inflates every later child's paint index (see removeScrollViewProjectedChild). A
|
|
45
|
+
// windowed list recycles cells continuously, so that drift grows with scroll distance.
|
|
46
|
+
const projectedOwners = new WeakMap<
|
|
47
|
+
ISymbioteNode,
|
|
48
|
+
ScrollViewProjectionController
|
|
49
|
+
>();
|
|
50
|
+
|
|
51
|
+
// Controllers with a coalesced sticky reconcile owed, drained once per change-detection pass by
|
|
52
|
+
// flushScrollViewProjections (see ScrollViewProjectionController.scheduleReconcile for why the
|
|
53
|
+
// flush must be synchronous rather than a microtask). A strong Set is correct here: an entry lives
|
|
54
|
+
// only from the mutation that queued it to the end of that same pass.
|
|
55
|
+
const pendingProjections = new Set<ScrollViewProjectionController>();
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run every sticky reconcile owed by this change-detection pass. Wired to the renderer factory's
|
|
59
|
+
* `end()`, which Angular calls at the end of `ApplicationRef.tick` — after both channels that feed
|
|
60
|
+
* a projection controller have written (children through the renderer, `stickyHeaderIndices` through
|
|
61
|
+
* an `@Input`), and before the surface's microtask-coalesced commit runs.
|
|
62
|
+
*/
|
|
63
|
+
export function flushScrollViewProjections(): void {
|
|
64
|
+
if (pendingProjections.size === 0) return;
|
|
65
|
+
// Snapshot: a reconcile can wrap a record, and wrapping is itself a projected mutation.
|
|
66
|
+
const owed = [...pendingProjections];
|
|
67
|
+
pendingProjections.clear();
|
|
68
|
+
for (const controller of owed) controller.flushReconcile();
|
|
69
|
+
}
|
|
34
70
|
|
|
35
71
|
interface IProjectedRecord {
|
|
36
72
|
child: ISymbioteNode;
|
|
@@ -58,7 +94,10 @@ function directNode(record: IProjectedRecord): ISymbioteNode {
|
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
function isProjectedRefreshControl(node: ISymbioteNode): boolean {
|
|
61
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
node.component === 'PullToRefreshView' ||
|
|
99
|
+
node.component === 'AndroidSwipeRefreshLayout'
|
|
100
|
+
) {
|
|
62
101
|
return true;
|
|
63
102
|
}
|
|
64
103
|
// Angular public <RefreshControl> is an anchor host whose real native refresh node lives inside
|
|
@@ -73,6 +112,9 @@ function isProjectedRefreshControl(node: ISymbioteNode): boolean {
|
|
|
73
112
|
// delay, rebuild ranges, cross-talk record) live there; this runner only EXECUTES the effects on the
|
|
74
113
|
// engine node (build interpolation + wire listener, hold the debounce timer, re-apply the node props).
|
|
75
114
|
// Because it owns the child index, it consumes the reducer's record-header-y effect for cross-talk.
|
|
115
|
+
// Diagnostic-only: a process-wide counter so each controller's log lines are attributable.
|
|
116
|
+
let projectionControllerSeq = 0;
|
|
117
|
+
|
|
76
118
|
class StickyProjectionWrapper {
|
|
77
119
|
private readonly state = createInitialStickyState();
|
|
78
120
|
private animatedTranslateY: AnimatedInterpolation;
|
|
@@ -84,13 +126,19 @@ class StickyProjectionWrapper {
|
|
|
84
126
|
|
|
85
127
|
constructor(
|
|
86
128
|
private readonly controller: ScrollViewProjectionController,
|
|
87
|
-
|
|
129
|
+
// NOT readonly: a window step renumbers every paint index below it without changing which child
|
|
130
|
+
// this header is. See setChildIndex.
|
|
131
|
+
private childIndex: number,
|
|
88
132
|
private readonly node: ISymbioteNode,
|
|
89
133
|
) {
|
|
90
|
-
this.animatedTranslateY =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
134
|
+
this.animatedTranslateY =
|
|
135
|
+
this.controller.config.scrollAnimatedValue.interpolate({
|
|
136
|
+
inputRange: [-1, 0],
|
|
137
|
+
outputRange: [0, 0],
|
|
138
|
+
});
|
|
139
|
+
dlog(
|
|
140
|
+
`STICKY[wrap] created index=${childIndex} scrollValueIsNative=${this.controller.config.scrollAnimatedValue.__isNative()}`,
|
|
141
|
+
);
|
|
94
142
|
this.dispatch({ kind: 'inputs-changed' });
|
|
95
143
|
}
|
|
96
144
|
|
|
@@ -117,6 +165,24 @@ class StickyProjectionWrapper {
|
|
|
117
165
|
this.dispatch({ kind: 'inputs-changed' });
|
|
118
166
|
}
|
|
119
167
|
|
|
168
|
+
// A window step renumbers every paint index below it while the header itself is unchanged: same
|
|
169
|
+
// child, same measured layout, new position among the projected children. Recreating the wrapper
|
|
170
|
+
// for that throws away `measured`/`layoutY` so the header re-measures from scratch, and costs a
|
|
171
|
+
// native round trip per step (restoreDefaultValues + dropAnimatedNode out, a fresh interpolation +
|
|
172
|
+
// AnimatedProps in). Feed the new index to the SAME state machine and let the reducer's range
|
|
173
|
+
// guard decide whether anything must be rebuilt.
|
|
174
|
+
//
|
|
175
|
+
// Identity is the CHILD - it owns the record, the record owns this wrapper - never the position.
|
|
176
|
+
// Angular CDK's virtual scroll makes the same call: on a window move `_updateContext` writes
|
|
177
|
+
// `view.context.index = renderedRange.start + i` into the EXISTING view rather than recreating it,
|
|
178
|
+
// and its trackBy is offset by `renderedRange.start` so identity stays absolute
|
|
179
|
+
// (`cdk/scrolling/virtual-for-of.ts`).
|
|
180
|
+
setChildIndex(next: number): void {
|
|
181
|
+
if (this.childIndex === next) return;
|
|
182
|
+
this.childIndex = next;
|
|
183
|
+
this.dispatch({ kind: 'inputs-changed' });
|
|
184
|
+
}
|
|
185
|
+
|
|
120
186
|
private inputs(): IStickyReducerInputs {
|
|
121
187
|
return {
|
|
122
188
|
os: Platform.OS,
|
|
@@ -135,7 +201,10 @@ class StickyProjectionWrapper {
|
|
|
135
201
|
for (const effect of effects) {
|
|
136
202
|
switch (effect.kind) {
|
|
137
203
|
case 'rebuild-interpolation': {
|
|
138
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
this.interpolation !== undefined &&
|
|
206
|
+
this.listenerId !== undefined
|
|
207
|
+
) {
|
|
139
208
|
this.interpolation.removeListener(this.listenerId);
|
|
140
209
|
this.listenerId = undefined;
|
|
141
210
|
}
|
|
@@ -148,12 +217,13 @@ class StickyProjectionWrapper {
|
|
|
148
217
|
this.animatedTranslateY = next;
|
|
149
218
|
this.applyProps();
|
|
150
219
|
dlog(
|
|
151
|
-
`
|
|
220
|
+
`STICKY[wrap] rebuilt index=${this.childIndex} measured=${this.state.measured} y=${this.state.layoutY}`,
|
|
152
221
|
);
|
|
153
222
|
break;
|
|
154
223
|
}
|
|
155
224
|
case 'schedule-debounce':
|
|
156
|
-
if (this.debounceTimer !== undefined)
|
|
225
|
+
if (this.debounceTimer !== undefined)
|
|
226
|
+
clearTimeout(this.debounceTimer);
|
|
157
227
|
this.debounceTimer = setTimeout(() => {
|
|
158
228
|
this.debounceTimer = undefined;
|
|
159
229
|
this.dispatch({ kind: 'debounce-fired', value: effect.value });
|
|
@@ -172,6 +242,9 @@ class StickyProjectionWrapper {
|
|
|
172
242
|
private readonly onLayout = (event: ISymbioteEvent): void => {
|
|
173
243
|
const y = readLayoutNumber(event, 'y');
|
|
174
244
|
const height = readLayoutNumber(event, 'height');
|
|
245
|
+
dlog(
|
|
246
|
+
`STICKY[wrap] index=${this.childIndex} onLayout y=${y} height=${height}`,
|
|
247
|
+
);
|
|
175
248
|
// Keep the previous value when a field is absent (RN sets state only on a defined read).
|
|
176
249
|
this.dispatch({
|
|
177
250
|
kind: 'layout',
|
|
@@ -180,8 +253,21 @@ class StickyProjectionWrapper {
|
|
|
180
253
|
});
|
|
181
254
|
};
|
|
182
255
|
|
|
183
|
-
|
|
184
|
-
|
|
256
|
+
// The ONLY thing that drives the committed pin on this path: applyProps has no __makeNative(),
|
|
257
|
+
// so the wrapper is JS-driven end to end. If the scroll value has been made native up front the
|
|
258
|
+
// cascade to child listeners stops (AnimatedWithChildren.__callListeners skips children once
|
|
259
|
+
// isNative) and this never fires - the header then renders in place and never moves.
|
|
260
|
+
private readonly animatedValueListener = ({
|
|
261
|
+
value,
|
|
262
|
+
}: {
|
|
263
|
+
value: number | string;
|
|
264
|
+
}): void => {
|
|
265
|
+
dlog(
|
|
266
|
+
() =>
|
|
267
|
+
`STICKY[wrap] index=${this.childIndex} animated-tick value=${String(value)}`,
|
|
268
|
+
);
|
|
269
|
+
if (typeof value === 'number')
|
|
270
|
+
this.dispatch({ kind: 'animated-tick', value });
|
|
185
271
|
};
|
|
186
272
|
|
|
187
273
|
private props(): Record<string, unknown> {
|
|
@@ -192,7 +278,10 @@ class StickyProjectionWrapper {
|
|
|
192
278
|
const passthrough =
|
|
193
279
|
this.state.translateY === null
|
|
194
280
|
? undefined
|
|
195
|
-
: {
|
|
281
|
+
: {
|
|
282
|
+
transform: [{ translateY: this.state.translateY }],
|
|
283
|
+
zIndex: STICKY_HEADER_Z_INDEX,
|
|
284
|
+
};
|
|
196
285
|
return {
|
|
197
286
|
style: passthrough === undefined ? style : [style, passthrough],
|
|
198
287
|
onLayout: this.onLayout,
|
|
@@ -203,14 +292,40 @@ class StickyProjectionWrapper {
|
|
|
203
292
|
private applyProps(): void {
|
|
204
293
|
const props = this.props();
|
|
205
294
|
const reduced = reduceProps(props);
|
|
206
|
-
|
|
295
|
+
// Thunk: applyProps is on the scroll-driven path (every animated-tick while the pin is
|
|
296
|
+
// JS-driven), and JSON.stringify of a reduced transform is not free.
|
|
297
|
+
dlog(
|
|
298
|
+
() =>
|
|
299
|
+
`STICKY[wrap] index=${this.childIndex} applyProps translateY=${String(this.state.translateY)} ` +
|
|
300
|
+
`measured=${this.state.measured} layoutY=${this.state.layoutY} ` +
|
|
301
|
+
`scrollValueIsNative=${this.controller.config.scrollAnimatedValue.__isNative()} ` +
|
|
302
|
+
`reducedTransform=${JSON.stringify(reduced.transform ?? reduced.style)}`,
|
|
303
|
+
);
|
|
304
|
+
for (const [key, value] of Object.entries(reduced))
|
|
305
|
+
routeProp(this.node, key, value);
|
|
207
306
|
|
|
208
307
|
const next = new AnimatedProps(props);
|
|
209
308
|
next.__attach();
|
|
309
|
+
// No explicit __makeNative() here on purpose: __attach joins the leaf to the interpolation,
|
|
310
|
+
// which sits under the shared scroll value, and AnimatedWithChildren.__addChild promotes a
|
|
311
|
+
// child joining an ALREADY-native parent (graph.ts). So the pin is native exactly when the
|
|
312
|
+
// scroll value is - logged below, because "is this actually on the UI thread" is otherwise
|
|
313
|
+
// invisible until you read three files.
|
|
314
|
+
dlog(
|
|
315
|
+
() =>
|
|
316
|
+
`STICKY[wrap] index=${this.childIndex} leaf attached isNative=${next.__isNative()} ` +
|
|
317
|
+
`interpolationIsNative=${this.animatedTranslateY.__isNative()} ` +
|
|
318
|
+
`scrollValueIsNative=${this.controller.config.scrollAnimatedValue.__isNative()}`,
|
|
319
|
+
);
|
|
210
320
|
if (this.animatedProps !== undefined) this.animatedProps.__detach();
|
|
211
321
|
this.animatedProps = next;
|
|
212
322
|
this.cancelBind?.();
|
|
213
|
-
this.cancelBind = whenCommitted(this.node, () =>
|
|
323
|
+
this.cancelBind = whenCommitted(this.node, () => {
|
|
324
|
+
next.setNativeView(this.node);
|
|
325
|
+
dlog(
|
|
326
|
+
`STICKY[wrap] index=${this.childIndex} setNativeView isNative=${next.__isNative()}`,
|
|
327
|
+
);
|
|
328
|
+
});
|
|
214
329
|
}
|
|
215
330
|
}
|
|
216
331
|
|
|
@@ -220,13 +335,61 @@ export class ScrollViewProjectionController {
|
|
|
220
335
|
private readonly records: IProjectedRecord[] = [];
|
|
221
336
|
private readonly headerLayoutYs = new Map<number, number>();
|
|
222
337
|
|
|
338
|
+
// Diagnostic-only: one ScrollView can host several projection controllers over its lifetime,
|
|
339
|
+
// and an app has several ScrollViews at once. Without an id every log line below is
|
|
340
|
+
// indistinguishable between instances, which is what made a "logs never stop" report
|
|
341
|
+
// impossible to read as either a real loop or two lists warming up in turn.
|
|
342
|
+
private readonly instanceId = ++projectionControllerSeq;
|
|
343
|
+
private reconcileSeq = 0;
|
|
344
|
+
private isReconcilePending = false;
|
|
345
|
+
private shouldNotifyAll = false;
|
|
346
|
+
|
|
223
347
|
constructor(config: IScrollViewProjectionConfig) {
|
|
224
348
|
this.config = config;
|
|
225
349
|
}
|
|
226
350
|
|
|
227
351
|
update(config: IScrollViewProjectionConfig): void {
|
|
228
352
|
this.config = config;
|
|
229
|
-
|
|
353
|
+
// The one caller that must notify every wrapper: scrollViewHeight / invertStickyHeaders live in
|
|
354
|
+
// this config and reach the wrappers through nothing else.
|
|
355
|
+
this.scheduleReconcile(true);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Coalesce every reconcile request of one change-detection pass into a SINGLE walk.
|
|
360
|
+
*
|
|
361
|
+
* Cost is half the reason: the walk is O(records), so running it per projected mutation makes
|
|
362
|
+
* mounting M children O(M^2) - 801 children on the benchmark screen's PATH A. Correctness is the
|
|
363
|
+
* worse half. A windowed list feeds the controller through TWO channels that land at different
|
|
364
|
+
* moments in the same pass: children arrive through the renderer (insert/remove), while
|
|
365
|
+
* `stickyHeaderIndices` arrive as an ordinary `@Input` via `update()` from ngOnChanges. Reconciling
|
|
366
|
+
* on each mutation therefore guarantees at least one walk matching the NEW children against the
|
|
367
|
+
* OLD indices - and those indices are positions in the rendered child array
|
|
368
|
+
* (`stickyChildPositions`), which shift by 1-3 the moment VirtualizedList grows a leading spacer or
|
|
369
|
+
* a forced sticky cell. Every mismatch re-labels a header and re-parents engine nodes for nothing.
|
|
370
|
+
*
|
|
371
|
+
* The flush point is `RendererFactory2.end()`: the end of the pass, still SYNCHRONOUS. It must not
|
|
372
|
+
* be a microtask - engine mutations only `markDirty`, the commit is microtask-coalesced by the
|
|
373
|
+
* surface, and a reconcile queued after that commit mutates a tree nobody commits again. Headless
|
|
374
|
+
* the sticky wrapper then never reaches Fabric at all (six tests caught exactly this); on device it
|
|
375
|
+
* lands a frame late or not at all. `end()` runs inside `ApplicationRef.tick`, so it precedes every
|
|
376
|
+
* microtask queued during the pass.
|
|
377
|
+
*/
|
|
378
|
+
private scheduleReconcile(notifyAll = false): void {
|
|
379
|
+
countAngular('projectionSchedules');
|
|
380
|
+
this.shouldNotifyAll ||= notifyAll;
|
|
381
|
+
if (this.isReconcilePending) return;
|
|
382
|
+
this.isReconcilePending = true;
|
|
383
|
+
pendingProjections.add(this);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Called by the renderer factory's end(). Public only for that seam.
|
|
387
|
+
flushReconcile(): void {
|
|
388
|
+
countAngular('projectionFlushes');
|
|
389
|
+
this.isReconcilePending = false;
|
|
390
|
+
const notifyAll = this.shouldNotifyAll;
|
|
391
|
+
this.shouldNotifyAll = false;
|
|
392
|
+
this.reconcileStickyRecords(notifyAll);
|
|
230
393
|
}
|
|
231
394
|
|
|
232
395
|
bindContentNode(node: ISymbioteNode): void {
|
|
@@ -237,18 +400,29 @@ export class ScrollViewProjectionController {
|
|
|
237
400
|
contentProjection.set(node, this);
|
|
238
401
|
if (this.records.length === 0 && node.children.length > 0) {
|
|
239
402
|
for (const child of [...node.children]) {
|
|
240
|
-
this.records.push({
|
|
403
|
+
this.records.push({
|
|
404
|
+
child,
|
|
405
|
+
wrapper: undefined,
|
|
406
|
+
sticky: undefined,
|
|
407
|
+
stickyIndex: undefined,
|
|
408
|
+
});
|
|
409
|
+
projectedOwners.set(child, this);
|
|
241
410
|
}
|
|
242
411
|
}
|
|
243
|
-
this.
|
|
412
|
+
this.scheduleReconcile();
|
|
244
413
|
}
|
|
245
414
|
|
|
246
|
-
appendProjectedChild(
|
|
415
|
+
appendProjectedChild(
|
|
416
|
+
parent: ISymbioteNode,
|
|
417
|
+
child: ISymbioteNode,
|
|
418
|
+
insert: IInsert,
|
|
419
|
+
): void {
|
|
247
420
|
dlog(
|
|
248
421
|
`Angular ScrollView projection appendProjectedChild parent=${parent.component} child=${child.component} recordsBefore=${this.records.length}`,
|
|
249
422
|
);
|
|
250
423
|
const existing = this.records.find(record => record.child === child);
|
|
251
|
-
if (existing !== undefined)
|
|
424
|
+
if (existing !== undefined)
|
|
425
|
+
this.records.splice(this.records.indexOf(existing), 1);
|
|
252
426
|
const record: IProjectedRecord = {
|
|
253
427
|
child,
|
|
254
428
|
wrapper: undefined,
|
|
@@ -256,8 +430,9 @@ export class ScrollViewProjectionController {
|
|
|
256
430
|
stickyIndex: undefined,
|
|
257
431
|
};
|
|
258
432
|
this.records.push(record);
|
|
433
|
+
projectedOwners.set(child, this);
|
|
259
434
|
this.insertRecord(parent, record, undefined, insert);
|
|
260
|
-
this.
|
|
435
|
+
this.scheduleReconcile();
|
|
261
436
|
}
|
|
262
437
|
|
|
263
438
|
insertProjectedChild(
|
|
@@ -270,12 +445,14 @@ export class ScrollViewProjectionController {
|
|
|
270
445
|
`Angular ScrollView projection insertProjectedChild parent=${parent.component} child=${child.component} before=${beforeChild ? `${beforeChild.component}` : 'null'} recordsBefore=${this.records.length}`,
|
|
271
446
|
);
|
|
272
447
|
const existing = this.records.find(record => record.child === child);
|
|
273
|
-
if (existing !== undefined)
|
|
448
|
+
if (existing !== undefined)
|
|
449
|
+
this.records.splice(this.records.indexOf(existing), 1);
|
|
274
450
|
const beforeRecord =
|
|
275
451
|
beforeChild === null
|
|
276
452
|
? undefined
|
|
277
453
|
: this.records.find(
|
|
278
|
-
record =>
|
|
454
|
+
record =>
|
|
455
|
+
record.child === beforeChild || record.wrapper === beforeChild,
|
|
279
456
|
);
|
|
280
457
|
const record: IProjectedRecord = {
|
|
281
458
|
child,
|
|
@@ -283,35 +460,71 @@ export class ScrollViewProjectionController {
|
|
|
283
460
|
sticky: undefined,
|
|
284
461
|
stickyIndex: undefined,
|
|
285
462
|
};
|
|
463
|
+
// An insert-BEFORE whose anchor is not a record degrades into an append, here and again in
|
|
464
|
+
// insertRecord below - silently, which is how an ordering fault stays invisible until the screen
|
|
465
|
+
// is already wrong. A list that only ever appends (the benchmark screen's PATH A) never hits it;
|
|
466
|
+
// a virtualized list recycling cells into the middle does.
|
|
467
|
+
if (beforeChild !== null && beforeRecord === undefined) {
|
|
468
|
+
dlog(
|
|
469
|
+
() =>
|
|
470
|
+
`STICKY[proj#${this.instanceId}] ORDER insert-before anchor NOT a record ` +
|
|
471
|
+
`child=${child.component} before=${beforeChild.component} -> APPENDED to end`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
286
474
|
const index =
|
|
287
|
-
beforeRecord === undefined
|
|
475
|
+
beforeRecord === undefined
|
|
476
|
+
? this.records.length
|
|
477
|
+
: this.records.indexOf(beforeRecord);
|
|
288
478
|
this.records.splice(index, 0, record);
|
|
479
|
+
projectedOwners.set(child, this);
|
|
289
480
|
this.insertRecord(parent, record, beforeRecord, insert);
|
|
290
|
-
this.
|
|
481
|
+
this.scheduleReconcile();
|
|
291
482
|
}
|
|
292
483
|
|
|
293
484
|
removeProjectedChild(child: ISymbioteNode, remove: IRemove): boolean {
|
|
294
|
-
const record = this.records.find(
|
|
485
|
+
const record = this.records.find(
|
|
486
|
+
entry => entry.child === child || entry.wrapper === child,
|
|
487
|
+
);
|
|
295
488
|
if (record === undefined || this.contentNode === undefined) return false;
|
|
296
489
|
record.sticky?.destroy();
|
|
297
|
-
|
|
490
|
+
projectedOwners.delete(record.child);
|
|
298
491
|
const direct = directNode(record);
|
|
299
|
-
|
|
492
|
+
// The wrapper, when there is one, is the node actually parented to the content — detach
|
|
493
|
+
// whichever of the two carries the link, so a wrapped child never leaves its wrapper behind.
|
|
494
|
+
if (direct.parent !== undefined) remove(direct.parent, direct);
|
|
300
495
|
this.records.splice(this.records.indexOf(record), 1);
|
|
301
|
-
this.
|
|
496
|
+
this.scheduleReconcile();
|
|
302
497
|
return true;
|
|
303
498
|
}
|
|
304
499
|
|
|
305
500
|
nextStickyHeaderY(index: number): number | undefined {
|
|
306
501
|
const stickyIndices = this.config.stickyHeaderIndices ?? [];
|
|
307
502
|
const next = stickyIndices.find(entry => entry > index);
|
|
308
|
-
|
|
503
|
+
const y = next === undefined ? undefined : this.headerLayoutYs.get(next);
|
|
504
|
+
dlog(`STICKY[proj] nextStickyHeaderY(index=${index}) next=${next} y=${y}`);
|
|
505
|
+
return y;
|
|
309
506
|
}
|
|
310
507
|
|
|
311
508
|
recordHeaderLayoutY(index: number, y: number): void {
|
|
509
|
+
dlog(`STICKY[proj] recordHeaderLayoutY index=${index} y=${y}`);
|
|
312
510
|
if (this.headerLayoutYs.get(index) === y) return;
|
|
313
511
|
this.headerLayoutYs.set(index, y);
|
|
314
|
-
|
|
512
|
+
// EXACTLY ONE wrapper reads this value. `nextStickyHeaderY(i)` returns the first sticky index
|
|
513
|
+
// greater than i, so `index` is the "next header" of the closest sticky index BELOW it and of
|
|
514
|
+
// no one else. Broadcasting to all of them instead cost O(N) per layout and O(N^2) to settle a
|
|
515
|
+
// screen - 200 headers answered "ranges unchanged, skipped rebuild" 199 times per layout event,
|
|
516
|
+
// 40 000 dispatches to lay out the benchmark screen's PATH A.
|
|
517
|
+
const stickyIndices = this.config.stickyHeaderIndices ?? [];
|
|
518
|
+
let previous: number | undefined;
|
|
519
|
+
// Same ascending-order assumption nextStickyHeaderY already makes.
|
|
520
|
+
for (const entry of stickyIndices) {
|
|
521
|
+
if (entry >= index) break;
|
|
522
|
+
previous = entry;
|
|
523
|
+
}
|
|
524
|
+
if (previous === undefined) return;
|
|
525
|
+
this.records
|
|
526
|
+
.find(record => record.stickyIndex === previous)
|
|
527
|
+
?.sticky?.rebuild();
|
|
315
528
|
}
|
|
316
529
|
|
|
317
530
|
private insertRecord(
|
|
@@ -320,46 +533,104 @@ export class ScrollViewProjectionController {
|
|
|
320
533
|
beforeRecord: IProjectedRecord | undefined,
|
|
321
534
|
insert: IInsert,
|
|
322
535
|
): void {
|
|
323
|
-
const before =
|
|
536
|
+
const before =
|
|
537
|
+
beforeRecord === undefined ? undefined : directNode(beforeRecord);
|
|
324
538
|
dlog(
|
|
325
|
-
`
|
|
539
|
+
`STICKY[proj#${this.instanceId}] insertRecord child=${record.child.component} ` +
|
|
540
|
+
`before=${before ? before.component : 'append'} parentIsContentNode=${parent === this.contentNode}`,
|
|
326
541
|
);
|
|
327
542
|
if (before === undefined) insert(parent, record.child);
|
|
328
543
|
else insert(parent, record.child, before);
|
|
329
544
|
}
|
|
330
545
|
|
|
331
|
-
|
|
546
|
+
// notifyAll re-dispatches every sticky wrapper's inputs. Only a CONFIG change needs that
|
|
547
|
+
// (scrollViewHeight / invertStickyHeaders arrive through update() and reach the wrappers nowhere
|
|
548
|
+
// else). An INSERT must not: this walk runs once per projected child, so notifying N wrappers on
|
|
549
|
+
// each of M inserts is M x N - 801 x 200 = 160 200 dispatches to mount the benchmark screen's
|
|
550
|
+
// PATH A, versus 70 x 3 = 210 on the canary, which is why only the big screen dies. The two input
|
|
551
|
+
// changes an insert can actually cause are covered without a broadcast: a shifted childIndex by
|
|
552
|
+
// the stickyIndex branch below, and a moved neighbour by recordHeaderLayoutY.
|
|
553
|
+
private reconcileStickyRecords(notifyAll = false): void {
|
|
332
554
|
if (this.contentNode === undefined) return;
|
|
555
|
+
const seq = ++this.reconcileSeq;
|
|
333
556
|
dlog(
|
|
334
|
-
|
|
557
|
+
() =>
|
|
558
|
+
`STICKY[proj#${this.instanceId}] reconcile#${seq} records=${this.records.length} ` +
|
|
559
|
+
`children=${this.contentNode?.children.length} sticky=${JSON.stringify(this.config.stickyHeaderIndices ?? [])}`,
|
|
335
560
|
);
|
|
336
561
|
const stickyIndices = new Set(this.config.stickyHeaderIndices ?? []);
|
|
337
|
-
if (
|
|
562
|
+
if (
|
|
563
|
+
this.config.customStickyHeaderComponent !== undefined &&
|
|
564
|
+
stickyIndices.size > 0
|
|
565
|
+
) {
|
|
338
566
|
dlog(
|
|
339
|
-
'
|
|
567
|
+
'STICKY[proj] uses built-in sticky wrapper; custom StickyHeaderComponent is explicit-composition only',
|
|
340
568
|
);
|
|
341
569
|
}
|
|
342
570
|
|
|
343
571
|
let paintIndex = 0;
|
|
344
572
|
for (const record of [...this.records]) {
|
|
345
|
-
if (
|
|
346
|
-
this.
|
|
573
|
+
if (
|
|
574
|
+
this.config.excludeRefreshControl &&
|
|
575
|
+
isProjectedRefreshControl(record.child)
|
|
576
|
+
) {
|
|
577
|
+
// Removed INLINE, not through removeProjectedChild: that method ends with its own
|
|
578
|
+
// reconcileStickyRecords(), so calling it from inside this walk re-enters the walk while
|
|
579
|
+
// this.records is being spliced under it - one refresh control could then drive an
|
|
580
|
+
// unbounded recursion. Same removal, without the re-entry.
|
|
581
|
+
this.dropRecord(record);
|
|
347
582
|
continue;
|
|
348
583
|
}
|
|
349
|
-
const countsAsChild =
|
|
584
|
+
const countsAsChild =
|
|
585
|
+
!isAnchor(record.child) && !isProjectedRefreshControl(record.child);
|
|
350
586
|
const childIndex = paintIndex;
|
|
351
587
|
if (countsAsChild) paintIndex += 1;
|
|
352
588
|
const shouldWrap = countsAsChild && stickyIndices.has(childIndex);
|
|
353
|
-
|
|
354
|
-
|
|
589
|
+
// Deliberately NOT logged per child per walk: reconcile runs on every projected insert, so
|
|
590
|
+
// that shape is O(children x inserts) and buries the few lines that carry information. Only
|
|
591
|
+
// a state TRANSITION is logged, below.
|
|
592
|
+
if (shouldWrap && record.wrapper === undefined)
|
|
593
|
+
this.wrapRecord(record, childIndex);
|
|
594
|
+
else if (!shouldWrap && record.wrapper !== undefined)
|
|
595
|
+
this.unwrapRecord(record);
|
|
355
596
|
else if (shouldWrap && record.stickyIndex !== childIndex) {
|
|
356
|
-
|
|
357
|
-
record.sticky
|
|
597
|
+
countAngular('stickyWrapperReuses');
|
|
598
|
+
record.sticky?.setChildIndex(childIndex);
|
|
358
599
|
record.stickyIndex = childIndex;
|
|
359
|
-
} else if (shouldWrap) record.sticky?.rebuild();
|
|
600
|
+
} else if (shouldWrap && notifyAll) record.sticky?.rebuild();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// `paintIndex` above is counted off RECORDS, but the screen paints `contentNode.children`. The
|
|
604
|
+
// reconcile line at the top prints only the two COUNTS, which stay equal under any permutation
|
|
605
|
+
// - so it can never see this. Once the orders diverge, every sticky index addresses the wrong
|
|
606
|
+
// child and each wrap moves another node, so the fault compounds with scroll distance instead
|
|
607
|
+
// of correcting itself. Guarded by isDebug because it walks both lists.
|
|
608
|
+
if (!isDebug()) return;
|
|
609
|
+
const expected = this.records.map(record => directNode(record));
|
|
610
|
+
const painted = this.contentNode?.children ?? [];
|
|
611
|
+
const at = expected.findIndex((node, index) => node !== painted[index]);
|
|
612
|
+
if (at >= 0 || expected.length !== painted.length) {
|
|
613
|
+
dlog(
|
|
614
|
+
`STICKY[proj#${this.instanceId}] ORDER DIVERGED at ${at} ` +
|
|
615
|
+
`records=${expected.length} painted=${painted.length} ` +
|
|
616
|
+
`expected=${expected[at]?.component ?? 'none'} painted=${painted[at]?.component ?? 'none'}`,
|
|
617
|
+
);
|
|
360
618
|
}
|
|
361
619
|
}
|
|
362
620
|
|
|
621
|
+
// The removal half of removeProjectedChild, without its trailing reconcile - safe to call from
|
|
622
|
+
// inside the reconcile walk itself.
|
|
623
|
+
private dropRecord(record: IProjectedRecord): void {
|
|
624
|
+
if (this.contentNode === undefined) return;
|
|
625
|
+
record.sticky?.destroy();
|
|
626
|
+
projectedOwners.delete(record.child);
|
|
627
|
+
const direct = directNode(record);
|
|
628
|
+
if (direct.parent === this.contentNode)
|
|
629
|
+
removeChild(this.contentNode, direct);
|
|
630
|
+
const index = this.records.indexOf(record);
|
|
631
|
+
if (index !== -1) this.records.splice(index, 1);
|
|
632
|
+
}
|
|
633
|
+
|
|
363
634
|
// Auto projection runs after Angular has already created projected child host nodes. At this
|
|
364
635
|
// renderer boundary we can AOT-safely rearrange engine nodes, but we cannot instantiate an
|
|
365
636
|
// arbitrary Angular component class as a wrapper without owning a ViewContainerRef/injector and
|
|
@@ -370,23 +641,52 @@ export class ScrollViewProjectionController {
|
|
|
370
641
|
private wrapRecord(record: IProjectedRecord, childIndex: number): void {
|
|
371
642
|
if (this.contentNode === undefined) return;
|
|
372
643
|
const wrapper = createViewNode();
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
644
|
+
// The slot comes from `records`, the list that always knows where this record sits, never from
|
|
645
|
+
// the child's own current parent. Read off the child, a child not sitting DIRECTLY in the content
|
|
646
|
+
// has no slot to give up and the wrapper lands at the END - the wrapped cell jumps to the bottom
|
|
647
|
+
// of the scroll content, a silent reordering that compounds with scroll distance instead of
|
|
648
|
+
// correcting itself, and a windowed list reaches that state routinely. Angular CDK carries the
|
|
649
|
+
// same warning at its own insert site: insert AT the index, never insert-then-move, because an
|
|
650
|
+
// extra anchor node in between throws the move off (`cdk/scrolling/virtual-for-of.ts`,
|
|
651
|
+
// `_getEmbeddedViewArgs`) - and this file's tree is full of anchors.
|
|
652
|
+
//
|
|
653
|
+
// Order is load-bearing: place the wrapper at the slot FIRST, then move the child into it. The
|
|
654
|
+
// engine's appendChild detaches the child from its old parent, so the wrapper slides into the
|
|
655
|
+
// position the child vacates and no explicit removeChild is needed.
|
|
656
|
+
const after = this.nextDirectAfter(record);
|
|
657
|
+
if (after === undefined) appendChild(this.contentNode, wrapper);
|
|
658
|
+
else insertBefore(this.contentNode, wrapper, after);
|
|
381
659
|
appendChild(wrapper, record.child);
|
|
382
660
|
record.wrapper = wrapper;
|
|
661
|
+
countAngular('stickyWrapperCreates');
|
|
383
662
|
record.sticky = new StickyProjectionWrapper(this, childIndex, wrapper);
|
|
384
663
|
record.stickyIndex = childIndex;
|
|
385
|
-
|
|
664
|
+
dlog(
|
|
665
|
+
`STICKY[proj#${this.instanceId}] WRAP child=${record.child.component} paintIndex=${childIndex}`,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// The painted node of the first record AFTER `record` that is actually parented to the content —
|
|
670
|
+
// i.e. the anchor the wrapper must be inserted before. Records whose node has not been inserted
|
|
671
|
+
// yet are skipped rather than treated as a slot: a record exists from the moment the renderer
|
|
672
|
+
// reports the child, which can precede its placement.
|
|
673
|
+
private nextDirectAfter(record: IProjectedRecord): ISymbioteNode | undefined {
|
|
674
|
+
for (
|
|
675
|
+
let index = this.records.indexOf(record) + 1;
|
|
676
|
+
index < this.records.length;
|
|
677
|
+
index += 1
|
|
678
|
+
) {
|
|
679
|
+
const node = directNode(this.records[index]);
|
|
680
|
+
if (node.parent === this.contentNode) return node;
|
|
681
|
+
}
|
|
682
|
+
return undefined;
|
|
386
683
|
}
|
|
387
684
|
|
|
388
685
|
private unwrapRecord(record: IProjectedRecord): void {
|
|
389
686
|
if (this.contentNode === undefined || record.wrapper === undefined) return;
|
|
687
|
+
dlog(
|
|
688
|
+
`STICKY[proj#${this.instanceId}] UNWRAP child=${record.child.component}`,
|
|
689
|
+
);
|
|
390
690
|
record.sticky?.destroy();
|
|
391
691
|
record.sticky = undefined;
|
|
392
692
|
record.stickyIndex = undefined;
|
|
@@ -394,7 +694,6 @@ export class ScrollViewProjectionController {
|
|
|
394
694
|
insertBefore(this.contentNode, record.child, wrapper);
|
|
395
695
|
removeChild(this.contentNode, wrapper);
|
|
396
696
|
record.wrapper = undefined;
|
|
397
|
-
projectedWrappers.delete(record.child);
|
|
398
697
|
}
|
|
399
698
|
}
|
|
400
699
|
|
|
@@ -404,11 +703,11 @@ export function getScrollViewProjection(
|
|
|
404
703
|
return contentProjection.get(node);
|
|
405
704
|
}
|
|
406
705
|
|
|
407
|
-
export function removeScrollViewProjectedChild(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
706
|
+
export function removeScrollViewProjectedChild(
|
|
707
|
+
child: ISymbioteNode,
|
|
708
|
+
remove: IRemove,
|
|
709
|
+
): boolean {
|
|
710
|
+
return (
|
|
711
|
+
projectedOwners.get(child)?.removeProjectedChild(child, remove) ?? false
|
|
712
|
+
);
|
|
414
713
|
}
|