@symbiote-native/vue 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/components/flat-list/index.d.ts +4 -3
- package/build/components/flat-list/index.js +18 -21
- package/build/components/input-accessory-view-props.d.ts +4 -4
- package/build/components/keyboard-avoiding-view.d.ts +11 -3
- package/build/components/keyboard-avoiding-view.js +38 -69
- package/build/components/layout-conformance-props.d.ts +4 -0
- package/build/components/layout-conformance-props.js +1 -0
- package/build/components/modal/index.d.ts +18 -5
- package/build/components/modal/index.js +53 -24
- package/build/components/refresh-control-props.d.ts +7 -7
- package/build/components/scroll-view/scroll-view-props.d.ts +8 -5
- package/build/components/section-list/index.d.ts +1 -1
- package/build/components/section-list/index.js +2 -6
- package/build/components/text-props.d.ts +3 -3
- package/build/components/touchable-props.d.ts +6 -6
- package/build/components/virtualized-list/index.d.ts +5 -75
- package/build/components/virtualized-list/index.js +130 -558
- package/build/components/virtualized-list/list-children.d.ts +18 -0
- package/build/components/virtualized-list/list-children.js +81 -0
- package/build/components/virtualized-list/list-elements.d.ts +7 -0
- package/build/components/virtualized-list/list-elements.js +29 -0
- package/build/components/virtualized-list/list-scroll-props.d.ts +20 -0
- package/build/components/virtualized-list/list-scroll-props.js +9 -0
- package/build/components/virtualized-list/narrow-props.d.ts +79 -0
- package/build/components/virtualized-list/narrow-props.js +89 -0
- package/build/components/virtualized-list/nested-scope.d.ts +3 -0
- package/build/components/virtualized-list/nested-scope.js +12 -0
- package/build/components/virtualized-list/user-handlers.d.ts +3 -0
- package/build/components/virtualized-list/user-handlers.js +24 -0
- package/build/components/virtualized-list/virtualized-list-props.d.ts +82 -0
- package/build/components/virtualized-list/virtualized-list-props.js +41 -0
- package/build/components/virtualized-section-list/index.d.ts +20 -5
- package/build/components/virtualized-section-list/index.js +81 -95
- package/build/components/virtualized-section-list/section-item-cell.d.ts +18 -0
- package/build/components/virtualized-section-list/section-item-cell.js +52 -0
- package/build/composables/use-animated-value.d.ts +7 -0
- package/build/composables/use-animated-value.js +14 -0
- package/build/composables/use-color-scheme.d.ts +1 -1
- package/build/create-transition/index.d.ts +2 -2
- package/build/create-transition/index.js +1 -1
- package/build/index.d.ts +13 -4
- package/build/index.js +16 -31
- package/build/modules/animated/index.d.ts +54 -41
- package/build/modules/animated/index.js +2 -26
- package/build/modules/app-registry/index.d.ts +3 -3
- package/build/modules/app-registry/index.js +2 -2
- package/build/modules/status-bar.js +22 -24
- package/metro-vue-transformer.cjs +177 -229
- package/package.json +6 -6
|
@@ -1,369 +1,109 @@
|
|
|
1
|
-
// VirtualizedList, the Vue lifecycle half
|
|
2
|
-
//
|
|
3
|
-
// `
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// `<ItemT,>(props: IVirtualizedListProps<ItemT>, ctx: ICtx<IVirtualizedListEmits<ItemT>>)` so the
|
|
14
|
-
// five adapter-synthesized events emit with ItemT-typed payloads (`@viewable-items-changed` carries
|
|
15
|
-
// ItemT, not unknown). For that to infer at the call site, the generic INPUTS (data/getItem/
|
|
16
|
-
// renderItem/…) are read from typed `props`, declared in the runtime `props` array; the raw native
|
|
17
|
-
// scroll events (onScroll/onScrollBeginDrag/…) + any unknown attrs ride through `$attrs` onto the
|
|
18
|
-
// inner ScrollView. RefreshControl + viewability stay GATED on listener presence, so each emit
|
|
19
|
-
// bridge is wired ONLY when the consumer actually listens (read off the instance vnode props),
|
|
20
|
-
// keeping behavior identical to the prop-callback era.
|
|
21
|
-
//
|
|
22
|
-
// Lists have no Descriptor render fn (the cell content is the framework's own children).
|
|
23
|
-
// Cells/spacers are built with h() directly off the plan.
|
|
24
|
-
//
|
|
25
|
-
// Reactivity gotcha: the ScrollView's exposed handle is held in a shallowRef so the engine
|
|
26
|
-
// node it closes over is reached by identity; a deep ref would proxy it and the imperative
|
|
27
|
-
// scroll commands would silently no-op. The folded listState is a PLAIN object (not reactive) —
|
|
28
|
-
// mutating it never triggers Vue; the `version` ref is bumped when a transition changes render state.
|
|
29
|
-
import { computed, defineComponent, getCurrentInstance, h, isVNode, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
|
|
30
|
-
import { DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildScrollViewHandle, buildViewabilityPairs, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveItemKey, } from '@symbiote-native/components';
|
|
31
|
-
import { dlog, isSymbioteNode, } from '@symbiote-native/engine';
|
|
32
|
-
import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
|
|
33
|
-
import { componentFromSlot } from '../../utils/slots-to-render-props.js';
|
|
34
|
-
// The typed inputs VirtualizedList reads off `props`; everything else (raw scroll, keyboard*,
|
|
35
|
-
// accessibility, testID, …) falls through $attrs onto the inner ScrollView. Listed for the runtime
|
|
36
|
-
// `props` declaration (keyof can't derive it: the index signature widens keyof to `string`). The
|
|
37
|
-
// five raw-scroll events and the five emit events are deliberately absent.
|
|
38
|
-
const PROP_KEYS = [
|
|
39
|
-
'data',
|
|
40
|
-
'getItem',
|
|
41
|
-
'getItemCount',
|
|
42
|
-
'keyExtractor',
|
|
43
|
-
'getItemLayout',
|
|
44
|
-
'viewabilityConfig',
|
|
45
|
-
'viewabilityConfigCallbackPairs',
|
|
46
|
-
'extraData',
|
|
47
|
-
'horizontal',
|
|
48
|
-
'inverted',
|
|
49
|
-
'onEndReachedThreshold',
|
|
50
|
-
'onStartReachedThreshold',
|
|
51
|
-
'refreshing',
|
|
52
|
-
'progressViewOffset',
|
|
53
|
-
'initialNumToRender',
|
|
54
|
-
'initialScrollIndex',
|
|
55
|
-
'maxToRenderPerBatch',
|
|
56
|
-
'updateCellsBatchingPeriod',
|
|
57
|
-
'windowSize',
|
|
58
|
-
'stickyHeaderIndices',
|
|
59
|
-
'maintainVisibleContentPosition',
|
|
60
|
-
'style',
|
|
61
|
-
'contentContainerStyle',
|
|
62
|
-
'keyboardShouldPersistTaps',
|
|
63
|
-
'keyboardDismissMode',
|
|
64
|
-
];
|
|
65
|
-
const EMIT_KEYS = [
|
|
66
|
-
'viewableItemsChanged',
|
|
67
|
-
'endReached',
|
|
68
|
-
'startReached',
|
|
69
|
-
'refresh',
|
|
70
|
-
'scrollToIndexFailed',
|
|
71
|
-
];
|
|
72
|
-
function isHandler(value) {
|
|
73
|
-
return typeof value === 'function';
|
|
74
|
-
}
|
|
75
|
-
function isComponent(value) {
|
|
76
|
-
return (typeof value === 'function' || (typeof value === 'object' && value !== null));
|
|
77
|
-
}
|
|
78
|
-
function asNumber(value, fallback) {
|
|
79
|
-
return typeof value === 'number' ? value : fallback;
|
|
80
|
-
}
|
|
81
|
-
// The attrs that arrive via $attrs but must NOT be forwarded raw onto the inner ScrollView: VL
|
|
82
|
-
// composes its own onScroll (windowing offset + the user's onScroll) and its own onLayout (viewport
|
|
83
|
-
// measure), so a consumer-passed one is dropped here and re-set explicitly on the scroll props.
|
|
84
|
-
const HANDLED_ATTRS = ['onScroll', 'onLayout'];
|
|
85
|
-
function forwardAttrs(attrs) {
|
|
86
|
-
const result = {};
|
|
87
|
-
for (const key of Object.keys(attrs)) {
|
|
88
|
-
if (!HANDLED_ATTRS.includes(key))
|
|
89
|
-
result[key] = attrs[key];
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
}
|
|
93
|
-
// A list component element (ListHeaderComponent / ListEmptyComponent / ListFooterComponent) is
|
|
94
|
-
// either a Vue component (invoked via h) or a ready VNode (passed through). RN resolves the same
|
|
95
|
-
// `Component | element` union; we narrow with isVNode before isComponent (a VNode is an object).
|
|
96
|
-
function resolveElement(value) {
|
|
97
|
-
if (value === undefined || value === null)
|
|
98
|
-
return undefined;
|
|
99
|
-
if (isVNode(value))
|
|
100
|
-
return value;
|
|
101
|
-
if (isComponent(value))
|
|
102
|
-
return h(value);
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
function renderSeparatorElement(component, leadingItem, trailingItem, overrides) {
|
|
106
|
-
if (component === undefined)
|
|
107
|
-
return undefined;
|
|
108
|
-
const props = {
|
|
109
|
-
highlighted: false,
|
|
110
|
-
leadingItem,
|
|
111
|
-
trailingItem,
|
|
112
|
-
...overrides,
|
|
113
|
-
};
|
|
114
|
-
return h(component, props);
|
|
115
|
-
}
|
|
1
|
+
// VirtualizedList, the Vue lifecycle half over the shared `reduceList` machine
|
|
2
|
+
// A generic setup keeps the five emits typed by `ItemT`, raw native scroll events ride `$attrs`
|
|
3
|
+
// `listState` is a plain object, a `version` ref is bumped when a transition changes render state
|
|
4
|
+
import { computed, defineComponent, getCurrentInstance, h, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
|
|
5
|
+
import { EMPTY_OFFSET, LIST_ACTION_KIND, buildListHandle, buildListReducerInputs, buildScrollViewHandle, buildSeparatorHandles, buildViewabilityPairs, clearTimers, createInitialListState, createListHandlers, createListNesting, isSeparatorGapInRange, listEffectSignature, listHasMore, reduceList, resolveItemKey, runListEffects, scrollTargetOf, viewPropsOf, } from '@symbiote-native/components';
|
|
6
|
+
import { dlog, isDebug, isSymbioteNode, } from '@symbiote-native/engine';
|
|
7
|
+
import { buildListChildren } from './list-children.js';
|
|
8
|
+
import { buildScrollProps } from './list-scroll-props.js';
|
|
9
|
+
import { narrowProps } from './narrow-props.js';
|
|
10
|
+
import { provideVirtualizedListScope, useVirtualizedListScope, } from './nested-scope.js';
|
|
11
|
+
import { userHandlersOf } from './user-handlers.js';
|
|
12
|
+
import { EMIT_KEYS, PROP_KEYS, } from './virtualized-list-props.js';
|
|
116
13
|
export const VirtualizedList = defineComponent((props, { attrs, expose, emit, slots, }) => {
|
|
117
|
-
// Bumped when a transition changes render-relevant state, so the `metrics` computed and the
|
|
118
|
-
// render fn re-run (listState is plain, non-reactive, so mutating it triggers nothing itself).
|
|
119
14
|
const version = ref(EMPTY_OFFSET);
|
|
120
15
|
const separatorVersion = ref(EMPTY_OFFSET);
|
|
121
|
-
//
|
|
122
|
-
// commit path re-applies it even when the numeric value repeats. undefined = none pending.
|
|
16
|
+
// A fresh object each push so the commit path re-applies a repeated value
|
|
123
17
|
const commandedOffset = ref(undefined);
|
|
124
|
-
//
|
|
125
|
-
// node directly, reached by identity through the engine's WeakMap mirror. A deep ref would
|
|
126
|
-
// proxy the object and every imperative scroll (scrollToOffset/Index/…) would miss the node
|
|
127
|
-
// and silently no-op.
|
|
18
|
+
// `<scroll-view>` is a tag, so its `ref` hands back the raw engine node, held by identity
|
|
128
19
|
const scrollNodeRef = shallowRef(null);
|
|
129
20
|
const setScrollNodeRef = (el) => {
|
|
130
21
|
scrollNodeRef.value = isSymbioteNode(el) ? el : null;
|
|
131
22
|
};
|
|
132
|
-
// Built once: `getNode` is lazy, read on every call, so this stays valid across the ref
|
|
133
|
-
// transitioning from null to the committed node.
|
|
134
23
|
const scrollHandle = buildScrollViewHandle(() => scrollNodeRef.value);
|
|
135
|
-
// The
|
|
136
|
-
// presence off the instance's own vnode props (what the parent passed), exactly like FlatList, so
|
|
137
|
-
// an emit bridge is wired ONLY when the consumer actually listens (preserving the RefreshControl /
|
|
138
|
-
// viewability gating of the prop-callback era).
|
|
24
|
+
// The parent's own vnode props tell whether it listens, Vue strips emits from `$attrs`
|
|
139
25
|
const instance = getCurrentInstance();
|
|
140
26
|
const listens = (onName) => {
|
|
141
27
|
const vnodeProps = instance?.vnode.props;
|
|
142
28
|
return vnodeProps != null && typeof vnodeProps[onName] === 'function';
|
|
143
29
|
};
|
|
144
|
-
// The one folded state cell (the Vue twin of React's stateRef); plus the per-gap separator
|
|
145
|
-
// overrides + the adapter-owned debounce/fill timers.
|
|
146
30
|
const listState = createInitialListState();
|
|
147
31
|
const separatorOverrides = new Map();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const narrowed = computed(() => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
data: props.data,
|
|
157
|
-
getItem: props.getItem,
|
|
158
|
-
getItemCount: props.getItemCount,
|
|
159
|
-
// Cell + chrome come from scoped slots. #item IS the render fn (a slot fn returning
|
|
160
|
-
// VNode|VNode[], which the cell wrapper accepts directly). #separator carries scope props
|
|
161
|
-
// (h(component, props)) so it goes through componentFromSlot; #header/#footer/#empty are
|
|
162
|
-
// scopeless, so the bare slot fn is handed to resolveElement (its isComponent branch).
|
|
163
|
-
renderItem: slots.item,
|
|
164
|
-
keyExtractor: props.keyExtractor,
|
|
165
|
-
getItemLayout: props.getItemLayout,
|
|
166
|
-
itemSeparatorComponent: componentFromSlot(slots.separator),
|
|
167
|
-
listHeaderComponent: slots.header,
|
|
168
|
-
listFooterComponent: slots.footer,
|
|
169
|
-
listEmptyComponent: slots.empty,
|
|
170
|
-
horizontal: props.horizontal === true,
|
|
171
|
-
inverted: props.inverted === true,
|
|
172
|
-
onEndReached: listens('onEndReached')
|
|
173
|
-
? (info) => emit('endReached', info)
|
|
174
|
-
: undefined,
|
|
175
|
-
onEndReachedThreshold: typeof props.onEndReachedThreshold === 'number'
|
|
176
|
-
? props.onEndReachedThreshold
|
|
177
|
-
: undefined,
|
|
178
|
-
onStartReached: listens('onStartReached')
|
|
179
|
-
? (info) => emit('startReached', info)
|
|
180
|
-
: undefined,
|
|
181
|
-
onStartReachedThreshold: typeof props.onStartReachedThreshold === 'number'
|
|
182
|
-
? props.onStartReachedThreshold
|
|
183
|
-
: undefined,
|
|
184
|
-
onRefresh: listens('onRefresh')
|
|
185
|
-
? () => emit('refresh')
|
|
186
|
-
: undefined,
|
|
187
|
-
refreshing: props.refreshing === true,
|
|
188
|
-
progressViewOffset: props.progressViewOffset,
|
|
189
|
-
onViewableItemsChanged: listens('onViewableItemsChanged')
|
|
190
|
-
? (info) => emit('viewableItemsChanged', info)
|
|
191
|
-
: undefined,
|
|
192
|
-
viewabilityConfig: props.viewabilityConfig,
|
|
193
|
-
viewabilityConfigCallbackPairs: props.viewabilityConfigCallbackPairs,
|
|
194
|
-
onScrollToIndexFailed: listens('onScrollToIndexFailed')
|
|
195
|
-
? (info) => emit('scrollToIndexFailed', info)
|
|
196
|
-
: undefined,
|
|
197
|
-
initialNumToRender: asNumber(props.initialNumToRender, DEFAULT_INITIAL_NUM_TO_RENDER),
|
|
198
|
-
initialScrollIndex: props.initialScrollIndex,
|
|
199
|
-
maxToRenderPerBatch: asNumber(props.maxToRenderPerBatch, DEFAULT_MAX_TO_RENDER_PER_BATCH),
|
|
200
|
-
updateCellsBatchingPeriod: asNumber(props.updateCellsBatchingPeriod, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD),
|
|
201
|
-
windowSize: asNumber(props.windowSize, DEFAULT_WINDOW_SIZE),
|
|
202
|
-
stickyHeaderIndices: props.stickyHeaderIndices,
|
|
203
|
-
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
|
|
204
|
-
userOnScroll: isHandler(folded.onScroll) ? folded.onScroll : undefined,
|
|
205
|
-
style: props.style,
|
|
206
|
-
contentContainerStyle: props.contentContainerStyle,
|
|
207
|
-
keyboardShouldPersistTaps: props.keyboardShouldPersistTaps,
|
|
208
|
-
keyboardDismissMode: props.keyboardDismissMode,
|
|
209
|
-
forwarded: forwardAttrs(folded),
|
|
210
|
-
};
|
|
211
|
-
});
|
|
212
|
-
// The reducer inputs, folded off `narrowed`. Rebuilt per call (cheap); the viewability pairs are
|
|
213
|
-
// rebuilt here so the reduce and the effect fire against the same set.
|
|
214
|
-
const buildInputs = () => {
|
|
215
|
-
const p = narrowed.value;
|
|
216
|
-
return {
|
|
217
|
-
data: p.data,
|
|
218
|
-
getItem: p.getItem,
|
|
219
|
-
getItemCount: p.getItemCount,
|
|
220
|
-
keyExtractor: p.keyExtractor,
|
|
221
|
-
getItemLayout: p.getItemLayout,
|
|
222
|
-
horizontal: p.horizontal,
|
|
223
|
-
windowSize: p.windowSize,
|
|
224
|
-
initialNumToRender: p.initialNumToRender,
|
|
225
|
-
maxToRenderPerBatch: p.maxToRenderPerBatch,
|
|
226
|
-
updateCellsBatchingPeriod: p.updateCellsBatchingPeriod,
|
|
227
|
-
onEndReachedThreshold: p.onEndReachedThreshold,
|
|
228
|
-
onStartReachedThreshold: p.onStartReachedThreshold,
|
|
229
|
-
onEndReachedActive: p.onEndReached !== undefined,
|
|
230
|
-
onStartReachedActive: p.onStartReached !== undefined,
|
|
231
|
-
viewabilityPairs: buildViewabilityPairs(p.onViewableItemsChanged, p.viewabilityConfig, p.viewabilityConfigCallbackPairs),
|
|
232
|
-
maintainVisibleContentPosition: p.maintainVisibleContentPosition,
|
|
233
|
-
initialScrollIndex: p.initialScrollIndex,
|
|
234
|
-
};
|
|
235
|
-
};
|
|
32
|
+
const viewableTimer = { current: null };
|
|
33
|
+
const batchTimer = { current: null };
|
|
34
|
+
const narrowed = computed(() => narrowProps({ props, attrs, slots, emit, listens }));
|
|
35
|
+
const viewabilityPairs = computed(() => buildViewabilityPairs(narrowed.value.onViewableItemsChanged, narrowed.value.viewabilityConfig, narrowed.value.viewabilityConfigCallbackPairs));
|
|
36
|
+
const buildInputs = () => buildListReducerInputs({
|
|
37
|
+
...narrowed.value,
|
|
38
|
+
findFirstChildWithMore: (first, last) => nesting.findFirstChildWithMore(first, last),
|
|
39
|
+
}, viewabilityPairs.value);
|
|
236
40
|
const keyFor = (index) => {
|
|
237
41
|
const p = narrowed.value;
|
|
238
42
|
return resolveItemKey(p.getItem(p.data, index), index, p.keyExtractor);
|
|
239
43
|
};
|
|
240
44
|
const scrollToPixel = (offset, animated) => {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
: { x: EMPTY_OFFSET, y: clamped };
|
|
246
|
-
if (scrollNodeRef.value !== null) {
|
|
247
|
-
dlog(`Vue VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${p.horizontal})`);
|
|
248
|
-
scrollHandle.scrollTo({
|
|
249
|
-
x: targetOffset.x,
|
|
250
|
-
y: targetOffset.y,
|
|
251
|
-
animated,
|
|
252
|
-
});
|
|
45
|
+
const target = scrollTargetOf(offset, narrowed.value.horizontal);
|
|
46
|
+
if (scrollNodeRef.value === null) {
|
|
47
|
+
dlog(`Vue VirtualizedList scrollTo offset=${offset} pending-ref`);
|
|
48
|
+
commandedOffset.value = target;
|
|
253
49
|
return;
|
|
254
50
|
}
|
|
255
|
-
dlog(`Vue VirtualizedList scrollTo offset=${
|
|
256
|
-
|
|
51
|
+
dlog(`Vue VirtualizedList scrollTo offset=${offset} animated=${animated}`);
|
|
52
|
+
scrollHandle.scrollTo({ x: target.x, y: target.y, animated });
|
|
257
53
|
};
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
break;
|
|
270
|
-
case 'fire-start-reached':
|
|
271
|
-
p.onStartReached?.({ distanceFromStart: effect.distanceFromStart });
|
|
272
|
-
break;
|
|
273
|
-
case 'fire-scroll-to-index-failed':
|
|
274
|
-
p.onScrollToIndexFailed?.({
|
|
275
|
-
index: effect.index,
|
|
276
|
-
highestMeasuredFrameIndex: effect.highestMeasuredFrameIndex,
|
|
277
|
-
averageItemLength: effect.averageItemLength,
|
|
278
|
-
});
|
|
279
|
-
break;
|
|
280
|
-
case 'schedule-refill': {
|
|
281
|
-
if (batchTimer !== null)
|
|
282
|
-
clearTimeout(batchTimer);
|
|
283
|
-
batchTimer = setTimeout(() => {
|
|
284
|
-
batchTimer = null;
|
|
285
|
-
dispatch({ kind: 'batch-tick' });
|
|
286
|
-
}, effect.delay);
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
case 'fire-viewable': {
|
|
290
|
-
const pairs = inputs.viewabilityPairs;
|
|
291
|
-
const info = effect.info;
|
|
292
|
-
const map = effect.map;
|
|
293
|
-
const fire = () => {
|
|
294
|
-
for (const pair of pairs) {
|
|
295
|
-
pair.onViewableItemsChanged({
|
|
296
|
-
...info,
|
|
297
|
-
viewabilityConfig: pair.viewabilityConfig,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
dispatch({ kind: 'viewable-fired', map });
|
|
301
|
-
};
|
|
302
|
-
if (viewableTimer !== null) {
|
|
303
|
-
clearTimeout(viewableTimer);
|
|
304
|
-
viewableTimer = null;
|
|
305
|
-
}
|
|
306
|
-
if (effect.delay > EMPTY_OFFSET) {
|
|
307
|
-
viewableTimer = setTimeout(() => {
|
|
308
|
-
viewableTimer = null;
|
|
309
|
-
fire();
|
|
310
|
-
}, effect.delay);
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
fire();
|
|
314
|
-
}
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
54
|
+
const effectHost = {
|
|
55
|
+
callbacks: () => ({
|
|
56
|
+
onEndReached: narrowed.value.onEndReached,
|
|
57
|
+
onStartReached: narrowed.value.onStartReached,
|
|
58
|
+
onScrollToIndexFailed: narrowed.value.onScrollToIndexFailed,
|
|
59
|
+
viewabilityPairs: viewabilityPairs.value,
|
|
60
|
+
}),
|
|
61
|
+
scrollToPixel,
|
|
62
|
+
dispatch: () => dispatch,
|
|
63
|
+
viewableTimer,
|
|
64
|
+
batchTimer,
|
|
319
65
|
};
|
|
320
66
|
const dispatch = (action) => {
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
runEffects(result.effects, inputs);
|
|
67
|
+
const result = reduceList(listState, action, buildInputs());
|
|
68
|
+
runListEffects(result.effects, effectHost);
|
|
324
69
|
if (result.changed)
|
|
325
70
|
version.value += 1;
|
|
326
71
|
};
|
|
327
|
-
// The
|
|
328
|
-
//
|
|
329
|
-
// signature both read it without re-deriving (which would double-advance the throttle).
|
|
72
|
+
// The one window recompute, cached so the render and the commit signature both read it
|
|
73
|
+
// without re-deriving (which would double-advance the throttle)
|
|
330
74
|
const metrics = computed(() => {
|
|
331
75
|
void version.value;
|
|
332
|
-
reduceList(listState, { kind:
|
|
76
|
+
reduceList(listState, { kind: LIST_ACTION_KIND.refreshMetrics }, buildInputs());
|
|
333
77
|
return listState.metrics;
|
|
334
78
|
});
|
|
335
79
|
const commitSignature = computed(() => {
|
|
336
80
|
void metrics.value;
|
|
337
81
|
return listEffectSignature(listState);
|
|
338
82
|
});
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const offset = readLayoutOffset(event, narrowed.value.horizontal);
|
|
364
|
-
dlog(`Vue VirtualizedList cell ${index} measured length=${length} offset=${offset ?? 'none'}`);
|
|
365
|
-
dispatch({ kind: 'measure', index, length, offset });
|
|
366
|
-
};
|
|
83
|
+
// The list above hands its scope down, this list hands its own to the lists in its cells
|
|
84
|
+
const nesting = createListNesting({
|
|
85
|
+
parent: useVirtualizedListScope(),
|
|
86
|
+
horizontal: narrowed.value.horizontal,
|
|
87
|
+
getState: () => listState,
|
|
88
|
+
dispatch,
|
|
89
|
+
getContainerNode: () => scrollNodeRef.value,
|
|
90
|
+
getHasMore: () => listHasMore(listState),
|
|
91
|
+
keyFor,
|
|
92
|
+
handlers: () => handlers,
|
|
93
|
+
});
|
|
94
|
+
provideVirtualizedListScope(nesting.scope);
|
|
95
|
+
const handlers = createListHandlers({
|
|
96
|
+
isHorizontal: () => narrowed.value.horizontal,
|
|
97
|
+
user: () => userHandlersOf(narrowed.value),
|
|
98
|
+
nesting: () => nesting,
|
|
99
|
+
dispatch,
|
|
100
|
+
clearCommandedOffset: () => {
|
|
101
|
+
commandedOffset.value = undefined;
|
|
102
|
+
},
|
|
103
|
+
getNode: () => scrollNodeRef.value,
|
|
104
|
+
keyFor,
|
|
105
|
+
});
|
|
106
|
+
const makeCellFocus = (index) => () => dispatch({ kind: LIST_ACTION_KIND.cellFocused, index });
|
|
367
107
|
const mergeSeparator = (gapIndex, patch) => {
|
|
368
108
|
if (!isSeparatorGapInRange(gapIndex, listState.metrics.count))
|
|
369
109
|
return;
|
|
@@ -373,246 +113,78 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
373
113
|
});
|
|
374
114
|
separatorVersion.value += 1;
|
|
375
115
|
};
|
|
376
|
-
const makeSeparators = (index) => (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
dlog(`Vue VirtualizedList separator unhighlight cell=${index}`);
|
|
384
|
-
mergeSeparator(index - 1, { highlighted: false });
|
|
385
|
-
mergeSeparator(index, { highlighted: false });
|
|
386
|
-
},
|
|
387
|
-
updateProps: (select, newProps) => {
|
|
388
|
-
mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
// ---- imperative handle (the shared IVirtualizedListHandle surface) ------
|
|
392
|
-
const handle = {
|
|
393
|
-
scrollToOffset: (params) => {
|
|
394
|
-
dispatch({
|
|
395
|
-
kind: 'scroll-to-offset',
|
|
396
|
-
offset: params.offset,
|
|
397
|
-
animated: params.animated ?? true,
|
|
398
|
-
});
|
|
399
|
-
},
|
|
400
|
-
scrollToIndex: (params) => {
|
|
401
|
-
dispatch({
|
|
402
|
-
kind: 'scroll-to-index',
|
|
403
|
-
index: params.index,
|
|
404
|
-
animated: params.animated ?? true,
|
|
405
|
-
viewPosition: params.viewPosition ?? FIRST_INDEX,
|
|
406
|
-
viewOffset: params.viewOffset ?? EMPTY_OFFSET,
|
|
407
|
-
});
|
|
408
|
-
},
|
|
409
|
-
scrollToItem: (params) => {
|
|
410
|
-
dispatch({
|
|
411
|
-
kind: 'scroll-to-item',
|
|
412
|
-
item: params.item,
|
|
413
|
-
animated: params.animated ?? true,
|
|
414
|
-
viewPosition: params.viewPosition ?? FIRST_INDEX,
|
|
415
|
-
});
|
|
416
|
-
},
|
|
417
|
-
scrollToEnd: (params) => {
|
|
418
|
-
dispatch({ kind: 'scroll-to-end', animated: params?.animated ?? true });
|
|
419
|
-
},
|
|
420
|
-
flashScrollIndicators: () => {
|
|
421
|
-
scrollHandle.flashScrollIndicators();
|
|
422
|
-
},
|
|
423
|
-
getNativeScrollRef: () => scrollNodeRef.value !== null ? scrollHandle : null,
|
|
424
|
-
getScrollableNode: () => scrollNodeRef.value !== null ? scrollHandle : null,
|
|
425
|
-
getScrollResponder: () => scrollNodeRef.value !== null ? scrollHandle : null,
|
|
426
|
-
getScrollNode: () => scrollNodeRef.value,
|
|
427
|
-
recordInteraction: () => {
|
|
428
|
-
dispatch({ kind: 'record-interaction' });
|
|
429
|
-
},
|
|
430
|
-
};
|
|
431
|
-
expose(handle);
|
|
432
|
-
// ---- after-commit pass (one post-flush watcher, the Vue twin of the layout effect) ----
|
|
433
|
-
// Runs the deferred effects (batch fill, edge-reached, viewability, initial-scroll, MVCP)
|
|
434
|
-
// whenever the windowing signature changes — the same dedup key every adapter shares.
|
|
116
|
+
const makeSeparators = (index) => buildSeparatorHandles(index, mergeSeparator);
|
|
117
|
+
expose(buildListHandle({
|
|
118
|
+
dispatch,
|
|
119
|
+
scrollHandle,
|
|
120
|
+
getNode: () => scrollNodeRef.value,
|
|
121
|
+
}));
|
|
122
|
+
// The after-commit pass, a post-flush watcher on the windowing signature
|
|
435
123
|
watch(commitSignature, () => {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
runEffects(result.effects, inputs);
|
|
124
|
+
const result = reduceList(listState, { kind: LIST_ACTION_KIND.commit }, buildInputs());
|
|
125
|
+
runListEffects(result.effects, effectHost);
|
|
439
126
|
}, { flush: 'post' });
|
|
440
127
|
onBeforeUnmount(() => {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (batchTimer !== null)
|
|
444
|
-
clearTimeout(batchTimer);
|
|
128
|
+
clearTimers([viewableTimer, batchTimer]);
|
|
129
|
+
nesting.detach();
|
|
445
130
|
});
|
|
446
131
|
return () => {
|
|
447
132
|
const p = narrowed.value;
|
|
448
133
|
const m = metrics.value;
|
|
449
|
-
//
|
|
134
|
+
// Reading the version makes a separator bump re-render
|
|
450
135
|
void separatorVersion.value;
|
|
451
|
-
if (p.renderItem === undefined)
|
|
452
|
-
dlog('Vue VirtualizedList: no #item slot provided
|
|
453
|
-
dlog(`Vue VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
454
|
-
`(offset=${listState.scrollOffset}, viewport=${listState.viewportLength}, rendered=${Math.max(0, m.last - m.first + 1)})`);
|
|
455
|
-
const children = [];
|
|
456
|
-
const stickySet = p.stickyHeaderIndices !== undefined
|
|
457
|
-
? new Set(p.stickyHeaderIndices)
|
|
458
|
-
: undefined;
|
|
459
|
-
const header = resolveElement(p.listHeaderComponent);
|
|
460
|
-
if (header !== undefined) {
|
|
461
|
-
children.push(h('view', { key: 'list-header' }, [header]));
|
|
462
|
-
}
|
|
463
|
-
if (m.count === FIRST_INDEX) {
|
|
464
|
-
const empty = resolveElement(p.listEmptyComponent);
|
|
465
|
-
if (empty !== undefined) {
|
|
466
|
-
children.push(h('view', { key: 'list-empty' }, [empty]));
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
const plan = buildListPlan({
|
|
471
|
-
count: m.count,
|
|
472
|
-
first: m.first,
|
|
473
|
-
last: m.last,
|
|
474
|
-
offsets: m.offsets,
|
|
475
|
-
lengths: m.lengths,
|
|
476
|
-
total: m.total,
|
|
477
|
-
keyFor,
|
|
478
|
-
stickyIndices: stickySet,
|
|
479
|
-
hasHeader: header !== undefined,
|
|
480
|
-
});
|
|
481
|
-
// `plan.stickyChildPositions` is deliberately NOT read — it was the input to the index form.
|
|
482
|
-
if (plan.leadingExtent > EMPTY_OFFSET) {
|
|
483
|
-
children.push(h('view', {
|
|
484
|
-
key: 'spacer-leading',
|
|
485
|
-
style: p.horizontal
|
|
486
|
-
? { width: plan.leadingExtent }
|
|
487
|
-
: { height: plan.leadingExtent },
|
|
488
|
-
}));
|
|
489
|
-
}
|
|
490
|
-
const cellInverted = p.inverted
|
|
491
|
-
? p.horizontal
|
|
492
|
-
? INVERTED_X_STYLE
|
|
493
|
-
: INVERTED_Y_STYLE
|
|
494
|
-
: undefined;
|
|
495
|
-
// The separator lives INSIDE the cell's own measuring wrapper, not as a sibling: a sibling
|
|
496
|
-
// is an extra flex child, so a leading-spacer collapse would land every cell below it
|
|
497
|
-
// short by (separator + gap). See .claude/rules/list-geometry-feedback-loop.md.
|
|
498
|
-
const pushCell = (cell) => {
|
|
499
|
-
const item = p.getItem(p.data, cell.index);
|
|
500
|
-
const content = p.renderItem?.({
|
|
501
|
-
item,
|
|
502
|
-
index: cell.index,
|
|
503
|
-
separators: makeSeparators(cell.index),
|
|
504
|
-
});
|
|
505
|
-
const separator =
|
|
506
|
-
// Gated on the last index of the DATA, not the WINDOW: since the separator lives
|
|
507
|
-
// inside the measuring wrapper, gating on the window would make a cell's own
|
|
508
|
-
// measured height change as the window slides past it.
|
|
509
|
-
cell.index < m.count - 1
|
|
510
|
-
? renderSeparatorElement(p.itemSeparatorComponent, item, p.getItem(p.data, cell.index + 1), separatorOverrides.get(cell.index))
|
|
511
|
-
: undefined;
|
|
512
|
-
children.push(h(
|
|
513
|
-
// A cell the app flagged sticky IS the `sticky-header` tag — same position, same key,
|
|
514
|
-
// same `onLayout` (the behavior forwards it rather than replacing it). The collision
|
|
515
|
-
// point then comes from the owner's DOCUMENT order, which is the one form that
|
|
516
|
-
// survives windowing; `stickyHeaderIndices` numbers paint children and does not.
|
|
517
|
-
stickySet?.has(cell.index) === true ? 'sticky-header' : 'view', {
|
|
518
|
-
key: `cell-${cell.key}`,
|
|
519
|
-
onLayout: makeCellMeasure(cell.index),
|
|
520
|
-
style: cellInverted,
|
|
521
|
-
}, separator === undefined ? [content] : [content, separator]));
|
|
522
|
-
};
|
|
523
|
-
// The nearest sticky section header below the window (RN _ensureClosestStickyHeader):
|
|
524
|
-
// force-mounted so it never gets destroyed/recreated while pinned off-window. Rendered
|
|
525
|
-
// ahead of the main window loop, with its own gap spacer to the window's first cell.
|
|
526
|
-
if (plan.forcedStickyCell !== undefined) {
|
|
527
|
-
pushCell(plan.forcedStickyCell);
|
|
528
|
-
if (plan.gapExtent > EMPTY_OFFSET) {
|
|
529
|
-
children.push(h('view', {
|
|
530
|
-
key: 'spacer-gap',
|
|
531
|
-
style: p.horizontal
|
|
532
|
-
? { width: plan.gapExtent }
|
|
533
|
-
: { height: plan.gapExtent },
|
|
534
|
-
}));
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
for (let cellPos = 0; cellPos < plan.cells.length; cellPos += 1) {
|
|
538
|
-
pushCell(plan.cells[cellPos]);
|
|
539
|
-
}
|
|
540
|
-
if (plan.trailingExtent > EMPTY_OFFSET) {
|
|
541
|
-
children.push(h('view', {
|
|
542
|
-
key: 'spacer-trailing',
|
|
543
|
-
style: p.horizontal
|
|
544
|
-
? { width: plan.trailingExtent }
|
|
545
|
-
: { height: plan.trailingExtent },
|
|
546
|
-
}));
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
const footer = resolveElement(p.listFooterComponent);
|
|
550
|
-
if (footer !== undefined) {
|
|
551
|
-
children.push(h('view', { key: 'list-footer' }, [footer]));
|
|
136
|
+
if (p.renderItem === undefined && p.listItemComponent === undefined) {
|
|
137
|
+
dlog('Vue VirtualizedList: no #item slot provided, cells render empty');
|
|
552
138
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
// VirtualizedList.js: `[inversionStyle, style]` — the app's style can override the flip.
|
|
557
|
-
const resolvedStyle = p.inverted
|
|
558
|
-
? [p.horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE, p.style]
|
|
559
|
-
: p.style;
|
|
560
|
-
// `horizontal` is NOT in the bag: the axis is the TAG (`horizontal-scroll-view`), so passing
|
|
561
|
-
// it would be redundant at best and a contradiction the behavior has to warn about at worst.
|
|
562
|
-
const scrollProps = {
|
|
563
|
-
...p.forwarded,
|
|
564
|
-
style: resolvedStyle,
|
|
565
|
-
contentContainerStyle: resolvedContentContainerStyle,
|
|
566
|
-
onScroll,
|
|
567
|
-
onLayout: onViewportLayout,
|
|
568
|
-
ref: setScrollNodeRef,
|
|
569
|
-
};
|
|
570
|
-
// The raw scroll-lifecycle callbacks (onScrollBeginDrag/…) and scrollEventThrottle are NOT in
|
|
571
|
-
// PROP_KEYS, so they already ride through via ...p.forwarded. The keyboard props come from
|
|
572
|
-
// typed props (PROP_KEYS), so re-set them explicitly here (only when provided).
|
|
573
|
-
// VirtualizedList.js:1111 — Android moves the scrollbar back after the `scale: -1` flip.
|
|
574
|
-
if (p.inverted)
|
|
575
|
-
scrollProps.isInvertedVirtualizedList = true;
|
|
576
|
-
if (p.keyboardShouldPersistTaps !== undefined) {
|
|
577
|
-
scrollProps.keyboardShouldPersistTaps = p.keyboardShouldPersistTaps;
|
|
139
|
+
if (isDebug()) {
|
|
140
|
+
dlog(`Vue VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
141
|
+
`(offset=${listState.scrollOffset}, viewport=${listState.viewportLength})`);
|
|
578
142
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
});
|
|
143
|
+
const { children, hasHeader } = buildListChildren({
|
|
144
|
+
props: p,
|
|
145
|
+
metrics: m,
|
|
146
|
+
keyFor,
|
|
147
|
+
separatorOverrides,
|
|
148
|
+
makeSeparators,
|
|
149
|
+
makeCellMeasure: handlers.makeCellMeasure,
|
|
150
|
+
makeCellFocus,
|
|
151
|
+
});
|
|
152
|
+
const scrollProps = buildScrollProps({
|
|
153
|
+
props: p,
|
|
154
|
+
total: m.total,
|
|
155
|
+
hasHeader,
|
|
156
|
+
onScroll: handlers.onScroll,
|
|
157
|
+
onScrollBeginDrag: handlers.onScrollBeginDrag,
|
|
158
|
+
onScrollEndDrag: handlers.onScrollEndDrag,
|
|
159
|
+
onMomentumScrollBegin: handlers.onMomentumScrollBegin,
|
|
160
|
+
onMomentumScrollEnd: handlers.onMomentumScrollEnd,
|
|
161
|
+
onContentSizeChange: handlers.onContentSizeChange,
|
|
162
|
+
onLayout: handlers.onViewportLayout,
|
|
163
|
+
setRef: setScrollNodeRef,
|
|
164
|
+
commandedOffset: commandedOffset.value,
|
|
165
|
+
});
|
|
166
|
+
// The list above scrolls, so RN renders a `View` with no content container
|
|
167
|
+
if (nesting.isNested) {
|
|
168
|
+
return h('view', {
|
|
169
|
+
...viewPropsOf(p.forwarded),
|
|
170
|
+
style: p.style,
|
|
171
|
+
onLayout: handlers.onViewportLayout,
|
|
172
|
+
ref: setScrollNodeRef,
|
|
173
|
+
}, children);
|
|
611
174
|
}
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
175
|
+
// With a `@refresh` listener the RefreshControl is an ordinary first child, the scroll
|
|
176
|
+
// behavior claims it and places it per platform
|
|
177
|
+
const refreshControl = p.onRefresh === undefined
|
|
178
|
+
? []
|
|
179
|
+
: [
|
|
180
|
+
h('refresh-control', {
|
|
181
|
+
refreshing: p.refreshing,
|
|
182
|
+
onRefresh: p.onRefresh,
|
|
183
|
+
progressViewOffset: p.progressViewOffset,
|
|
184
|
+
}),
|
|
185
|
+
];
|
|
186
|
+
// The scroll TAG, not a wrapper: the engine builds the content node and places the control
|
|
187
|
+
return h(p.horizontal ? 'horizontal-scroll-view' : 'scroll-view', scrollProps, [...refreshControl, ...children]);
|
|
616
188
|
};
|
|
617
189
|
}, {
|
|
618
190
|
name: 'VirtualizedList',
|