@symbiote-native/vue 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/build/components/activity-indicator/index.android.d.ts +4 -0
- package/build/components/activity-indicator/index.android.js +10 -0
- package/build/components/activity-indicator/index.d.ts +1 -0
- package/build/components/activity-indicator/index.ios.d.ts +4 -0
- package/build/components/activity-indicator/index.ios.js +10 -0
- package/build/components/activity-indicator/index.js +4 -0
- package/build/components/activity-indicator/shared.d.ts +8 -0
- package/build/components/activity-indicator/shared.js +48 -0
- package/build/components/button.d.ts +14 -0
- package/build/components/button.js +59 -0
- package/build/components/drawer-layout-android/index.android.d.ts +8 -0
- package/build/components/drawer-layout-android/index.android.js +144 -0
- package/build/components/drawer-layout-android/index.d.ts +8 -0
- package/build/components/drawer-layout-android/index.js +36 -0
- package/build/components/drawer-layout-android/shared.d.ts +18 -0
- package/build/components/drawer-layout-android/shared.js +6 -0
- package/build/components/flat-list/index.d.ts +88 -0
- package/build/components/flat-list/index.js +189 -0
- package/build/components/image-background.d.ts +9 -0
- package/build/components/image-background.js +114 -0
- package/build/components/image.d.ts +9 -0
- package/build/components/image.js +99 -0
- package/build/components/input-accessory-view.d.ts +10 -0
- package/build/components/input-accessory-view.js +41 -0
- package/build/components/keyboard-avoiding-view.d.ts +18 -0
- package/build/components/keyboard-avoiding-view.js +121 -0
- package/build/components/modal.d.ts +29 -0
- package/build/components/modal.js +134 -0
- package/build/components/pressable.d.ts +58 -0
- package/build/components/pressable.js +221 -0
- package/build/components/refresh-control.d.ts +20 -0
- package/build/components/refresh-control.js +53 -0
- package/build/components/safe-area-view.d.ts +8 -0
- package/build/components/safe-area-view.js +33 -0
- package/build/components/scroll-view/index.android.d.ts +4 -0
- package/build/components/scroll-view/index.android.js +74 -0
- package/build/components/scroll-view/index.d.ts +1 -0
- package/build/components/scroll-view/index.ios.d.ts +4 -0
- package/build/components/scroll-view/index.ios.js +15 -0
- package/build/components/scroll-view/index.js +4 -0
- package/build/components/scroll-view/shared.d.ts +96 -0
- package/build/components/scroll-view/shared.js +297 -0
- package/build/components/scroll-view/sticky-header.d.ts +7 -0
- package/build/components/scroll-view/sticky-header.js +195 -0
- package/build/components/section-list/index.d.ts +39 -0
- package/build/components/section-list/index.js +77 -0
- package/build/components/switch/index.android.d.ts +12 -0
- package/build/components/switch/index.android.js +13 -0
- package/build/components/switch/index.d.ts +2 -0
- package/build/components/switch/index.ios.d.ts +12 -0
- package/build/components/switch/index.ios.js +11 -0
- package/build/components/switch/index.js +5 -0
- package/build/components/switch/shared.d.ts +23 -0
- package/build/components/switch/shared.js +129 -0
- package/build/components/text-input/index.d.ts +24 -0
- package/build/components/text-input/index.js +257 -0
- package/build/components/touchable-native-feedback.d.ts +24 -0
- package/build/components/touchable-native-feedback.js +75 -0
- package/build/components/touchable.d.ts +44 -0
- package/build/components/touchable.js +150 -0
- package/build/components/virtualized-list/index.d.ts +121 -0
- package/build/components/virtualized-list/index.js +687 -0
- package/build/components/virtualized-section-list/index.d.ts +96 -0
- package/build/components/virtualized-section-list/index.js +160 -0
- package/build/components.d.ts +51 -0
- package/build/components.js +26 -0
- package/build/composables/use-color-scheme.d.ts +3 -0
- package/build/composables/use-color-scheme.js +24 -0
- package/build/composables/use-window-dimensions.d.ts +3 -0
- package/build/composables/use-window-dimensions.js +34 -0
- package/build/create-tunnel.d.ts +9 -0
- package/build/create-tunnel.js +37 -0
- package/build/descriptor-to-vue.d.ts +3 -0
- package/build/descriptor-to-vue.js +14 -0
- package/build/host-instance/index.d.ts +2 -0
- package/build/host-instance/index.js +34 -0
- package/build/index.d.ts +67 -0
- package/build/index.js +90 -0
- package/build/modules/animated/create-animated-component.d.ts +4 -0
- package/build/modules/animated/create-animated-component.js +169 -0
- package/build/modules/animated/index.d.ts +55 -0
- package/build/modules/animated/index.js +66 -0
- package/build/modules/app-registry/index.d.ts +7 -0
- package/build/modules/app-registry/index.js +32 -0
- package/build/modules/status-bar.d.ts +17 -0
- package/build/modules/status-bar.js +65 -0
- package/build/render.d.ts +7 -0
- package/build/render.js +43 -0
- package/build/renderer.d.ts +4 -0
- package/build/renderer.js +129 -0
- package/build/runtime-helpers.d.ts +28 -0
- package/build/runtime-helpers.js +68 -0
- package/build/utils/component-helpers.d.ts +2 -0
- package/build/utils/component-helpers.js +13 -0
- package/build/utils/model-binding.d.ts +4 -0
- package/build/utils/model-binding.js +17 -0
- package/build/utils/normalize-attrs.d.ts +1 -0
- package/build/utils/normalize-attrs.js +30 -0
- package/build/utils/slots-to-render-props.d.ts +4 -0
- package/build/utils/slots-to-render-props.js +18 -0
- package/metro-css-parser.cjs +12 -0
- package/package.json +54 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
// VirtualizedList, the Vue lifecycle half. The windowing engine (offset table, window
|
|
2
|
+
// compute, batch throttle, viewability, edge-reached, the child PLAN, the imperative-handle
|
|
3
|
+
// surface) lives in @symbiote-native/components/state, shared verbatim with the React adapter. Here
|
|
4
|
+
// Vue supplies only the reactivity: refs for scroll offset / viewport / measurement bumps, a
|
|
5
|
+
// `computed` that runs the shared windowing math, post-flush watchers for the after-commit
|
|
6
|
+
// work (batch fill, onEndReached/onStartReached, viewability, initialScroll, MVCP), and the
|
|
7
|
+
// imperative handle via expose(). This is the Vue twin of the React adapter's
|
|
8
|
+
// useState/useRef/useEffect over the same shared functions. It drives the Vue ScrollView,
|
|
9
|
+
// exactly as the React list drives the React ScrollView.
|
|
10
|
+
//
|
|
11
|
+
// Typed-emits generic component (mirrors FlatList): a GENERIC setup function
|
|
12
|
+
// `<ItemT,>(props: IVirtualizedListProps<ItemT>, ctx: ICtx<IVirtualizedListEmits<ItemT>>)` so the
|
|
13
|
+
// five adapter-synthesized events emit with ItemT-typed payloads (`@viewable-items-changed` carries
|
|
14
|
+
// ItemT, not unknown). For that to infer at the call site, the generic INPUTS (data/getItem/
|
|
15
|
+
// renderItem/…) are read from typed `props`, declared in the runtime `props` array; the raw native
|
|
16
|
+
// scroll events (onScroll/onScrollBeginDrag/…) + any unknown attrs ride through `$attrs` onto the
|
|
17
|
+
// inner ScrollView. RefreshControl + viewability stay GATED on listener presence, so each emit
|
|
18
|
+
// bridge is wired ONLY when the consumer actually listens (read off the instance vnode props),
|
|
19
|
+
// keeping behavior identical to the prop-callback era.
|
|
20
|
+
//
|
|
21
|
+
// Lists have no Descriptor render fn (the cell content is the framework's own children); see
|
|
22
|
+
// core/components/.docs-note-lists.md. Cells/spacers are built with h() directly off the plan.
|
|
23
|
+
//
|
|
24
|
+
// Reactivity gotcha (.claude/skills/vue-adapter-reactivity): the ScrollView's exposed handle
|
|
25
|
+
// is held in a shallowRef so the engine node it closes over is reached by identity; a deep
|
|
26
|
+
// ref would proxy it and the imperative scroll commands would silently no-op.
|
|
27
|
+
import { computed, defineComponent, getCurrentInstance, h, isVNode, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
|
|
28
|
+
import { DEFAULT_END_REACHED_THRESHOLD, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_START_REACHED_THRESHOLD, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, NO_CONTENT_LENGTH_SENT, averageMeasuredLength, buildListPlan, buildOffsets, buildViewabilityPairs, computeEndReached, computeStartReached, computeViewableSet, computeWindow, diffViewable, highestMeasuredIndex, maxMinimumViewTime, offsetForIndex, readLayoutLength, readScrollOffset, throttleWindow, } from '@symbiote-native/components';
|
|
29
|
+
import { dlog, } from '@symbiote-native/engine';
|
|
30
|
+
import { ScrollView } from '../scroll-view';
|
|
31
|
+
import { RefreshControl } from '../refresh-control';
|
|
32
|
+
import { normalizeVueAttrs } from '../../utils/normalize-attrs';
|
|
33
|
+
import { componentFromSlot } from '../../utils/slots-to-render-props';
|
|
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 isRecord(value) {
|
|
76
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
function isComponent(value) {
|
|
79
|
+
return typeof value === 'function' || (typeof value === 'object' && value !== null);
|
|
80
|
+
}
|
|
81
|
+
function asNumber(value, fallback) {
|
|
82
|
+
return typeof value === 'number' ? value : fallback;
|
|
83
|
+
}
|
|
84
|
+
// The attrs that arrive via $attrs but must NOT be forwarded raw onto the inner ScrollView: VL
|
|
85
|
+
// composes its own onScroll (windowing offset + the user's onScroll) and its own onLayout (viewport
|
|
86
|
+
// measure), so a consumer-passed one is dropped here and re-set explicitly on the scroll props.
|
|
87
|
+
const HANDLED_ATTRS = ['onScroll', 'onLayout'];
|
|
88
|
+
function forwardAttrs(attrs) {
|
|
89
|
+
const result = {};
|
|
90
|
+
for (const key of Object.keys(attrs)) {
|
|
91
|
+
if (!HANDLED_ATTRS.includes(key))
|
|
92
|
+
result[key] = attrs[key];
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
// A list component element (ListHeaderComponent / ListEmptyComponent / ListFooterComponent) is
|
|
97
|
+
// either a Vue component (invoked via h) or a ready VNode (passed through). RN resolves the same
|
|
98
|
+
// `Component | element` union; we narrow with isVNode before isComponent (a VNode is an object).
|
|
99
|
+
function resolveElement(value) {
|
|
100
|
+
if (value === undefined || value === null)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (isVNode(value))
|
|
103
|
+
return value;
|
|
104
|
+
if (isComponent(value))
|
|
105
|
+
return h(value);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
function renderSeparatorElement(component, leadingItem, trailingItem, overrides) {
|
|
109
|
+
if (component === undefined)
|
|
110
|
+
return undefined;
|
|
111
|
+
const props = {
|
|
112
|
+
highlighted: false,
|
|
113
|
+
leadingItem,
|
|
114
|
+
trailingItem,
|
|
115
|
+
...overrides,
|
|
116
|
+
};
|
|
117
|
+
return h(component, props);
|
|
118
|
+
}
|
|
119
|
+
function isScrollViewHandle(value) {
|
|
120
|
+
return isRecord(value) && typeof value.scrollTo === 'function';
|
|
121
|
+
}
|
|
122
|
+
export const VirtualizedList = defineComponent((props, { attrs, expose, emit, slots, }) => {
|
|
123
|
+
// Reactive lifecycle state (the Vue twin of React's useState): a change in any of these
|
|
124
|
+
// re-runs the windowing `computed` and the render fn.
|
|
125
|
+
const scrollOffset = ref(EMPTY_OFFSET);
|
|
126
|
+
const viewportLength = ref(EMPTY_OFFSET);
|
|
127
|
+
// The offset we are imperatively driving native to. A fresh object identity each push so the
|
|
128
|
+
// commit path re-applies it even when the numeric value repeats. undefined = none pending.
|
|
129
|
+
const commandedOffset = ref(undefined);
|
|
130
|
+
// Bumped when a NEW cell measurement lands or a batch tick fires, to request a re-render
|
|
131
|
+
// without thrashing on already-known cells (React's measuredRef + setMeasureVersion).
|
|
132
|
+
const measureVersion = ref(EMPTY_OFFSET);
|
|
133
|
+
const separatorVersion = ref(EMPTY_OFFSET);
|
|
134
|
+
// shallowRef, NOT ref: the ScrollView handle closes over the engine scroll node, reached by
|
|
135
|
+
// identity through the engine's WeakMap mirror. A deep ref would proxy the object and every
|
|
136
|
+
// imperative scroll (scrollToOffset/Index/…) would miss the node and silently no-op. See
|
|
137
|
+
// .claude/skills/vue-adapter-reactivity.
|
|
138
|
+
const scrollHandle = shallowRef(null);
|
|
139
|
+
const setScrollHandle = (instance) => {
|
|
140
|
+
scrollHandle.value = isScrollViewHandle(instance) ? instance : null;
|
|
141
|
+
};
|
|
142
|
+
// The five list events are emits, so Vue strips their onX listeners from $attrs. Detect listener
|
|
143
|
+
// presence off the instance's own vnode props (what the parent passed), exactly like FlatList, so
|
|
144
|
+
// an emit bridge is wired ONLY when the consumer actually listens (preserving the RefreshControl /
|
|
145
|
+
// viewability gating of the prop-callback era).
|
|
146
|
+
const instance = getCurrentInstance();
|
|
147
|
+
const listens = (onName) => {
|
|
148
|
+
const vnodeProps = instance?.vnode.props;
|
|
149
|
+
return vnodeProps != null && typeof vnodeProps[onName] === 'function';
|
|
150
|
+
};
|
|
151
|
+
// Non-reactive setup-scope state (the Vue twin of React's refs that never trigger a render):
|
|
152
|
+
const measured = new Map();
|
|
153
|
+
// The previously committed window, so throttleWindow grows it by at most maxToRenderPerBatch
|
|
154
|
+
// per tick instead of snapping.
|
|
155
|
+
let committedWindow = { first: FIRST_INDEX, last: -1 };
|
|
156
|
+
let sentEndForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
157
|
+
let sentStartForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
158
|
+
let lastViewable = new Map();
|
|
159
|
+
let viewableTimer = null;
|
|
160
|
+
let batchTimer = null;
|
|
161
|
+
let hasInteracted = false;
|
|
162
|
+
const separatorOverrides = new Map();
|
|
163
|
+
let appliedInitialScroll = false;
|
|
164
|
+
let firstVisibleKey = null;
|
|
165
|
+
const narrowed = computed(() => {
|
|
166
|
+
// extraData has no field of its own; reading it tracks it so a change forces a re-render
|
|
167
|
+
// (RN's extraData contract).
|
|
168
|
+
void props.extraData;
|
|
169
|
+
const folded = normalizeVueAttrs(attrs);
|
|
170
|
+
return {
|
|
171
|
+
data: props.data,
|
|
172
|
+
getItem: props.getItem,
|
|
173
|
+
getItemCount: props.getItemCount,
|
|
174
|
+
// Cell + chrome come from scoped slots. #item IS the render fn (a slot fn returning
|
|
175
|
+
// VNode|VNode[], which the cell wrapper accepts directly). #separator carries scope props
|
|
176
|
+
// (h(component, props)) so it goes through componentFromSlot; #header/#footer/#empty are
|
|
177
|
+
// scopeless, so the bare slot fn is handed to resolveElement (its isComponent branch).
|
|
178
|
+
renderItem: slots.item,
|
|
179
|
+
keyExtractor: props.keyExtractor,
|
|
180
|
+
getItemLayout: props.getItemLayout,
|
|
181
|
+
itemSeparatorComponent: componentFromSlot(slots.separator),
|
|
182
|
+
listHeaderComponent: slots.header,
|
|
183
|
+
listFooterComponent: slots.footer,
|
|
184
|
+
listEmptyComponent: slots.empty,
|
|
185
|
+
horizontal: props.horizontal === true,
|
|
186
|
+
inverted: props.inverted === true,
|
|
187
|
+
onEndReached: listens('onEndReached')
|
|
188
|
+
? (info) => emit('endReached', info)
|
|
189
|
+
: undefined,
|
|
190
|
+
onEndReachedThreshold: asNumber(props.onEndReachedThreshold, DEFAULT_END_REACHED_THRESHOLD),
|
|
191
|
+
onStartReached: listens('onStartReached')
|
|
192
|
+
? (info) => emit('startReached', info)
|
|
193
|
+
: undefined,
|
|
194
|
+
onStartReachedThreshold: asNumber(props.onStartReachedThreshold, DEFAULT_START_REACHED_THRESHOLD),
|
|
195
|
+
onRefresh: listens('onRefresh') ? () => emit('refresh') : undefined,
|
|
196
|
+
refreshing: props.refreshing === true,
|
|
197
|
+
progressViewOffset: props.progressViewOffset,
|
|
198
|
+
onViewableItemsChanged: listens('onViewableItemsChanged')
|
|
199
|
+
? (info) => emit('viewableItemsChanged', info)
|
|
200
|
+
: undefined,
|
|
201
|
+
viewabilityConfig: props.viewabilityConfig,
|
|
202
|
+
viewabilityConfigCallbackPairs: props.viewabilityConfigCallbackPairs,
|
|
203
|
+
onScrollToIndexFailed: listens('onScrollToIndexFailed')
|
|
204
|
+
? (info) => emit('scrollToIndexFailed', info)
|
|
205
|
+
: undefined,
|
|
206
|
+
initialNumToRender: asNumber(props.initialNumToRender, DEFAULT_INITIAL_NUM_TO_RENDER),
|
|
207
|
+
initialScrollIndex: props.initialScrollIndex,
|
|
208
|
+
maxToRenderPerBatch: asNumber(props.maxToRenderPerBatch, DEFAULT_MAX_TO_RENDER_PER_BATCH),
|
|
209
|
+
updateCellsBatchingPeriod: asNumber(props.updateCellsBatchingPeriod, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD),
|
|
210
|
+
windowSize: asNumber(props.windowSize, DEFAULT_WINDOW_SIZE),
|
|
211
|
+
stickyHeaderIndices: props.stickyHeaderIndices,
|
|
212
|
+
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
|
|
213
|
+
userOnScroll: isHandler(folded.onScroll) ? folded.onScroll : undefined,
|
|
214
|
+
style: props.style,
|
|
215
|
+
contentContainerStyle: props.contentContainerStyle,
|
|
216
|
+
keyboardShouldPersistTaps: props.keyboardShouldPersistTaps,
|
|
217
|
+
keyboardDismissMode: props.keyboardDismissMode,
|
|
218
|
+
forwarded: forwardAttrs(folded),
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
const keyFor = (index) => {
|
|
222
|
+
const p = narrowed.value;
|
|
223
|
+
const item = p.getItem(p.data, index);
|
|
224
|
+
return p.keyExtractor ? p.keyExtractor(item, index) : String(index);
|
|
225
|
+
};
|
|
226
|
+
// The windowing math, run through the shared @symbiote-native/components functions. It mutates the
|
|
227
|
+
// non-reactive committedWindow as it throttles (the controlled side effect React runs during
|
|
228
|
+
// render): committedWindow is plain state, so the write triggers no reactivity loop.
|
|
229
|
+
const metrics = computed(() => {
|
|
230
|
+
const p = narrowed.value;
|
|
231
|
+
void measureVersion.value;
|
|
232
|
+
const count = p.getItemCount(p.data);
|
|
233
|
+
const gil = p.getItemLayout;
|
|
234
|
+
const data = p.data;
|
|
235
|
+
const fixedLayout = gil !== undefined
|
|
236
|
+
? (index) => {
|
|
237
|
+
const layout = gil(data, index);
|
|
238
|
+
return { length: layout.length, offset: layout.offset };
|
|
239
|
+
}
|
|
240
|
+
: undefined;
|
|
241
|
+
const averageLength = fixedLayout !== undefined
|
|
242
|
+
? count > FIRST_INDEX
|
|
243
|
+
? fixedLayout(FIRST_INDEX).length
|
|
244
|
+
: EMPTY_OFFSET
|
|
245
|
+
: averageMeasuredLength(measured);
|
|
246
|
+
const { offsets, lengths, total } = buildOffsets(count, measured, fixedLayout, averageLength);
|
|
247
|
+
const target = computeWindow(count, offsets, lengths, scrollOffset.value, viewportLength.value, p.windowSize, p.initialNumToRender);
|
|
248
|
+
const throttled = throttleWindow(target, committedWindow, p.maxToRenderPerBatch);
|
|
249
|
+
committedWindow = throttled;
|
|
250
|
+
return {
|
|
251
|
+
count,
|
|
252
|
+
offsets,
|
|
253
|
+
lengths,
|
|
254
|
+
total,
|
|
255
|
+
first: throttled.first,
|
|
256
|
+
last: throttled.last,
|
|
257
|
+
target,
|
|
258
|
+
fixedLayout,
|
|
259
|
+
averageLength,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
const scrollToPixel = (offset, animated) => {
|
|
263
|
+
const p = narrowed.value;
|
|
264
|
+
const clamped = Math.max(EMPTY_OFFSET, offset);
|
|
265
|
+
const targetOffset = p.horizontal
|
|
266
|
+
? { x: clamped, y: EMPTY_OFFSET }
|
|
267
|
+
: { x: EMPTY_OFFSET, y: clamped };
|
|
268
|
+
const handle = scrollHandle.value;
|
|
269
|
+
if (handle !== null) {
|
|
270
|
+
dlog(`Vue VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${p.horizontal})`);
|
|
271
|
+
handle.scrollTo({ x: targetOffset.x, y: targetOffset.y, animated });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
dlog(`Vue VirtualizedList scrollTo offset=${clamped} pending-ref`);
|
|
275
|
+
commandedOffset.value = targetOffset;
|
|
276
|
+
};
|
|
277
|
+
const offsetForIndexLocal = (index, viewPosition, viewOffset) => {
|
|
278
|
+
const m = metrics.value;
|
|
279
|
+
return offsetForIndex(index, viewPosition, viewOffset, m.count, m.offsets, m.lengths, viewportLength.value);
|
|
280
|
+
};
|
|
281
|
+
const onScroll = (event) => {
|
|
282
|
+
const p = narrowed.value;
|
|
283
|
+
const offset = readScrollOffset(event, p.horizontal);
|
|
284
|
+
if (offset === undefined)
|
|
285
|
+
return;
|
|
286
|
+
dlog(`Vue VirtualizedList onScroll offset=${offset}`);
|
|
287
|
+
// First scroll is the interaction that ungates waitForInteraction configs.
|
|
288
|
+
hasInteracted = true;
|
|
289
|
+
scrollOffset.value = offset;
|
|
290
|
+
// A real native scroll supersedes any pending commanded offset.
|
|
291
|
+
commandedOffset.value = undefined;
|
|
292
|
+
// Compose, don't clobber: internal windowing ran first, now the user's onScroll.
|
|
293
|
+
if (p.userOnScroll !== undefined)
|
|
294
|
+
p.userOnScroll(event);
|
|
295
|
+
};
|
|
296
|
+
const onViewportLayout = (event) => {
|
|
297
|
+
const length = readLayoutLength(event, narrowed.value.horizontal);
|
|
298
|
+
if (length === undefined)
|
|
299
|
+
return;
|
|
300
|
+
dlog(`Vue VirtualizedList onLayout viewport=${length}`);
|
|
301
|
+
viewportLength.value = length;
|
|
302
|
+
};
|
|
303
|
+
const makeCellMeasure = (index) => (event) => {
|
|
304
|
+
const p = narrowed.value;
|
|
305
|
+
if (p.getItemLayout !== undefined)
|
|
306
|
+
return;
|
|
307
|
+
const length = readLayoutLength(event, p.horizontal);
|
|
308
|
+
if (length === undefined)
|
|
309
|
+
return;
|
|
310
|
+
if (measured.get(index) === length)
|
|
311
|
+
return;
|
|
312
|
+
measured.set(index, length);
|
|
313
|
+
dlog(`Vue VirtualizedList cell ${index} measured length=${length}`);
|
|
314
|
+
measureVersion.value += 1;
|
|
315
|
+
};
|
|
316
|
+
const mergeSeparator = (gapIndex, patch) => {
|
|
317
|
+
const count = metrics.value.count;
|
|
318
|
+
if (gapIndex < FIRST_INDEX || gapIndex > count - 2)
|
|
319
|
+
return;
|
|
320
|
+
separatorOverrides.set(gapIndex, { ...separatorOverrides.get(gapIndex), ...patch });
|
|
321
|
+
separatorVersion.value += 1;
|
|
322
|
+
};
|
|
323
|
+
const makeSeparators = (index) => ({
|
|
324
|
+
highlight: () => {
|
|
325
|
+
dlog(`Vue VirtualizedList separator highlight cell=${index}`);
|
|
326
|
+
mergeSeparator(index - 1, { highlighted: true });
|
|
327
|
+
mergeSeparator(index, { highlighted: true });
|
|
328
|
+
},
|
|
329
|
+
unhighlight: () => {
|
|
330
|
+
dlog(`Vue VirtualizedList separator unhighlight cell=${index}`);
|
|
331
|
+
mergeSeparator(index - 1, { highlighted: false });
|
|
332
|
+
mergeSeparator(index, { highlighted: false });
|
|
333
|
+
},
|
|
334
|
+
updateProps: (select, newProps) => {
|
|
335
|
+
mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
// ---- imperative handle (the shared IVirtualizedListHandle surface) ------
|
|
339
|
+
const handle = {
|
|
340
|
+
scrollToOffset: (params) => {
|
|
341
|
+
scrollToPixel(params.offset, params.animated ?? true);
|
|
342
|
+
},
|
|
343
|
+
scrollToIndex: (params) => {
|
|
344
|
+
const p = narrowed.value;
|
|
345
|
+
const m = metrics.value;
|
|
346
|
+
// No getItemLayout AND the target is past the last measured cell: report the failure
|
|
347
|
+
// instead of scrolling to a fabricated estimate (RN VirtualizedList.js:179-195).
|
|
348
|
+
if (p.getItemLayout === undefined && params.index > highestMeasuredIndex(measured)) {
|
|
349
|
+
dlog(`Vue VirtualizedList onScrollToIndexFailed index=${params.index} ` +
|
|
350
|
+
`highestMeasured=${highestMeasuredIndex(measured)} (no getItemLayout)`);
|
|
351
|
+
p.onScrollToIndexFailed?.({
|
|
352
|
+
index: params.index,
|
|
353
|
+
highestMeasuredFrameIndex: highestMeasuredIndex(measured),
|
|
354
|
+
averageItemLength: m.averageLength,
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
scrollToPixel(offsetForIndexLocal(params.index, params.viewPosition ?? FIRST_INDEX, params.viewOffset ?? EMPTY_OFFSET), params.animated ?? true);
|
|
359
|
+
},
|
|
360
|
+
scrollToItem: (params) => {
|
|
361
|
+
const p = narrowed.value;
|
|
362
|
+
const count = metrics.value.count;
|
|
363
|
+
for (let index = FIRST_INDEX; index < count; index += 1) {
|
|
364
|
+
if (p.getItem(p.data, index) === params.item) {
|
|
365
|
+
scrollToPixel(offsetForIndexLocal(index, params.viewPosition ?? FIRST_INDEX, EMPTY_OFFSET), params.animated ?? true);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
dlog('Vue VirtualizedList scrollToItem: item not found');
|
|
370
|
+
},
|
|
371
|
+
scrollToEnd: (params) => {
|
|
372
|
+
scrollToPixel(Math.max(EMPTY_OFFSET, metrics.value.total - viewportLength.value), params?.animated ?? true);
|
|
373
|
+
},
|
|
374
|
+
flashScrollIndicators: () => {
|
|
375
|
+
scrollHandle.value?.flashScrollIndicators();
|
|
376
|
+
},
|
|
377
|
+
getNativeScrollRef: () => scrollHandle.value,
|
|
378
|
+
getScrollableNode: () => scrollHandle.value,
|
|
379
|
+
getScrollResponder: () => scrollHandle.value,
|
|
380
|
+
getScrollNode: () => scrollHandle.value?.getScrollNode() ?? null,
|
|
381
|
+
recordInteraction: () => {
|
|
382
|
+
hasInteracted = true;
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
expose(handle);
|
|
386
|
+
// ---- after-commit effects (post-flush watchers, the Vue twin of useEffect) ----
|
|
387
|
+
// Batch fill: when the throttled window has not reached the target, schedule a re-render so
|
|
388
|
+
// the window keeps filling toward target over successive ticks (React's batch effect).
|
|
389
|
+
watch(metrics, m => {
|
|
390
|
+
if (batchTimer !== null) {
|
|
391
|
+
clearTimeout(batchTimer);
|
|
392
|
+
batchTimer = null;
|
|
393
|
+
}
|
|
394
|
+
if (m.first <= m.target.first && m.last >= m.target.last)
|
|
395
|
+
return;
|
|
396
|
+
batchTimer = setTimeout(() => {
|
|
397
|
+
batchTimer = null;
|
|
398
|
+
measureVersion.value += 1;
|
|
399
|
+
}, narrowed.value.updateCellsBatchingPeriod);
|
|
400
|
+
}, { flush: 'post' });
|
|
401
|
+
// onEndReached: fire only when the actual last cell is rendered AND within threshold; dedup by
|
|
402
|
+
// content length; re-arm on scroll away from the end (RN _maybeCallOnEdgeReached).
|
|
403
|
+
watch(() => [narrowed.value, scrollOffset.value, viewportLength.value, metrics.value], () => {
|
|
404
|
+
const p = narrowed.value;
|
|
405
|
+
if (p.onEndReached === undefined || viewportLength.value <= EMPTY_OFFSET)
|
|
406
|
+
return;
|
|
407
|
+
const m = metrics.value;
|
|
408
|
+
const { distanceFromEnd, withinThreshold } = computeEndReached(m.total, scrollOffset.value, viewportLength.value, p.onEndReachedThreshold);
|
|
409
|
+
const lastCellRendered = m.last === m.count - 1;
|
|
410
|
+
if (withinThreshold && lastCellRendered && sentEndForContentLength !== m.total) {
|
|
411
|
+
sentEndForContentLength = m.total;
|
|
412
|
+
dlog(`Vue VirtualizedList onEndReached distanceFromEnd=${distanceFromEnd} ` +
|
|
413
|
+
`(last=${m.last} of ${m.count}, contentLength=${m.total})`);
|
|
414
|
+
p.onEndReached({ distanceFromEnd });
|
|
415
|
+
}
|
|
416
|
+
if (!withinThreshold)
|
|
417
|
+
sentEndForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
418
|
+
}, { flush: 'post' });
|
|
419
|
+
// onStartReached: the top-edge twin of onEndReached.
|
|
420
|
+
watch(() => [narrowed.value, scrollOffset.value, viewportLength.value, metrics.value], () => {
|
|
421
|
+
const p = narrowed.value;
|
|
422
|
+
if (p.onStartReached === undefined || viewportLength.value <= EMPTY_OFFSET)
|
|
423
|
+
return;
|
|
424
|
+
const m = metrics.value;
|
|
425
|
+
const { distanceFromStart, withinThreshold } = computeStartReached(scrollOffset.value, viewportLength.value, p.onStartReachedThreshold);
|
|
426
|
+
const firstCellRendered = m.first === FIRST_INDEX;
|
|
427
|
+
if (withinThreshold && firstCellRendered && sentStartForContentLength !== m.total) {
|
|
428
|
+
sentStartForContentLength = m.total;
|
|
429
|
+
dlog(`Vue VirtualizedList onStartReached distanceFromStart=${distanceFromStart} ` +
|
|
430
|
+
`(first=${m.first}, contentLength=${m.total})`);
|
|
431
|
+
p.onStartReached({ distanceFromStart });
|
|
432
|
+
}
|
|
433
|
+
if (!withinThreshold)
|
|
434
|
+
sentStartForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
435
|
+
}, { flush: 'post' });
|
|
436
|
+
// Viewability: recompute which rendered cells clear the threshold and, if the viewable set
|
|
437
|
+
// changed, fire onViewableItemsChanged + every config/callback pair, honoring minimumViewTime.
|
|
438
|
+
watch(() => [narrowed.value, scrollOffset.value, viewportLength.value, metrics.value], () => {
|
|
439
|
+
const p = narrowed.value;
|
|
440
|
+
const m = metrics.value;
|
|
441
|
+
const pairs = buildViewabilityPairs(p.onViewableItemsChanged, p.viewabilityConfig, p.viewabilityConfigCallbackPairs);
|
|
442
|
+
if (pairs.length === EMPTY_OFFSET || viewportLength.value <= EMPTY_OFFSET)
|
|
443
|
+
return;
|
|
444
|
+
if (m.count === FIRST_INDEX)
|
|
445
|
+
return;
|
|
446
|
+
const { tokens, map } = computeViewableSet({
|
|
447
|
+
first: m.first,
|
|
448
|
+
last: m.last,
|
|
449
|
+
count: m.count,
|
|
450
|
+
offsets: m.offsets,
|
|
451
|
+
lengths: m.lengths,
|
|
452
|
+
scrollOffset: scrollOffset.value,
|
|
453
|
+
viewportLength: viewportLength.value,
|
|
454
|
+
data: p.data,
|
|
455
|
+
getItem: p.getItem,
|
|
456
|
+
keyExtractor: p.keyExtractor,
|
|
457
|
+
pairs,
|
|
458
|
+
hasInteracted,
|
|
459
|
+
});
|
|
460
|
+
const diff = diffViewable(lastViewable, map, tokens);
|
|
461
|
+
if (!diff.hasChanged)
|
|
462
|
+
return;
|
|
463
|
+
const commitAndFire = () => {
|
|
464
|
+
lastViewable = map;
|
|
465
|
+
dlog(`Vue VirtualizedList viewable=${tokens.length} changed=${diff.changed.length} ` +
|
|
466
|
+
`(window [${m.first}, ${m.last}])`);
|
|
467
|
+
const info = {
|
|
468
|
+
viewableItems: tokens,
|
|
469
|
+
changed: diff.changed,
|
|
470
|
+
};
|
|
471
|
+
for (const pair of pairs)
|
|
472
|
+
pair.onViewableItemsChanged(info);
|
|
473
|
+
};
|
|
474
|
+
const minimumViewTime = maxMinimumViewTime(pairs);
|
|
475
|
+
if (viewableTimer !== null) {
|
|
476
|
+
clearTimeout(viewableTimer);
|
|
477
|
+
viewableTimer = null;
|
|
478
|
+
}
|
|
479
|
+
if (minimumViewTime > EMPTY_OFFSET) {
|
|
480
|
+
dlog(`Vue VirtualizedList viewability debounce ${minimumViewTime}ms (window [${m.first}, ${m.last}])`);
|
|
481
|
+
viewableTimer = setTimeout(() => {
|
|
482
|
+
viewableTimer = null;
|
|
483
|
+
commitAndFire();
|
|
484
|
+
}, minimumViewTime);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
commitAndFire();
|
|
488
|
+
}, { flush: 'post' });
|
|
489
|
+
// initialScrollIndex: once the first viewport is known, jump to that index a single time.
|
|
490
|
+
watch(() => [narrowed.value, viewportLength.value, metrics.value], () => {
|
|
491
|
+
const p = narrowed.value;
|
|
492
|
+
if (p.initialScrollIndex === undefined || appliedInitialScroll)
|
|
493
|
+
return;
|
|
494
|
+
if (viewportLength.value <= EMPTY_OFFSET || metrics.value.count === FIRST_INDEX)
|
|
495
|
+
return;
|
|
496
|
+
appliedInitialScroll = true;
|
|
497
|
+
// The initial jump is instant (RN does not animate initialScrollIndex).
|
|
498
|
+
scrollToPixel(offsetForIndexLocal(p.initialScrollIndex, FIRST_INDEX, EMPTY_OFFSET), false);
|
|
499
|
+
}, { flush: 'post' });
|
|
500
|
+
// maintainVisibleContentPosition JS anchor adjustment: track the key of the item at
|
|
501
|
+
// minIndexForVisible; when a prepend moves it down, add the inserted extent in the leading
|
|
502
|
+
// SPACER (the off-window items native MVCP cannot see) to scrollOffset so the anchored item
|
|
503
|
+
// does not jump (RN getDerivedStateFromProps). Runs post-flush so the correction lands fast.
|
|
504
|
+
watch(() => [narrowed.value, metrics.value, scrollOffset.value], () => {
|
|
505
|
+
const p = narrowed.value;
|
|
506
|
+
const m = metrics.value;
|
|
507
|
+
if (p.maintainVisibleContentPosition === undefined || m.count === FIRST_INDEX) {
|
|
508
|
+
firstVisibleKey = null;
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const minIndexForVisible = p.maintainVisibleContentPosition.minIndexForVisible;
|
|
512
|
+
const newFirstVisibleKey = m.count > minIndexForVisible ? keyFor(minIndexForVisible) : null;
|
|
513
|
+
const prevKey = firstVisibleKey;
|
|
514
|
+
if (prevKey !== null && newFirstVisibleKey !== null && prevKey !== newFirstVisibleKey) {
|
|
515
|
+
let anchorIndex = -1;
|
|
516
|
+
for (let index = minIndexForVisible; index < m.count; index += 1) {
|
|
517
|
+
if (keyFor(index) === prevKey) {
|
|
518
|
+
anchorIndex = index;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (anchorIndex > minIndexForVisible) {
|
|
523
|
+
const spacerEnd = Math.min(anchorIndex, committedWindow.first);
|
|
524
|
+
const insertedExtent = spacerEnd > minIndexForVisible
|
|
525
|
+
? m.offsets[spacerEnd] - m.offsets[minIndexForVisible]
|
|
526
|
+
: EMPTY_OFFSET;
|
|
527
|
+
if (insertedExtent > EMPTY_OFFSET) {
|
|
528
|
+
const autoThreshold = p.maintainVisibleContentPosition.autoscrollToTopThreshold;
|
|
529
|
+
const anchoredNearTop = autoThreshold !== undefined && scrollOffset.value <= autoThreshold;
|
|
530
|
+
if (anchoredNearTop) {
|
|
531
|
+
dlog(`Vue VirtualizedList MVCP autoscroll-to-top (offset=${scrollOffset.value} <= ${autoThreshold})`);
|
|
532
|
+
scrollToPixel(EMPTY_OFFSET, true);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
dlog(`Vue VirtualizedList MVCP adjust +${insertedExtent}px ` +
|
|
536
|
+
`(anchor "${prevKey}" moved ${minIndexForVisible}->${anchorIndex})`);
|
|
537
|
+
scrollToPixel(scrollOffset.value + insertedExtent, false);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
firstVisibleKey = newFirstVisibleKey;
|
|
543
|
+
}, { flush: 'post' });
|
|
544
|
+
onBeforeUnmount(() => {
|
|
545
|
+
if (viewableTimer !== null)
|
|
546
|
+
clearTimeout(viewableTimer);
|
|
547
|
+
if (batchTimer !== null)
|
|
548
|
+
clearTimeout(batchTimer);
|
|
549
|
+
});
|
|
550
|
+
return () => {
|
|
551
|
+
const p = narrowed.value;
|
|
552
|
+
const m = metrics.value;
|
|
553
|
+
// Read the version refs so a separator/measurement bump re-renders.
|
|
554
|
+
void separatorVersion.value;
|
|
555
|
+
if (p.renderItem === undefined)
|
|
556
|
+
dlog('Vue VirtualizedList: no #item slot provided — cells render empty');
|
|
557
|
+
dlog(`Vue VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
558
|
+
`(offset=${scrollOffset.value}, viewport=${viewportLength.value}, rendered=${Math.max(0, m.last - m.first + 1)})`);
|
|
559
|
+
const children = [];
|
|
560
|
+
const stickySet = p.stickyHeaderIndices !== undefined ? new Set(p.stickyHeaderIndices) : undefined;
|
|
561
|
+
const header = resolveElement(p.listHeaderComponent);
|
|
562
|
+
if (header !== undefined) {
|
|
563
|
+
children.push(h('symbiote-view', { key: 'list-header' }, [header]));
|
|
564
|
+
}
|
|
565
|
+
let renderedStickyIndices = [];
|
|
566
|
+
if (m.count === FIRST_INDEX) {
|
|
567
|
+
const empty = resolveElement(p.listEmptyComponent);
|
|
568
|
+
if (empty !== undefined) {
|
|
569
|
+
children.push(h('symbiote-view', { key: 'list-empty' }, [empty]));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
const plan = buildListPlan({
|
|
574
|
+
count: m.count,
|
|
575
|
+
first: m.first,
|
|
576
|
+
last: m.last,
|
|
577
|
+
offsets: m.offsets,
|
|
578
|
+
lengths: m.lengths,
|
|
579
|
+
total: m.total,
|
|
580
|
+
keyFor,
|
|
581
|
+
stickyIndices: stickySet,
|
|
582
|
+
hasHeader: header !== undefined,
|
|
583
|
+
hasSeparators: p.itemSeparatorComponent !== undefined,
|
|
584
|
+
});
|
|
585
|
+
renderedStickyIndices = plan.stickyChildPositions;
|
|
586
|
+
if (plan.leadingExtent > EMPTY_OFFSET) {
|
|
587
|
+
children.push(h('symbiote-view', {
|
|
588
|
+
key: 'spacer-leading',
|
|
589
|
+
style: p.horizontal ? { width: plan.leadingExtent } : { height: plan.leadingExtent },
|
|
590
|
+
}));
|
|
591
|
+
}
|
|
592
|
+
const cellInverted = p.inverted
|
|
593
|
+
? p.horizontal
|
|
594
|
+
? INVERTED_X_STYLE
|
|
595
|
+
: INVERTED_Y_STYLE
|
|
596
|
+
: undefined;
|
|
597
|
+
for (let cellPos = 0; cellPos < plan.cells.length; cellPos += 1) {
|
|
598
|
+
const cell = plan.cells[cellPos];
|
|
599
|
+
const item = p.getItem(p.data, cell.index);
|
|
600
|
+
const content = p.renderItem?.({
|
|
601
|
+
item,
|
|
602
|
+
index: cell.index,
|
|
603
|
+
separators: makeSeparators(cell.index),
|
|
604
|
+
});
|
|
605
|
+
children.push(h('symbiote-view', {
|
|
606
|
+
key: `cell-${cell.key}`,
|
|
607
|
+
onLayout: makeCellMeasure(cell.index),
|
|
608
|
+
style: cellInverted,
|
|
609
|
+
}, [content]));
|
|
610
|
+
if (cell.index < m.last) {
|
|
611
|
+
const separator = renderSeparatorElement(p.itemSeparatorComponent, item, p.getItem(p.data, cell.index + 1), separatorOverrides.get(cell.index));
|
|
612
|
+
if (separator !== undefined) {
|
|
613
|
+
children.push(h('symbiote-view', { key: `sep-${cell.key}` }, [separator]));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (plan.trailingExtent > EMPTY_OFFSET) {
|
|
618
|
+
children.push(h('symbiote-view', {
|
|
619
|
+
key: 'spacer-trailing',
|
|
620
|
+
style: p.horizontal
|
|
621
|
+
? { width: plan.trailingExtent }
|
|
622
|
+
: { height: plan.trailingExtent },
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const footer = resolveElement(p.listFooterComponent);
|
|
627
|
+
if (footer !== undefined) {
|
|
628
|
+
children.push(h('symbiote-view', { key: 'list-footer' }, [footer]));
|
|
629
|
+
}
|
|
630
|
+
const resolvedContentContainerStyle = p.horizontal
|
|
631
|
+
? [p.contentContainerStyle, { width: m.total }]
|
|
632
|
+
: p.contentContainerStyle;
|
|
633
|
+
const resolvedStyle = p.inverted
|
|
634
|
+
? [p.style, p.horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE]
|
|
635
|
+
: p.style;
|
|
636
|
+
const scrollProps = {
|
|
637
|
+
...p.forwarded,
|
|
638
|
+
style: resolvedStyle,
|
|
639
|
+
contentContainerStyle: resolvedContentContainerStyle,
|
|
640
|
+
horizontal: p.horizontal,
|
|
641
|
+
onScroll,
|
|
642
|
+
onLayout: onViewportLayout,
|
|
643
|
+
ref: setScrollHandle,
|
|
644
|
+
};
|
|
645
|
+
// The raw scroll-lifecycle callbacks (onScrollBeginDrag/…) and scrollEventThrottle are NOT in
|
|
646
|
+
// PROP_KEYS, so they already ride through via ...p.forwarded. The keyboard props come from
|
|
647
|
+
// typed props (PROP_KEYS), so re-set them explicitly here (only when provided).
|
|
648
|
+
if (p.keyboardShouldPersistTaps !== undefined) {
|
|
649
|
+
scrollProps.keyboardShouldPersistTaps = p.keyboardShouldPersistTaps;
|
|
650
|
+
}
|
|
651
|
+
if (p.keyboardDismissMode !== undefined) {
|
|
652
|
+
scrollProps.keyboardDismissMode = p.keyboardDismissMode;
|
|
653
|
+
}
|
|
654
|
+
// A pending imperative/initial scroll rides down as contentOffset (fallback for the
|
|
655
|
+
// pre-mount window before the handle attaches).
|
|
656
|
+
if (commandedOffset.value !== undefined)
|
|
657
|
+
scrollProps.contentOffset = commandedOffset.value;
|
|
658
|
+
// Headers in the window stick; an empty list leaves the prop off entirely.
|
|
659
|
+
if (renderedStickyIndices.length > 0)
|
|
660
|
+
scrollProps.stickyHeaderIndices = renderedStickyIndices;
|
|
661
|
+
// Forward maintainVisibleContentPosition to the ScrollView so it anchors the in-window cells.
|
|
662
|
+
// minIndexForVisible is bumped by 1 when a ListHeaderComponent occupies child 0.
|
|
663
|
+
if (p.maintainVisibleContentPosition !== undefined) {
|
|
664
|
+
scrollProps.maintainVisibleContentPosition = {
|
|
665
|
+
...p.maintainVisibleContentPosition,
|
|
666
|
+
minIndexForVisible: p.maintainVisibleContentPosition.minIndexForVisible + (header !== undefined ? 1 : 0),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
// Pull-to-refresh: when a @refresh listener is present, build a RefreshControl for the
|
|
670
|
+
// ScrollView's refreshControl prop (iOS sibling / Android wrap, owned by ScrollView). The
|
|
671
|
+
// onRefresh bridge is gated on the listener, so an unlistened list builds no control.
|
|
672
|
+
if (p.onRefresh !== undefined) {
|
|
673
|
+
dlog('Vue VirtualizedList wiring RefreshControl (@refresh listened)');
|
|
674
|
+
scrollProps.refreshControl = h(RefreshControl, {
|
|
675
|
+
refreshing: p.refreshing,
|
|
676
|
+
onRefresh: p.onRefresh,
|
|
677
|
+
progressViewOffset: p.progressViewOffset,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
return h(ScrollView, scrollProps, { default: () => children });
|
|
681
|
+
};
|
|
682
|
+
}, {
|
|
683
|
+
name: 'VirtualizedList',
|
|
684
|
+
inheritAttrs: false,
|
|
685
|
+
props: PROP_KEYS,
|
|
686
|
+
emits: EMIT_KEYS,
|
|
687
|
+
});
|