@ng-native/components 0.0.1 → 0.1.1

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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -1
  3. package/dist/activity-indicator.d.ts +41 -0
  4. package/dist/activity-indicator.js +66 -0
  5. package/dist/activity-indicator.js.map +1 -0
  6. package/dist/animated-style.d.ts +31 -0
  7. package/dist/animated-style.js +102 -0
  8. package/dist/animated-style.js.map +1 -0
  9. package/dist/animated.d.ts +125 -0
  10. package/dist/animated.js +538 -0
  11. package/dist/animated.js.map +1 -0
  12. package/dist/animation.d.ts +40 -0
  13. package/dist/animation.js +20 -0
  14. package/dist/animation.js.map +1 -0
  15. package/dist/animations-web.d.ts +8 -0
  16. package/dist/animations-web.js +31 -0
  17. package/dist/animations-web.js.map +1 -0
  18. package/dist/animations.d.ts +7 -0
  19. package/dist/animations.js +58 -0
  20. package/dist/animations.js.map +1 -0
  21. package/dist/controlled-model.d.ts +33 -0
  22. package/dist/controlled-model.js +47 -0
  23. package/dist/controlled-model.js.map +1 -0
  24. package/dist/easing.d.ts +21 -0
  25. package/dist/easing.js +50 -0
  26. package/dist/easing.js.map +1 -0
  27. package/dist/events.d.ts +169 -0
  28. package/dist/events.js +2 -0
  29. package/dist/events.js.map +1 -0
  30. package/dist/gesture-backend.d.ts +26 -0
  31. package/dist/gesture-backend.js +14 -0
  32. package/dist/gesture-backend.js.map +1 -0
  33. package/dist/gesture-dispatch.d.ts +67 -0
  34. package/dist/gesture-dispatch.js +106 -0
  35. package/dist/gesture-dispatch.js.map +1 -0
  36. package/dist/gestures.d.ts +23 -0
  37. package/dist/gestures.js +164 -0
  38. package/dist/gestures.js.map +1 -0
  39. package/dist/height-index.d.ts +31 -0
  40. package/dist/height-index.js +80 -0
  41. package/dist/height-index.js.map +1 -0
  42. package/dist/image-background.d.ts +32 -0
  43. package/dist/image-background.js +80 -0
  44. package/dist/image-background.js.map +1 -0
  45. package/dist/image.d.ts +79 -0
  46. package/dist/image.js +155 -0
  47. package/dist/image.js.map +1 -0
  48. package/dist/index.d.ts +38 -0
  49. package/dist/index.js +39 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/input-accessory-view.d.ts +16 -0
  52. package/dist/input-accessory-view.js +29 -0
  53. package/dist/input-accessory-view.js.map +1 -0
  54. package/dist/keyboard-avoiding-view.d.ts +79 -0
  55. package/dist/keyboard-avoiding-view.js +172 -0
  56. package/dist/keyboard-avoiding-view.js.map +1 -0
  57. package/dist/keyboard-dock.d.ts +126 -0
  58. package/dist/keyboard-dock.js +327 -0
  59. package/dist/keyboard-dock.js.map +1 -0
  60. package/dist/keyboard-taps.d.ts +12 -0
  61. package/dist/keyboard-taps.js +51 -0
  62. package/dist/keyboard-taps.js.map +1 -0
  63. package/dist/modal.d.ts +56 -0
  64. package/dist/modal.js +102 -0
  65. package/dist/modal.js.map +1 -0
  66. package/dist/native-gesture.d.ts +21 -0
  67. package/dist/native-gesture.js +56 -0
  68. package/dist/native-gesture.js.map +1 -0
  69. package/dist/native-ref.d.ts +7 -0
  70. package/dist/native-ref.js +13 -0
  71. package/dist/native-ref.js.map +1 -0
  72. package/dist/pressable.d.ts +220 -0
  73. package/dist/pressable.js +484 -0
  74. package/dist/pressable.js.map +1 -0
  75. package/dist/reanimated.d.ts +19 -0
  76. package/dist/reanimated.js +111 -0
  77. package/dist/reanimated.js.map +1 -0
  78. package/dist/refresh-control.d.ts +50 -0
  79. package/dist/refresh-control.js +103 -0
  80. package/dist/refresh-control.js.map +1 -0
  81. package/dist/safe-area-provider.d.ts +11 -0
  82. package/dist/safe-area-provider.js +70 -0
  83. package/dist/safe-area-provider.js.map +1 -0
  84. package/dist/safe-area-view.d.ts +22 -0
  85. package/dist/safe-area-view.js +77 -0
  86. package/dist/safe-area-view.js.map +1 -0
  87. package/dist/safe-area.d.ts +12 -0
  88. package/dist/safe-area.js +27 -0
  89. package/dist/safe-area.js.map +1 -0
  90. package/dist/scroll-view-props.d.ts +89 -0
  91. package/dist/scroll-view-props.js +153 -0
  92. package/dist/scroll-view-props.js.map +1 -0
  93. package/dist/scroll-view.d.ts +95 -0
  94. package/dist/scroll-view.js +181 -0
  95. package/dist/scroll-view.js.map +1 -0
  96. package/dist/section-list.d.ts +146 -0
  97. package/dist/section-list.js +300 -0
  98. package/dist/section-list.js.map +1 -0
  99. package/dist/sticky-headers.d.ts +41 -0
  100. package/dist/sticky-headers.js +118 -0
  101. package/dist/sticky-headers.js.map +1 -0
  102. package/dist/swipe-refresh-layout.d.ts +51 -0
  103. package/dist/swipe-refresh-layout.js +162 -0
  104. package/dist/swipe-refresh-layout.js.map +1 -0
  105. package/dist/switch.d.ts +54 -0
  106. package/dist/switch.js +110 -0
  107. package/dist/switch.js.map +1 -0
  108. package/dist/template-slot.d.ts +18 -0
  109. package/dist/template-slot.js +30 -0
  110. package/dist/template-slot.js.map +1 -0
  111. package/dist/text-input.d.ts +168 -0
  112. package/dist/text-input.js +312 -0
  113. package/dist/text-input.js.map +1 -0
  114. package/dist/text.d.ts +70 -0
  115. package/dist/text.js +111 -0
  116. package/dist/text.js.map +1 -0
  117. package/dist/touchable-opacity.d.ts +36 -0
  118. package/dist/touchable-opacity.js +76 -0
  119. package/dist/touchable-opacity.js.map +1 -0
  120. package/dist/transforms.d.ts +2 -0
  121. package/dist/transforms.js +17 -0
  122. package/dist/transforms.js.map +1 -0
  123. package/dist/view-base.d.ts +332 -0
  124. package/dist/view-base.js +534 -0
  125. package/dist/view-base.js.map +1 -0
  126. package/dist/view.d.ts +12 -0
  127. package/dist/view.js +18 -0
  128. package/dist/view.js.map +1 -0
  129. package/dist/virtual-list.d.ts +530 -0
  130. package/dist/virtual-list.js +1150 -0
  131. package/dist/virtual-list.js.map +1 -0
  132. package/dist/worklet-scroll.d.ts +13 -0
  133. package/dist/worklet-scroll.js +49 -0
  134. package/dist/worklet-scroll.js.map +1 -0
  135. package/dist/worklet-style.d.ts +13 -0
  136. package/dist/worklet-style.js +47 -0
  137. package/dist/worklet-style.js.map +1 -0
  138. package/dist/worklets.d.ts +89 -0
  139. package/dist/worklets.js +42 -0
  140. package/dist/worklets.js.map +1 -0
  141. package/dist/written-props.d.ts +6 -0
  142. package/dist/written-props.js +180 -0
  143. package/dist/written-props.js.map +1 -0
  144. package/package.json +88 -3
@@ -0,0 +1,1150 @@
1
+ import { Component, DestroyRef, Directive, ElementRef, TemplateRef, afterEveryRender, booleanAttribute, computed, contentChild, inject, input, linkedSignal, numberAttribute, output, signal, viewChild, } from '@angular/core';
2
+ import { nativePlatform, pinnedRange, } from '@ng-native/fabric';
3
+ import { HeightIndex } from "./height-index.js";
4
+ import { dismissKeyboardOnTap } from "./keyboard-taps.js";
5
+ import { RefreshControl } from "./refresh-control.js";
6
+ import { TemplateSlot } from "./template-slot.js";
7
+ import { View } from "./view.js";
8
+ import { ScrollViewProps } from "./scroll-view-props.js";
9
+ import * as i0 from "@angular/core";
10
+ /**
11
+ * Marks the template drawn between two rows: `<ng-template virtualListSeparator let-leading>`.
12
+ * FlatList's `ItemSeparatorComponent`.
13
+ */
14
+ export class VirtualListSeparator {
15
+ template = inject(TemplateRef);
16
+ static ngTemplateContextGuard(_directive, context) {
17
+ return true;
18
+ }
19
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualListSeparator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
20
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.2.0", type: VirtualListSeparator, isStandalone: true, selector: "ng-template[virtualListSeparator]", ngImport: i0 });
21
+ }
22
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualListSeparator, decorators: [{
23
+ type: Directive,
24
+ args: [{ selector: 'ng-template[virtualListSeparator]' }]
25
+ }] });
26
+ /** A parked slot: out of layout, out of sight, and out of the accessibility tree. */
27
+ const PARKED = Object.freeze({ display: 'none' });
28
+ /** How many hidden slots of each row type are kept ready for reuse. */
29
+ const PARK_LIMIT = 6;
30
+ /** The style of a row that sizes itself and needs nothing else: in flow, where it falls. */
31
+ const IN_FLOW = Object.freeze({});
32
+ /** Points of drift below which a held position is left alone, as UIKit rounds to half-points. */
33
+ const HOLD_TOLERANCE = 0.5;
34
+ /**
35
+ * Windowed list: a native scroll view whose content is sized to the full list, with only the
36
+ * visible slice rendered. Commits as `RCTScrollView`.
37
+ *
38
+ * RN's FlatList, SectionList and VirtualizedList are a large React component tree, and none of
39
+ * it is reachable. This is the small equivalent. Angular's own `@for` does the rendering, so
40
+ * tracking, reuse and destruction all behave normally, and the loop stays in the caller's
41
+ * template:
42
+ *
43
+ * ```html
44
+ * <virtual-list #list [items]="rows()" [itemHeight]="44" (endReached)="loadMore()">
45
+ * <text listHeader>Header</text>
46
+ * @for (row of list.window(); track row.slot) {
47
+ * <view [virtualListRow]="row"><text>{{ row.item.label }}</text></view>
48
+ * }
49
+ * <text listFooter>Footer</text>
50
+ * </virtual-list>
51
+ * ```
52
+ *
53
+ * With `itemHeight`, every row's size is known up front and each is absolutely positioned at its
54
+ * offset. Without it, rows size themselves, as a feed's posts and a chat's messages do: the window
55
+ * is laid out in normal flow after a spacer standing in for the rows above it, so native places
56
+ * every row correctly in the frame it first appears, and each row's layout tells the list its
57
+ * height for sizing what is not rendered. Until a row has been measured it counts as
58
+ * `estimatedItemHeight`. Measurements are kept by `keyExtractor`, so they follow their item across
59
+ * inserts, removals and moves. `virtualListRow` on each row's view applies its style and reports
60
+ * its layout; `[style]="row.style"` still works for fixed heights, which need no measuring.
61
+ *
62
+ * Content marked `listHeader` and `listFooter` renders above and below the rows in normal flow,
63
+ * as FlatList's header and footer components do. A `<refresh-control>` projects like any child.
64
+ *
65
+ * `<ng-template virtualListSeparator let-leading let-trailing="trailingItem">` is drawn between
66
+ * each pair of rows and not after the last, as FlatList's `ItemSeparatorComponent` is. It is drawn
67
+ * inside the leading row's slot, at its trailing edge, so `itemHeight` includes it the way
68
+ * `getItemLayout`'s `length` includes it in RN; a measured row has it drawn over its trailing edge
69
+ * once its height is known. The slot it sits in lets touches through to the row underneath.
70
+ *
71
+ * `horizontal` swaps the axis: rows are placed by `left` and sized by `width`, and the offset is
72
+ * read from `contentOffset.x`. `inverted` is a scale of -1 on the list and on each row, which is
73
+ * how FlatList does it too - the list scrolls from the bottom and each row is flipped back the
74
+ * right way up, so a chat transcript needs no reversed array.
75
+ *
76
+ * `stickyIndices` pins a row to the leading edge while the rows under it scroll past, which is
77
+ * FlatList's `stickyHeaderIndices` counted without the header. `stickyHeader` pins the
78
+ * `listHeader` content the same way, FlatList's index 0 when it has a `ListHeaderComponent`, and
79
+ * the first sticky row pushes it off as RN's sticky headers push each other. Neither is native:
80
+ * RN's `ScrollView.js` translates a sticky child against the scroll offset in JavaScript, and a
81
+ * row here follows the offset the same way while it is the current one - by its `top` when
82
+ * absolutely positioned, by a transform when in flow.
83
+ *
84
+ * What is on screen stays on screen when a measured row above it turns out taller or shorter than
85
+ * its estimate: the offset is corrected by the difference. `maintainVisibleContentPosition` does
86
+ * the same for rows inserted or removed above it, which a feed receiving new posts and a chat
87
+ * loading older messages above the ones being read both need.
88
+ *
89
+ * ponytail: viewability is `itemVisiblePercentThreshold` only: `minimumViewTime` and
90
+ * `waitForInteraction` are timing policy on top of the same measurement, and nothing has needed
91
+ * them. A separator is not told `highlighted`, which RN drives from the row's press state. A
92
+ * sticky header follows the scroll a commit behind native, where RN's is on the native driver. A
93
+ * held position is restored by a `scrollTo` after the commit, not in the mount as native
94
+ * `maintainVisibleContentPosition` is, so a correction made mid-fling lands where the last scroll
95
+ * event said the list was.
96
+ */
97
+ export class VirtualList extends ScrollViewProps {
98
+ items = input.required(/* @ts-ignore */
99
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
100
+ /**
101
+ * A fixed height, or a function for variable ones, when every row's size is known up front. The
102
+ * function form builds a cumulative offset table and binary-searches it, which is what section
103
+ * lists need: a header row and an item row are different heights, so uniform maths cannot place
104
+ * them. Leave it unset for rows that size themselves.
105
+ */
106
+ itemHeight = input(/* @ts-ignore */
107
+ ...(ngDevMode ? [undefined, { debugName: "itemHeight" }] : /* istanbul ignore next */ []));
108
+ /**
109
+ * What a row that sizes itself counts as until it has been measured. The closer it is, the more
110
+ * honest the scroll bar is and the less a jump to an unmeasured row has to correct.
111
+ */
112
+ estimatedItemHeight = input(50, /* @ts-ignore */
113
+ ...(ngDevMode ? [{ debugName: "estimatedItemHeight" }] : /* istanbul ignore next */ []));
114
+ /**
115
+ * An item's identity, as FlatList's `keyExtractor`: what a measured height, a slot and a held
116
+ * position follow across an insert. Without it an item is its own key, which is enough for items
117
+ * that are replaced, not copied, when they change.
118
+ */
119
+ keyExtractor = input(/* @ts-ignore */
120
+ ...(ngDevMode ? [undefined, { debugName: "keyExtractor" }] : /* istanbul ignore next */ []));
121
+ /**
122
+ * What kind of row an item is, when rows of different kinds share the list: a text post and a
123
+ * photo post, a message and a date divider. A slot is only ever recycled into a row of the same
124
+ * type, so its views are reused rather than torn down for another kind's, as UIKit's reuse
125
+ * identifiers and FlashList's `getItemType` do.
126
+ */
127
+ itemType = input(/* @ts-ignore */
128
+ ...(ngDevMode ? [undefined, { debugName: "itemType" }] : /* istanbul ignore next */ []));
129
+ /** Keep what is on screen still when rows are inserted or removed before it. */
130
+ maintainVisibleContentPosition = input(/* @ts-ignore */
131
+ ...(ngDevMode ? [undefined, { debugName: "maintainVisibleContentPosition" }] : /* istanbul ignore next */ []));
132
+ /** Rows rendered beyond each edge of the viewport, to cover fling gaps. */
133
+ overscan = input(4, { ...(ngDevMode ? { debugName: "overscan" } : /* istanbul ignore next */ {}), transform: numberAttribute });
134
+ /** How close to the end, in viewport heights, `(endReached)` fires. RN's default is 2. */
135
+ endReachedThreshold = input(2, { ...(ngDevMode ? { debugName: "endReachedThreshold" } : /* istanbul ignore next */ {}), transform: numberAttribute });
136
+ /** Lay the rows out along x instead of y, and scroll that way. */
137
+ horizontal = input(false, { ...(ngDevMode ? { debugName: "horizontal" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
138
+ /** Start at the end and scroll backwards, with every row flipped back the right way up. */
139
+ inverted = input(false, { ...(ngDevMode ? { debugName: "inverted" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
140
+ /**
141
+ * Rows that pin to the leading edge while the ones under them scroll past. Ascending; a row is
142
+ * pinned from the moment it would leave until the next sticky row pushes it off.
143
+ */
144
+ stickyIndices = input([], /* @ts-ignore */
145
+ ...(ngDevMode ? [{ debugName: "stickyIndices" }] : /* istanbul ignore next */ []));
146
+ /** Pin the `listHeader` content to the leading edge until the first sticky row pushes it off. */
147
+ stickyHeader = input(false, { ...(ngDevMode ? { debugName: "stickyHeader" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
148
+ /** What a tap in the list does while a text input has the keyboard up. See `ScrollView`. */
149
+ keyboardShouldPersistTaps = input('never', /* @ts-ignore */
150
+ ...(ngDevMode ? [{ debugName: "keyboardShouldPersistTaps" }] : /* istanbul ignore next */ []));
151
+ /** How much of a row must be on screen to count as viewable. RN's default is 0, meaning any. */
152
+ itemVisiblePercentThreshold = input(0, { ...(ngDevMode ? { debugName: "itemVisiblePercentThreshold" } : /* istanbul ignore next */ {}), transform: numberAttribute });
153
+ /**
154
+ * The scroll position is within `endReachedThreshold` of the end: once per item count, and again
155
+ * after the list has scrolled away from the end and come back.
156
+ */
157
+ endReached = output();
158
+ /** Which rows are on screen, and what changed since last time. */
159
+ viewableItemsChanged = output();
160
+ viewport = signal(0, /* @ts-ignore */
161
+ ...(ngDevMode ? [{ debugName: "viewport" }] : /* istanbul ignore next */ []));
162
+ /** The leading edge of the viewport in content space, which sticky rows and viewability need. */
163
+ offset = signal(0, /* @ts-ignore */
164
+ ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
165
+ headerHeight = signal(0, /* @ts-ignore */
166
+ ...(ngDevMode ? [{ debugName: "headerHeight" }] : /* istanbul ignore next */ []));
167
+ /** What native reported the content measures along the axis, once it has. */
168
+ contentSize = 0;
169
+ /** The item count `endReached` last fired for, cleared once the list is away from the end. */
170
+ endReachedFor = -1;
171
+ /** Slot styles are cached so a scroll does not hand Fabric new prop objects for stable rows. */
172
+ slots = new Map();
173
+ /** A separator's slot style, per row slot style, so it is as stable as the row's own. */
174
+ gapStyles = new WeakMap();
175
+ /** The same for a measured row's separator, by key, while its place and size stay put. */
176
+ measuredGaps = new Map();
177
+ separator = contentChild(VirtualListSeparator, /* @ts-ignore */
178
+ ...(ngDevMode ? [{ debugName: "separator" }] : /* istanbul ignore next */ []));
179
+ refreshControl = contentChild(RefreshControl, /* @ts-ignore */
180
+ ...(ngDevMode ? [{ debugName: "refreshControl" }] : /* istanbul ignore next */ []));
181
+ /** See ScrollView: inside Android's swipe layout the list must see a drag first. */
182
+ nestedScroll = computed(() => nativePlatform() === 'android' && this.refreshControl() ? true : undefined, /* @ts-ignore */
183
+ ...(ngDevMode ? [{ debugName: "nestedScroll" }] : /* istanbul ignore next */ []));
184
+ /**
185
+ * Whether native moves sticky rows. Each then stays at its own place in the list and native
186
+ * translates it on every frame the list scrolls, as RN's `ScrollViewStickyHeader` does; a
187
+ * translate committed from each scroll event lands a frame or more late, and on a fast fling
188
+ * the row shudders and comes away from the edge.
189
+ */
190
+ nativeSticky = this.engine.drivesScroll;
191
+ /** The offset once scrolling pauses, which the settled translate of a native-moved row is from. */
192
+ settled = signal(0, /* @ts-ignore */
193
+ ...(ngDevMode ? [{ debugName: "settled" }] : /* istanbul ignore next */ []));
194
+ settleTimer;
195
+ /** What native is moving, by row view, with the transform kept beside the translate. */
196
+ drives = new Map();
197
+ canvasView = viewChild.required('rows', { ...(ngDevMode ? { debugName: "canvasView" } : /* istanbul ignore next */ {}), read: ElementRef });
198
+ /** Whether the scroll offset is needed as a signal, which only pinning needs. */
199
+ pinning = computed(() => this.stickyHeader() || this.stickyIndices().length > 0, /* @ts-ignore */
200
+ ...(ngDevMode ? [{ debugName: "pinning" }] : /* istanbul ignore next */ []));
201
+ /** Rows size themselves and are laid out in flow, rather than placed at a known offset. */
202
+ measuring = computed(() => this.itemHeight() === undefined, /* @ts-ignore */
203
+ ...(ngDevMode ? [{ debugName: "measuring" }] : /* istanbul ignore next */ []));
204
+ /** Heights native has reported, by key. */
205
+ measured = new Map();
206
+ /** Bumped when a measurement changes, which is what re-sizes the list. */
207
+ measuredVersion = signal(0, /* @ts-ignore */
208
+ ...(ngDevMode ? [{ debugName: "measuredVersion" }] : /* istanbul ignore next */ []));
209
+ /** Each item's key, or null when items are their own. */
210
+ keys = computed(() => {
211
+ const extract = this.keyExtractor();
212
+ return extract ? this.items().map(extract) : null;
213
+ }, /* @ts-ignore */
214
+ ...(ngDevMode ? [{ debugName: "keys" }] : /* istanbul ignore next */ []));
215
+ keyAt(index) {
216
+ return this.keys()?.[index] ?? this.items()[index];
217
+ }
218
+ /**
219
+ * Each row's size and where it starts, for variable heights; uniform rows need no index at all.
220
+ * Built once per change of items. A measurement changes it in place and bumps
221
+ * `measuredVersion`, which is what everything that reads it depends on, so a row laying out
222
+ * costs a walk of the tree rather than a rebuild of every offset.
223
+ */
224
+ table = computed(() => {
225
+ const height = this.itemHeight();
226
+ if (typeof height === 'number')
227
+ return null;
228
+ const items = this.items();
229
+ const sizes = new Float64Array(items.length);
230
+ if (height) {
231
+ for (let i = 0; i < items.length; i++)
232
+ sizes[i] = height(items[i], i);
233
+ return new HeightIndex(sizes);
234
+ }
235
+ const estimate = this.estimatedItemHeight();
236
+ const keys = this.keys();
237
+ for (let i = 0; i < items.length; i++) {
238
+ sizes[i] =
239
+ this.measured.get(keys ? keys[i] : items[i]) ??
240
+ (typeof estimate === 'number' ? estimate : estimate(items[i], i));
241
+ }
242
+ return new HeightIndex(sizes);
243
+ }, /* @ts-ignore */
244
+ ...(ngDevMode ? [{ debugName: "table" }] : /* istanbul ignore next */ []));
245
+ /** Where each key is, so a measurement finds its row without a search. */
246
+ indexOfKey = computed(() => {
247
+ if (!this.measuring())
248
+ return null;
249
+ const map = new Map();
250
+ (this.keys() ?? this.items()).forEach((key, index) => map.set(key, index));
251
+ return map;
252
+ }, /* @ts-ignore */
253
+ ...(ngDevMode ? [{ debugName: "indexOfKey" }] : /* istanbul ignore next */ []));
254
+ /** The index, with a dependency on its measurements for whoever is reading it. */
255
+ layout() {
256
+ const table = this.table();
257
+ if (table)
258
+ this.measuredVersion();
259
+ return table;
260
+ }
261
+ /** The list's own total extent along its axis. */
262
+ extent = computed(() => {
263
+ const table = this.layout();
264
+ if (table)
265
+ return table.total;
266
+ return this.items().length * this.itemHeight();
267
+ }, /* @ts-ignore */
268
+ ...(ngDevMode ? [{ debugName: "extent" }] : /* istanbul ignore next */ []));
269
+ canvas = computed(() => this.horizontal() ? { width: this.extent(), flexDirection: 'row' } : { height: this.extent() }, /* @ts-ignore */
270
+ ...(ngDevMode ? [{ debugName: "canvas" }] : /* istanbul ignore next */ []));
271
+ /**
272
+ * `scaleY: -1` on the list, and the same on every row.
273
+ *
274
+ * The list ends up scrolled from the far end with its rows in reverse, and the second flip puts
275
+ * each row's own content back the right way up. It is what `FlatList` does, and it is why an
276
+ * inverted list needs no reversed array and no reversed maths.
277
+ */
278
+ flip = computed(() => {
279
+ if (!this.inverted())
280
+ return undefined;
281
+ const scale = this.horizontal() ? { scaleX: -1 } : { scaleY: -1 };
282
+ return [scale];
283
+ }, /* @ts-ignore */
284
+ ...(ngDevMode ? [{ debugName: "flip" }] : /* istanbul ignore next */ []));
285
+ /**
286
+ * The flip as a style's transform: CSS text, which the engine turns into the list native reads.
287
+ * A style binding validates its values as the DOM's, and warns on a list in development.
288
+ */
289
+ flipText = computed(() => {
290
+ if (!this.inverted())
291
+ return undefined;
292
+ return this.horizontal() ? 'scaleX(-1)' : 'scaleY(-1)';
293
+ }, /* @ts-ignore */
294
+ ...(ngDevMode ? [{ debugName: "flipText" }] : /* istanbul ignore next */ []));
295
+ /** A row in flow, flipped back when the list is inverted. One object, so rows never re-clone. */
296
+ flowStyle = computed(() => {
297
+ const flip = this.flipText();
298
+ return flip ? { transform: flip } : IN_FLOW;
299
+ }, /* @ts-ignore */
300
+ ...(ngDevMode ? [{ debugName: "flowStyle" }] : /* istanbul ignore next */ []));
301
+ /** Top of a row. */
302
+ topOf(index) {
303
+ const table = this.layout();
304
+ return table ? table.topOf(index) : index * this.itemHeight();
305
+ }
306
+ heightOf(index) {
307
+ const table = this.layout();
308
+ return table ? table.heightOf(index) : this.itemHeight();
309
+ }
310
+ /** First row whose bottom is past `y`. */
311
+ indexAt(y) {
312
+ const table = this.layout();
313
+ if (!table)
314
+ return Math.max(0, Math.floor(y / this.itemHeight()));
315
+ return table.indexAt(y);
316
+ }
317
+ // --- holding position -------------------------------------------------------------------
318
+ /** The row the viewport's leading edge is in, taken at each scroll. */
319
+ anchor = null;
320
+ /** The last offset seen. A field rather than a signal: reading it renders nothing. */
321
+ lastOffset = 0;
322
+ /**
323
+ * A correction to make once the commit that needs it is on its way to native: an offset in the
324
+ * rows' own space, after the header, or the very start of the list.
325
+ */
326
+ pendingScroll = null;
327
+ /** What moves the rows: which items, and where each one is. */
328
+ placement = computed(() => ({
329
+ items: this.items(),
330
+ table: this.table(),
331
+ version: this.measuredVersion(),
332
+ }), /* @ts-ignore */
333
+ ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
334
+ /**
335
+ * The first *visible* row, not the first rendered one, and not the raw scroll offset. A fling
336
+ * produces a scroll event per frame, and setting a signal on each one costs a change-detection
337
+ * pass even when the visible rows are identical. Deriving the index in the handler means most
338
+ * scroll events change nothing and cost nothing.
339
+ *
340
+ * Linked to the rows' placement, because that is when the row on screen can move without the
341
+ * user scrolling: an insert above it, or a row above it measuring differently from its estimate.
342
+ * The index follows the anchored row there, and the offset follows it too.
343
+ */
344
+ visible = linkedSignal({ ...(ngDevMode ? { debugName: "visible" } : /* istanbul ignore next */ {}), source: this.placement,
345
+ computation: (placement) => this.reanchor(placement.items) });
346
+ /** Take the anchor at a row, keeping the viewport where it is. */
347
+ anchorAt(at, offset) {
348
+ const items = this.items();
349
+ if (!items.length) {
350
+ this.anchor = null;
351
+ return;
352
+ }
353
+ const index = Math.min(at, items.length - 1);
354
+ this.anchor = { key: this.keyAt(index), within: offset - this.topOf(index), items, index };
355
+ }
356
+ /**
357
+ * Where the row on screen went when the rows moved under it, and the offset that keeps it where
358
+ * the user left it. An insert or a removal is only held when the caller asked for it; a row
359
+ * above measuring differently always is, since nothing the user did moved anything.
360
+ */
361
+ reanchor(items) {
362
+ const anchor = this.anchor;
363
+ // Empty for a moment, as a filter typed through leaves it: the offset is kept for what comes.
364
+ if (!items.length)
365
+ return 0;
366
+ if (!anchor)
367
+ return this.atOffset();
368
+ const index = this.heldIndex(anchor, items);
369
+ if (index === 'start')
370
+ return this.showStart();
371
+ if (index < 0)
372
+ return this.atOffset();
373
+ const held = this.topOf(index) + anchor.within;
374
+ if (Math.abs(held - this.lastOffset) > HOLD_TOLERANCE) {
375
+ this.pendingScroll = { offset: held, animated: false };
376
+ this.lastOffset = held;
377
+ }
378
+ this.anchor = { ...anchor, items, index };
379
+ return index;
380
+ }
381
+ /**
382
+ * The row under the offset native is still at, in rows that have just replaced the old ones, as
383
+ * a filter replaces them. Native keeps its offset whatever the items became, so the window is
384
+ * worked out from that, not from the old row's index, which in the new rows sits somewhere else.
385
+ * When the new rows end before that offset, the list is scrolled back to their end: native
386
+ * would otherwise stay scrolled past it, showing nothing until the next drag.
387
+ */
388
+ atOffset() {
389
+ const end = Math.max(0, this.extent() - this.viewport());
390
+ if (this.viewport() > 0 && this.lastOffset > end + HOLD_TOLERANCE) {
391
+ this.pendingScroll = { offset: end, animated: false };
392
+ this.lastOffset = end;
393
+ }
394
+ const index = Math.max(0, Math.min(this.indexAt(this.lastOffset), this.items().length - 1));
395
+ this.anchorAt(index, this.lastOffset);
396
+ return index;
397
+ }
398
+ /**
399
+ * The index to hold the viewport at, -1 to let it move, or the start of the list when an
400
+ * insert lands within the autoscroll threshold of it.
401
+ */
402
+ heldIndex(anchor, items) {
403
+ if (items === anchor.items)
404
+ return anchor.index;
405
+ const hold = this.maintainVisibleContentPosition();
406
+ if (!hold)
407
+ return -1;
408
+ const threshold = hold.autoscrollToTopThreshold;
409
+ if (threshold !== undefined && this.lastOffset <= threshold)
410
+ return 'start';
411
+ const index = this.successorOf(anchor, items);
412
+ return index < (hold.minIndexForVisible ?? 0) ? -1 : index;
413
+ }
414
+ /** Scroll to what arrived at the start, rather than holding the viewport past it. */
415
+ showStart() {
416
+ this.anchorAt(0, 0);
417
+ if (this.lastOffset !== 0)
418
+ this.pendingScroll = 'start';
419
+ this.lastOffset = 0;
420
+ return 0;
421
+ }
422
+ /**
423
+ * The anchored row's new index, or the first row after it that survived when it did not. Only
424
+ * the rows that were rendered are searched for a successor: past those the viewport has moved
425
+ * on, and holding it by something off screen would be holding it by nothing the user saw.
426
+ */
427
+ successorOf(anchor, items) {
428
+ const keys = this.keys() ?? items;
429
+ const index = keys.indexOf(anchor.key);
430
+ if (index !== -1)
431
+ return index;
432
+ const extract = this.keyExtractor();
433
+ const was = anchor.items;
434
+ const limit = Math.min(was.length, anchor.index + this.overscan() * 4 + 1);
435
+ for (let i = anchor.index + 1; i < limit; i++) {
436
+ const successor = keys.indexOf(extract ? extract(was[i], i) : was[i]);
437
+ if (successor !== -1)
438
+ return successor;
439
+ }
440
+ return -1;
441
+ }
442
+ /** Make a queued correction, after the commit that moved the rows has gone to native. */
443
+ flushScroll() {
444
+ const pending = this.pendingScroll;
445
+ if (!pending)
446
+ return;
447
+ this.pendingScroll = null;
448
+ if (pending === 'start')
449
+ this.scrollToOffset({ offset: 0, animated: true });
450
+ else
451
+ this.scrollToOffset({ offset: this.headerHeight() + pending.offset, animated: false });
452
+ }
453
+ constructor() {
454
+ super();
455
+ afterEveryRender(() => {
456
+ this.flushScroll();
457
+ if (this.nativeSticky)
458
+ this.driveStickyRows();
459
+ });
460
+ const stop = dismissKeyboardOnTap(this.engine, this.node, () => this.keyboardShouldPersistTaps());
461
+ inject(DestroyRef).onDestroy(() => {
462
+ stop();
463
+ clearTimeout(this.settleTimer);
464
+ for (const held of this.drives.values())
465
+ held.drive.stop();
466
+ });
467
+ }
468
+ // --- the window ---------------------------------------------------------------------------
469
+ /** The rows to render, and the edges of the run they sit in. */
470
+ span = computed(() => {
471
+ const items = this.items();
472
+ const viewport = this.viewport();
473
+ const overscan = this.overscan();
474
+ if (!items.length)
475
+ return null;
476
+ const visible = Math.min(this.visible(), items.length - 1);
477
+ const first = Math.max(0, visible - overscan);
478
+ // Before the first layout we have no viewport, so render a screen's worth optimistically
479
+ // rather than nothing: an empty first frame is worse than a few extra rows.
480
+ const budget = viewport > 0 ? viewport : this.heightOf(first) * overscan * 2;
481
+ /*
482
+ * Measured from the top of the first *visible* row, not the first rendered one.
483
+ *
484
+ * Anchoring it at `first` - which is already backed off by the overscan - spent the budget
485
+ * on rows above the viewport, so the window ran out that far before the bottom of the
486
+ * screen. The trailing `+ overscan` hid it whenever every row was the same height, because
487
+ * then the rows added below happened to be worth exactly the rows wasted above. With mixed
488
+ * heights they are not, and the list clips short of its own bottom.
489
+ */
490
+ const top = this.topOf(visible);
491
+ let last = visible;
492
+ while (last < items.length && this.topOf(last) < top + budget)
493
+ last++;
494
+ return { first, last: Math.min(items.length, last + overscan) };
495
+ }, /* @ts-ignore */
496
+ ...(ngDevMode ? [{ debugName: "span" }] : /* istanbul ignore next */ []));
497
+ window = computed(() => {
498
+ const span = this.span();
499
+ if (!span)
500
+ return [];
501
+ const rows = this.rowsFor(this.items(), span.first, span.last);
502
+ this.trimParked();
503
+ for (const pool of this.parked.values())
504
+ rows.push(...pool);
505
+ // The cache exists for rows that stay in the window between scrolls; anything that has left
506
+ // it is rebuilt if it comes back, so the map stays the size of the window, not of the list.
507
+ for (const index of this.slots.keys()) {
508
+ if (index < span.first || index >= span.last)
509
+ this.slots.delete(index);
510
+ }
511
+ return rows;
512
+ }, /* @ts-ignore */
513
+ ...(ngDevMode ? [{ debugName: "window" }] : /* istanbul ignore next */ []));
514
+ /**
515
+ * What stands in for the rows above the window when rows are in flow: their total extent, less
516
+ * a pinned row's, which is rendered in flow ahead of the window.
517
+ */
518
+ spacer = computed(() => {
519
+ const span = this.span();
520
+ let lead = span ? this.topOf(span.first) : 0;
521
+ const pinned = this.pinnedIndex();
522
+ if (span && pinned !== null && pinned < span.first) {
523
+ lead = this.nativeSticky ? this.topOf(pinned) : lead - this.heightOf(pinned);
524
+ }
525
+ return this.horizontal() ? { width: Math.max(0, lead) } : { height: Math.max(0, lead) };
526
+ }, /* @ts-ignore */
527
+ ...(ngDevMode ? [{ debugName: "spacer" }] : /* istanbul ignore next */ []));
528
+ /** The rows from `first` up to `last`, and the pinned row above them if there is one. */
529
+ rowsFor(items, first, last) {
530
+ const rows = [];
531
+ const pinned = this.pinnedIndex();
532
+ const pinnedAbove = pinned !== null && pinned < first;
533
+ this.releaseRecycled(first, last, pinnedAbove ? pinned : null);
534
+ // The pinned row is on screen even when its own place in the list is not, which is the whole
535
+ // point of it, so it is rendered whether or not the window reaches that far.
536
+ if (pinnedAbove) {
537
+ const key = this.keyAt(pinned);
538
+ rows.push(this.hold({
539
+ index: pinned,
540
+ slot: this.recycledSlot(key, items[pinned], pinned),
541
+ key,
542
+ item: items[pinned],
543
+ style: this.nativeSticky
544
+ ? this.stickyStyle(pinned)
545
+ : this.measuring()
546
+ ? this.pinnedFlowStyle(this.topOf(first) - this.heightOf(pinned))
547
+ : this.slotStyle(pinned, this.offset()),
548
+ }));
549
+ }
550
+ for (let index = first; index < last; index++) {
551
+ const key = this.keyAt(index);
552
+ rows.push(this.hold({
553
+ index,
554
+ slot: this.recycledSlot(key, items[index], index),
555
+ key,
556
+ item: items[index],
557
+ style: this.styleFor(index, index === pinned),
558
+ }));
559
+ }
560
+ if (pinnedAbove && this.nativeSticky && this.measuring())
561
+ this.leadWindow(rows, pinned, first);
562
+ return rows;
563
+ }
564
+ /**
565
+ * A pinned row native moves stays at its own place, so the rows between it and the window are
566
+ * made up before the window rather than after the row: new props on a view native is moving
567
+ * put its transform back for a frame.
568
+ */
569
+ leadWindow(rows, pinned, first) {
570
+ const lead = rows[1];
571
+ const gap = this.topOf(first) - this.topOf(pinned) - this.heightOf(pinned);
572
+ if (!lead || gap <= 0)
573
+ return;
574
+ const margin = this.horizontal() ? 'marginLeft' : 'marginTop';
575
+ rows[1] = this.hold({ ...lead, style: { ...lead.style, [margin]: gap } });
576
+ }
577
+ styleFor(index, pinned) {
578
+ if (this.nativeSticky && this.nextSticky().has(index))
579
+ return this.stickyStyle(index);
580
+ if (this.measuring()) {
581
+ return pinned ? this.pinnedFlowStyle(this.topOf(index)) : this.flowStyle();
582
+ }
583
+ return pinned ? this.slotStyle(index, this.offset()) : this.slotStyle(index);
584
+ }
585
+ /** A pinned row in flow, moved from where flow put it to the leading edge by a transform. */
586
+ pinnedFlowStyle(at) {
587
+ const shift = Math.max(0, this.offset() - at);
588
+ const move = `${this.horizontal() ? 'translateX' : 'translateY'}(${shift}px)`;
589
+ const flip = this.flipText();
590
+ return { zIndex: 1, transform: flip ? `${move} ${flip}` : move };
591
+ }
592
+ /**
593
+ * A sticky row native moves: at its own place, with the translate native had reached when
594
+ * scrolling paused, so the props agree with what is on screen.
595
+ */
596
+ stickyStyle(index) {
597
+ const horizontal = this.horizontal();
598
+ const style = this.measuring() ? {} : { ...this.slotStyle(index) };
599
+ style['zIndex'] = 1;
600
+ const top = this.topOf(index);
601
+ const shift = Math.min(Math.max(0, this.settled() - top), this.stopOf(index) - top);
602
+ const move = shift > 0 ? `${horizontal ? 'translateX' : 'translateY'}(${shift}px)` : '';
603
+ const transform = [move, this.flipText() ?? ''].filter(Boolean).join(' ');
604
+ if (transform)
605
+ style['transform'] = transform;
606
+ return style;
607
+ }
608
+ /** Each sticky row, and the one after it, which pushes it off. */
609
+ nextSticky = computed(() => {
610
+ const sticky = this.stickyIndices();
611
+ return new Map(sticky.map((index, at) => [index, sticky[at + 1]]));
612
+ }, /* @ts-ignore */
613
+ ...(ngDevMode ? [{ debugName: "nextSticky" }] : /* istanbul ignore next */ []));
614
+ /** Where a sticky row stops, pushed off by the next: that row's top, less this row's size. */
615
+ stopOf(index) {
616
+ const next = this.nextSticky().get(index);
617
+ return next === undefined ? Infinity : this.topOf(next) - this.heightOf(index);
618
+ }
619
+ /**
620
+ * How native moves a rendered row, or null when it does not: along the scroll view's own
621
+ * offset, which counts the header, from the row's top until the next sticky row pushes it off.
622
+ */
623
+ stickyDrive(row) {
624
+ if (!this.nativeSticky || row.parked || !this.nextSticky().has(row.index))
625
+ return null;
626
+ const header = this.headerHeight();
627
+ const start = header + this.topOf(row.index);
628
+ return {
629
+ range: pinnedRange(start, header + this.stopOf(row.index)),
630
+ statics: this.flip() ?? [],
631
+ };
632
+ }
633
+ /**
634
+ * Hand each rendered sticky row's view to native to move, and let go of the rest. The rows'
635
+ * views are the canvas's element children in window order, after the spacer, which is how
636
+ * `@for` over `window()` leaves them.
637
+ */
638
+ driveStickyRows() {
639
+ if (!this.pinning() && this.drives.size === 0)
640
+ return;
641
+ const canvas = this.canvasView().nativeElement;
642
+ const views = canvas.children
643
+ .filter((child) => child.kind === 'element')
644
+ .slice(this.measuring() ? 1 : 0);
645
+ const axis = this.horizontal() ? 'x' : 'y';
646
+ const live = new Set();
647
+ this.window().forEach((row, at) => {
648
+ const view = views[at];
649
+ const next = this.stickyDrive(row);
650
+ if (!view || !next)
651
+ return;
652
+ live.add(view);
653
+ const statics = JSON.stringify(next.statics);
654
+ const held = this.drives.get(view);
655
+ if (held?.statics === statics)
656
+ return held.drive.update(next.range);
657
+ held?.drive.stop();
658
+ const drive = this.engine.driveByScroll(view, this.node, axis, next.range, next.statics);
659
+ if (drive)
660
+ this.drives.set(view, { drive, statics });
661
+ });
662
+ for (const [view, held] of this.drives) {
663
+ if (live.has(view))
664
+ continue;
665
+ held.drive.stop();
666
+ this.drives.delete(view);
667
+ }
668
+ }
669
+ /** Which recycling slot each rendered item holds, by key: its type, and the row it last drew. */
670
+ recycled = new Map();
671
+ /** Slots that have left the window and are kept hidden, by the type of row that held them. */
672
+ parked = new Map();
673
+ nextSlot = 0;
674
+ /** Note the row a slot is drawing, for parking it once it leaves. */
675
+ hold(row) {
676
+ const held = this.recycled.get(row.key);
677
+ if (held)
678
+ held.row = row;
679
+ return row;
680
+ }
681
+ /**
682
+ * Park the slots of the items that have left the window, for the items arriving.
683
+ *
684
+ * Parked rather than freed: a slot missing from the window is destroyed by `@for`, so a slot is
685
+ * only reused if a row of its type arrives in the same pass as it left. Kept rendered and
686
+ * hidden, it waits for the next row of its type however many passes later, which is what a
687
+ * reuse queue of cells does in UIKit.
688
+ */
689
+ releaseRecycled(first, last, pinned) {
690
+ const keep = new Set();
691
+ for (let index = first; index < last; index++)
692
+ keep.add(this.keyAt(index));
693
+ if (pinned !== null)
694
+ keep.add(this.keyAt(pinned));
695
+ for (const [key, held] of this.recycled) {
696
+ if (keep.has(key))
697
+ continue;
698
+ this.recycled.delete(key);
699
+ let pool = this.parked.get(held.type);
700
+ if (!pool)
701
+ this.parked.set(held.type, (pool = []));
702
+ pool.push({ ...(held.row ?? this.placeholder(key, held.slot)), parked: true, style: PARKED });
703
+ }
704
+ }
705
+ /**
706
+ * Keep a few hidden slots of each type once this pass has taken what it needs, dropping the
707
+ * oldest: `@for` destroys the views of a slot that leaves the window for good.
708
+ */
709
+ trimParked() {
710
+ for (const pool of this.parked.values()) {
711
+ if (pool.length > PARK_LIMIT)
712
+ pool.splice(0, pool.length - PARK_LIMIT);
713
+ }
714
+ }
715
+ /** A parked row for a slot that never drew, which only a slot taken and left in one pass is. */
716
+ placeholder(key, slot) {
717
+ return { index: -1, slot, key, item: this.items()[0], style: PARKED };
718
+ }
719
+ recycledSlot(key, item, index) {
720
+ const held = this.recycled.get(key);
721
+ if (held)
722
+ return held.slot;
723
+ const type = this.itemType()?.(item, index);
724
+ const slot = this.parked.get(type)?.pop()?.slot ?? this.nextSlot++;
725
+ this.recycled.set(key, { slot, type, row: null });
726
+ return slot;
727
+ }
728
+ /**
729
+ * A row laid itself out; what `virtualListRow` calls. Only a row that sizes itself is measured,
730
+ * and a height that has not changed changes nothing.
731
+ */
732
+ measureRow(row, layout) {
733
+ if (!this.measuring() || !layout || row.parked)
734
+ return;
735
+ const size = this.horizontal() ? layout.width : layout.height;
736
+ if (size === undefined || this.measured.get(row.key) === size)
737
+ return;
738
+ this.measured.set(row.key, size);
739
+ this.forgetDeparted();
740
+ const index = this.indexOfKey()?.get(row.key);
741
+ if (index === undefined)
742
+ return;
743
+ this.table()?.set(index, size);
744
+ this.measuredVersion.update((version) => version + 1);
745
+ }
746
+ /** Drop the measurements of items long gone, so churn cannot grow the map without bound. */
747
+ forgetDeparted() {
748
+ const items = this.items();
749
+ if (this.measured.size <= items.length * 2 + 64)
750
+ return;
751
+ const current = new Set(this.keys() ?? items);
752
+ for (const key of this.measured.keys())
753
+ if (!current.has(key))
754
+ this.measured.delete(key);
755
+ }
756
+ /** A separator for every rendered row but the list's last, in its leading row's slot. */
757
+ gaps = computed(() => {
758
+ if (!this.separator())
759
+ return [];
760
+ const items = this.items();
761
+ const gaps = [];
762
+ for (const row of this.window()) {
763
+ if (row.parked || row.index >= items.length - 1)
764
+ continue;
765
+ const style = this.measuring() ? this.measuredGapStyle(row) : this.gapStyle(row);
766
+ if (!style)
767
+ continue;
768
+ const trailingItem = items[row.index + 1];
769
+ gaps.push({
770
+ index: row.index,
771
+ style,
772
+ context: {
773
+ $implicit: row.item,
774
+ leadingItem: row.item,
775
+ trailingItem,
776
+ leadingIndex: row.index,
777
+ },
778
+ });
779
+ }
780
+ return gaps;
781
+ }, /* @ts-ignore */
782
+ ...(ngDevMode ? [{ debugName: "gaps" }] : /* istanbul ignore next */ []));
783
+ /** The row's own slot, laid out to put the separator at its trailing edge. */
784
+ gapStyle(row) {
785
+ let style = this.gapStyles.get(row.style);
786
+ if (!style) {
787
+ style = {
788
+ ...row.style,
789
+ flexDirection: this.horizontal() ? 'row' : 'column',
790
+ justifyContent: 'flex-end',
791
+ pointerEvents: 'box-none',
792
+ };
793
+ this.gapStyles.set(row.style, style);
794
+ }
795
+ return style;
796
+ }
797
+ /**
798
+ * A measured row's slot, over the row itself: nothing until its height is known, since drawing
799
+ * it at the estimate would put a line through the middle of a row that turns out taller.
800
+ */
801
+ measuredGapStyle(row) {
802
+ if (!this.measured.has(row.key))
803
+ return null;
804
+ const start = this.topOf(row.index);
805
+ const size = this.heightOf(row.index);
806
+ const horizontal = this.horizontal();
807
+ const flip = this.flipText();
808
+ const cached = this.measuredGaps.get(row.key);
809
+ if (cached &&
810
+ cached[horizontal ? 'left' : 'top'] === start &&
811
+ cached[horizontal ? 'width' : 'height'] === size &&
812
+ cached['transform'] === flip) {
813
+ return cached;
814
+ }
815
+ const style = horizontal
816
+ ? { position: 'absolute', top: 0, bottom: 0, left: start, width: size, flexDirection: 'row' }
817
+ : { position: 'absolute', left: 0, right: 0, top: start, height: size };
818
+ style['justifyContent'] = 'flex-end';
819
+ style['pointerEvents'] = 'box-none';
820
+ if (flip)
821
+ style['transform'] = flip;
822
+ this.measuredGaps.set(row.key, style);
823
+ if (this.measuredGaps.size > 256)
824
+ this.measuredGaps.clear();
825
+ return style;
826
+ }
827
+ /**
828
+ * The header's translation while it is pinned: the scroll position, held back at zero by an
829
+ * overscroll and at the first sticky row, which pushes it off.
830
+ */
831
+ headerStyle = computed(() => {
832
+ const flip = this.flipText();
833
+ if (!this.stickyHeader())
834
+ return flip ? { transform: flip } : undefined;
835
+ const along = this.offset() + this.headerHeight();
836
+ const first = this.stickyIndices()[0];
837
+ const limit = first === undefined ? Infinity : this.topOf(first);
838
+ const shift = Math.min(Math.max(0, along), limit);
839
+ const move = shift > 0 ? `${this.horizontal() ? 'translateX' : 'translateY'}(${shift}px)` : '';
840
+ const transform = [move, flip ?? ''].filter(Boolean).join(' ');
841
+ return transform ? { zIndex: 1, transform } : { zIndex: 1 };
842
+ }, /* @ts-ignore */
843
+ ...(ngDevMode ? [{ debugName: "headerStyle" }] : /* istanbul ignore next */ []));
844
+ /**
845
+ * The footer, flipped back like every row when the list is inverted, as FlatList inverts its
846
+ * header and footer cells too; otherwise both read upside down.
847
+ */
848
+ footerStyle = computed(() => {
849
+ const flip = this.flipText();
850
+ return flip ? { transform: flip } : undefined;
851
+ }, /* @ts-ignore */
852
+ ...(ngDevMode ? [{ debugName: "footerStyle" }] : /* istanbul ignore next */ []));
853
+ /**
854
+ * The sticky row currently holding the leading edge: the last one at or before the offset.
855
+ * Null when the list has scrolled back above the first of them.
856
+ */
857
+ pinnedIndex = computed(() => {
858
+ const sticky = this.stickyIndices();
859
+ if (sticky.length === 0)
860
+ return null;
861
+ const offset = this.offset();
862
+ let pinned = null;
863
+ for (const index of sticky) {
864
+ if (this.topOf(index) <= offset)
865
+ pinned = index;
866
+ else
867
+ break;
868
+ }
869
+ return pinned;
870
+ }, /* @ts-ignore */
871
+ ...(ngDevMode ? [{ debugName: "pinnedIndex" }] : /* istanbul ignore next */ []));
872
+ /** Which rows are on screen far enough to count, by the caller's threshold. */
873
+ viewableAt(offset) {
874
+ const viewport = this.viewport();
875
+ if (!viewport)
876
+ return [];
877
+ const threshold = this.itemVisiblePercentThreshold() / 100;
878
+ const rows = [];
879
+ for (const row of this.window()) {
880
+ if (row.parked)
881
+ continue;
882
+ const start = this.topOf(row.index);
883
+ const size = this.heightOf(row.index);
884
+ const shown = Math.min(start + size, offset + viewport) - Math.max(start, offset);
885
+ if (shown > 0 && shown >= size * threshold)
886
+ rows.push(row.index);
887
+ }
888
+ return rows;
889
+ }
890
+ /** The last set announced, so the output fires on a change rather than on every frame. */
891
+ announced = [];
892
+ /**
893
+ * Announce a change in what is on screen.
894
+ *
895
+ * Deliberately not a `computed` over a signal offset. A fling produces a scroll event per
896
+ * frame, and writing a signal on each one costs a change-detection pass whether or not anything
897
+ * moved into view; comparing here costs an array walk over the window and emits only when the
898
+ * set really changed.
899
+ */
900
+ announceViewable(offset) {
901
+ const viewable = this.viewableAt(offset);
902
+ const entered = viewable.filter((index) => !this.announced.includes(index));
903
+ const left = this.announced.filter((index) => !viewable.includes(index));
904
+ if (entered.length === 0 && left.length === 0)
905
+ return;
906
+ this.announced = viewable;
907
+ const items = this.items();
908
+ this.viewableItemsChanged.emit({
909
+ viewable: viewable.map((index) => {
910
+ const key = this.keyAt(index);
911
+ return {
912
+ index,
913
+ // What is viewable is in the window, so it holds a slot already.
914
+ slot: this.recycled.get(key)?.slot ?? -1,
915
+ key,
916
+ item: items[index],
917
+ style: this.styleFor(index, false),
918
+ };
919
+ }),
920
+ entered,
921
+ left,
922
+ });
923
+ }
924
+ /**
925
+ * Scroll so that a row is at the start of the viewport, less `viewOffset`. The window moves
926
+ * there at once, and the row is held there while the rows around it are measured.
927
+ */
928
+ scrollToIndex(options) {
929
+ const items = this.items();
930
+ if (!items.length)
931
+ return;
932
+ const index = Math.max(0, Math.min(options.index, items.length - 1));
933
+ const within = -(options.viewOffset ?? 0);
934
+ const offset = this.topOf(index) + within;
935
+ this.lastOffset = offset;
936
+ this.anchor = { key: this.keyAt(index), within, items, index };
937
+ this.visible.set(index);
938
+ this.scrollToOffset({ offset: this.headerHeight() + offset, animated: options.animated });
939
+ }
940
+ /** Scroll to an offset along the list's own axis: x when it is horizontal, y otherwise. */
941
+ scrollToOffset(options) {
942
+ const { offset, animated = true } = options;
943
+ const [x, y] = this.horizontal() ? [offset, 0] : [0, offset];
944
+ this.engine.dispatchCommand(this.node, 'scrollTo', [x, y, animated]);
945
+ }
946
+ scrollToEnd(options = {}) {
947
+ this.engine.dispatchCommand(this.node, 'scrollToEnd', [options.animated ?? true]);
948
+ }
949
+ slotStyle(index, pinnedAt) {
950
+ const start = pinnedAt ?? this.topOf(index);
951
+ const size = this.heightOf(index);
952
+ const horizontal = this.horizontal();
953
+ const key = horizontal ? 'left' : 'top';
954
+ const extent = horizontal ? 'width' : 'height';
955
+ const flip = this.flipText();
956
+ let style = this.slots.get(index);
957
+ // The flip is part of the comparison because turning the list over changes every row, and a
958
+ // cached slot would otherwise keep the orientation it was built with.
959
+ const stale = !style ||
960
+ style[extent] !== size ||
961
+ style[key] !== start ||
962
+ style['transform'] !== flip ||
963
+ pinnedAt !== undefined;
964
+ if (stale) {
965
+ style = horizontal
966
+ ? { position: 'absolute', top: 0, bottom: 0, left: start, width: size }
967
+ : { position: 'absolute', left: 0, right: 0, top: start, height: size };
968
+ // A pinned row draws over the ones sliding under it; the transform is the row's half of
969
+ // the inversion.
970
+ if (pinnedAt !== undefined)
971
+ style['zIndex'] = 1;
972
+ if (flip)
973
+ style['transform'] = flip;
974
+ this.slots.set(index, style);
975
+ return style;
976
+ }
977
+ return style;
978
+ }
979
+ onScroll(event) {
980
+ const contentOffset = event.nativeEvent?.contentOffset;
981
+ const along = (this.horizontal() ? contentOffset?.x : contentOffset?.y) ?? 0;
982
+ const content = along - this.headerHeight();
983
+ // The first *visible* row, not the first rendered one. The overscan is applied where the
984
+ // window is built, so that both edges can be measured from the same place.
985
+ // Not clamped to the items: an input set in the same task as this event has not reached the
986
+ // list yet, and the window clamps when it renders.
987
+ const visible = this.indexAt(content);
988
+ if (visible !== this.visible())
989
+ this.visible.set(visible);
990
+ this.lastOffset = content;
991
+ this.anchorAt(visible, content);
992
+ // A pinned row follows the scroll continuously, so with sticky rows the offset has to be a
993
+ // signal - and only then. Without them this stays a field, and a fling that moves no row in
994
+ // or out of the window costs no change detection at all.
995
+ if (this.pinning())
996
+ this.offset.set(content);
997
+ if (this.nativeSticky && this.pinning())
998
+ this.settleAfterPause(content);
999
+ this.checkEnd(along);
1000
+ this.announceViewable(content);
1001
+ }
1002
+ /** RN's sticky header commits its translate after 64 ms without a scroll event, as this does. */
1003
+ settleAfterPause(content) {
1004
+ clearTimeout(this.settleTimer);
1005
+ this.settleTimer = setTimeout(() => this.settled.set(content), 64);
1006
+ }
1007
+ onLayout(event) {
1008
+ const layout = event.nativeEvent?.layout;
1009
+ this.viewport.set((this.horizontal() ? layout?.width : layout?.height) ?? 0);
1010
+ this.announceViewable(this.lastOffset);
1011
+ }
1012
+ /** The header's extent along the axis, which every row sits after. */
1013
+ onHeaderLayout(event) {
1014
+ const layout = event.nativeEvent?.layout;
1015
+ const height = (this.horizontal() ? layout?.width : layout?.height) ?? 0;
1016
+ if (height === this.headerHeight())
1017
+ return;
1018
+ this.headerHeight.set(height);
1019
+ this.holdPastHeader();
1020
+ }
1021
+ /**
1022
+ * The header grew or shrank - a typing indicator in a chat's, a banner in a feed's - and every
1023
+ * row moved with it. Held like an insert when the caller asked for that and the viewport is
1024
+ * past the header, among the rows; at the start the user is looking at the header, and it is
1025
+ * left to push the rows along. Made at once rather than after a render, because a layout arrives
1026
+ * outside change detection and nothing else may be about to render.
1027
+ */
1028
+ holdPastHeader() {
1029
+ const hold = this.maintainVisibleContentPosition();
1030
+ if (!hold || this.lastOffset <= (hold.autoscrollToTopThreshold ?? 0))
1031
+ return;
1032
+ this.scrollToOffset({ offset: this.headerHeight() + this.lastOffset, animated: false });
1033
+ }
1034
+ onContentSize(event) {
1035
+ const size = event.nativeEvent;
1036
+ this.contentSize = (this.horizontal() ? size?.width : size?.height) ?? 0;
1037
+ }
1038
+ checkEnd(y) {
1039
+ const count = this.items().length;
1040
+ const viewport = this.viewport();
1041
+ if (!count || !viewport)
1042
+ return;
1043
+ const total = this.contentSize || this.headerHeight() + this.extent();
1044
+ const distanceFromEnd = total - (y + viewport);
1045
+ // Away from the end again re-arms it, as React Native does, so a load that failed is retried
1046
+ // the next time the user comes back down.
1047
+ if (distanceFromEnd > this.endReachedThreshold() * viewport) {
1048
+ this.endReachedFor = -1;
1049
+ return;
1050
+ }
1051
+ if (this.endReachedFor === count)
1052
+ return;
1053
+ this.endReachedFor = count;
1054
+ this.endReached.emit({ distanceFromEnd });
1055
+ }
1056
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualList, deps: [], target: i0.ɵɵFactoryTarget.Component });
1057
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: VirtualList, isStandalone: true, selector: "virtual-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, itemHeight: { classPropertyName: "itemHeight", publicName: "itemHeight", isSignal: true, isRequired: false, transformFunction: null }, estimatedItemHeight: { classPropertyName: "estimatedItemHeight", publicName: "estimatedItemHeight", isSignal: true, isRequired: false, transformFunction: null }, keyExtractor: { classPropertyName: "keyExtractor", publicName: "keyExtractor", isSignal: true, isRequired: false, transformFunction: null }, itemType: { classPropertyName: "itemType", publicName: "itemType", isSignal: true, isRequired: false, transformFunction: null }, maintainVisibleContentPosition: { classPropertyName: "maintainVisibleContentPosition", publicName: "maintainVisibleContentPosition", isSignal: true, isRequired: false, transformFunction: null }, overscan: { classPropertyName: "overscan", publicName: "overscan", isSignal: true, isRequired: false, transformFunction: null }, endReachedThreshold: { classPropertyName: "endReachedThreshold", publicName: "endReachedThreshold", isSignal: true, isRequired: false, transformFunction: null }, horizontal: { classPropertyName: "horizontal", publicName: "horizontal", isSignal: true, isRequired: false, transformFunction: null }, inverted: { classPropertyName: "inverted", publicName: "inverted", isSignal: true, isRequired: false, transformFunction: null }, stickyIndices: { classPropertyName: "stickyIndices", publicName: "stickyIndices", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, keyboardShouldPersistTaps: { classPropertyName: "keyboardShouldPersistTaps", publicName: "keyboardShouldPersistTaps", isSignal: true, isRequired: false, transformFunction: null }, itemVisiblePercentThreshold: { classPropertyName: "itemVisiblePercentThreshold", publicName: "itemVisiblePercentThreshold", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { endReached: "endReached", viewableItemsChanged: "viewableItemsChanged" }, host: { listeners: { "scroll": "onScroll($event)", "layout": "onLayout($event)", "contentSizeChange": "onContentSize($event)" }, properties: { "flexDirection": "horizontal() ? 'row' : 'column'", "horizontal": "horizontal()", "nestedScrollEnabled": "nestedScroll()", "transform": "flip()", "scrollEventThrottle": "scrollEventThrottle()" } }, queries: [{ propertyName: "separator", first: true, predicate: VirtualListSeparator, descendants: true, isSignal: true }, { propertyName: "refreshControl", first: true, predicate: RefreshControl, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "canvasView", first: true, predicate: ["rows"], descendants: true, read: ElementRef, isSignal: true }], exportAs: ["virtualList"], usesInheritance: true, ngImport: i0, template: `
1058
+ <ng-content select="refresh-control" />
1059
+ <view [style]="headerStyle()" (layout)="onHeaderLayout($event)"
1060
+ ><ng-content select="[listHeader]"
1061
+ /></view>
1062
+ <view #rows [style]="canvas()" collapsable="false">
1063
+ @if (measuring()) {
1064
+ <view [style]="spacer()"></view>
1065
+ }
1066
+ <ng-content />
1067
+ @if (separator(); as separator) {
1068
+ @for (gap of gaps(); track gap.index) {
1069
+ <view [style]="gap.style"
1070
+ ><ng-container [templateSlot]="separator.template" [templateSlotContext]="gap.context"
1071
+ /></view>
1072
+ }
1073
+ }
1074
+ </view>
1075
+ <view [style]="footerStyle()"><ng-content select="[listFooter]" /></view>
1076
+ `, isInline: true, dependencies: [{ kind: "directive", type: TemplateSlot, selector: "[templateSlot]", inputs: ["templateSlot", "templateSlotContext"] }, { kind: "directive", type: View, selector: "view" }] });
1077
+ }
1078
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualList, decorators: [{
1079
+ type: Component,
1080
+ args: [{
1081
+ selector: 'virtual-list',
1082
+ exportAs: 'virtualList',
1083
+ imports: [TemplateSlot, View],
1084
+ // See ScrollView: the canvas carries only a height, so Fabric would flatten it away and drags
1085
+ // over the gaps between rows would not scroll. Separators come after the rows so they draw over
1086
+ // the slot they share. The spacer is there only when rows are in flow.
1087
+ // A refresh control comes first, as the scroll view's own child; see ScrollView.
1088
+ template: `
1089
+ <ng-content select="refresh-control" />
1090
+ <view [style]="headerStyle()" (layout)="onHeaderLayout($event)"
1091
+ ><ng-content select="[listHeader]"
1092
+ /></view>
1093
+ <view #rows [style]="canvas()" collapsable="false">
1094
+ @if (measuring()) {
1095
+ <view [style]="spacer()"></view>
1096
+ }
1097
+ <ng-content />
1098
+ @if (separator(); as separator) {
1099
+ @for (gap of gaps(); track gap.index) {
1100
+ <view [style]="gap.style"
1101
+ ><ng-container [templateSlot]="separator.template" [templateSlotContext]="gap.context"
1102
+ /></view>
1103
+ }
1104
+ }
1105
+ </view>
1106
+ <view [style]="footerStyle()"><ng-content select="[listFooter]" /></view>
1107
+ `,
1108
+ host: {
1109
+ // ScrollView.js's `baseHorizontal`: a column would stack the header, canvas and footer, and
1110
+ // the canvas, holding only absolutely positioned rows, would have no height to give them. A
1111
+ // top-level prop, as on <scroll-view>, so the caller's own [style] still applies over it.
1112
+ '[flexDirection]': "horizontal() ? 'row' : 'column'",
1113
+ '[horizontal]': 'horizontal()',
1114
+ '[nestedScrollEnabled]': 'nestedScroll()',
1115
+ // A prop rather than `[style.transform]`: Angular stringifies a style binding, and a
1116
+ // transform is a list of objects. The engine flattens style into props anyway, so this is
1117
+ // where it would have ended up.
1118
+ '[transform]': 'flip()',
1119
+ '[scrollEventThrottle]': 'scrollEventThrottle()',
1120
+ '(scroll)': 'onScroll($event)',
1121
+ '(layout)': 'onLayout($event)',
1122
+ '(contentSizeChange)': 'onContentSize($event)',
1123
+ },
1124
+ }]
1125
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: false }] }], estimatedItemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "estimatedItemHeight", required: false }] }], keyExtractor: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyExtractor", required: false }] }], itemType: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemType", required: false }] }], maintainVisibleContentPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "maintainVisibleContentPosition", required: false }] }], overscan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscan", required: false }] }], endReachedThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "endReachedThreshold", required: false }] }], horizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "horizontal", required: false }] }], inverted: [{ type: i0.Input, args: [{ isSignal: true, alias: "inverted", required: false }] }], stickyIndices: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyIndices", required: false }] }], stickyHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyHeader", required: false }] }], keyboardShouldPersistTaps: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardShouldPersistTaps", required: false }] }], itemVisiblePercentThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemVisiblePercentThreshold", required: false }] }], endReached: [{ type: i0.Output, args: ["endReached"] }], viewableItemsChanged: [{ type: i0.Output, args: ["viewableItemsChanged"] }], separator: [{ type: i0.ContentChild, args: [i0.forwardRef(() => VirtualListSeparator), { isSignal: true }] }], refreshControl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RefreshControl), { isSignal: true }] }], canvasView: [{ type: i0.ViewChild, args: ['rows', { ...{ read: ElementRef }, isSignal: true }] }] } });
1126
+ /**
1127
+ * Put on each row's view: `<view [virtualListRow]="row">`. Applies the row's style, and reports
1128
+ * the row's layout to the list, which is how a list of rows that size themselves learns how tall
1129
+ * each one is.
1130
+ */
1131
+ export class VirtualListRow {
1132
+ row = input.required({ ...(ngDevMode ? { debugName: "row" } : /* istanbul ignore next */ {}), alias: 'virtualListRow' });
1133
+ list = inject(VirtualList);
1134
+ onLayout(event) {
1135
+ this.list.measureRow(this.row(), event.nativeEvent?.layout);
1136
+ }
1137
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualListRow, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1138
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.2.0", type: VirtualListRow, isStandalone: true, selector: "[virtualListRow]", inputs: { row: { classPropertyName: "row", publicName: "virtualListRow", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "layout": "onLayout($event)" }, properties: { "style": "row().style" } }, ngImport: i0 });
1139
+ }
1140
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: VirtualListRow, decorators: [{
1141
+ type: Directive,
1142
+ args: [{
1143
+ selector: '[virtualListRow]',
1144
+ host: {
1145
+ '[style]': 'row().style',
1146
+ '(layout)': 'onLayout($event)',
1147
+ },
1148
+ }]
1149
+ }], propDecorators: { row: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualListRow", required: true }] }] } });
1150
+ //# sourceMappingURL=virtual-list.js.map