@symbiote-native/vue 0.3.7 → 0.4.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.js +18 -34
- package/build/components/image-background.js +4 -14
- package/build/components/image.js +9 -20
- package/build/components/keyboard-avoiding-view.js +10 -19
- package/build/components/modal/index.d.ts +4 -4
- package/build/components/pressable.js +24 -31
- package/build/components/refresh-control.js +12 -18
- package/build/components/scroll-view/index.android.js +21 -33
- package/build/components/scroll-view/shared.js +76 -121
- package/build/components/scroll-view/sticky-header.js +104 -104
- package/build/components/section-list/index.js +12 -19
- package/build/components/switch/shared.js +17 -29
- package/build/components/text-input/index.js +29 -49
- package/build/components/touchable.js +30 -50
- package/build/components/virtualized-list/index.js +174 -273
- package/build/components/virtualized-section-list/index.js +19 -29
- package/build/components.js +1 -3
- package/build/host-instance/index.js +2 -4
- package/build/index.d.ts +2 -1
- package/build/index.js +11 -45
- package/build/modules/animated/create-animated-component.js +34 -97
- package/build/modules/animated/index.js +13 -17
- package/build/modules/app-registry/index.js +7 -11
- package/build/modules/status-bar.js +8 -15
- package/build/renderer/index.js +11 -6
- package/build/runtime-helpers/index.js +16 -26
- package/metro-css-parser.cjs +9 -14
- package/metro-vue-transformer.cjs +112 -135
- package/package.json +4 -4
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// VirtualizedList, the Vue lifecycle half. The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Vue supplies only the reactivity:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
1
|
+
// VirtualizedList, the Vue lifecycle half. The orchestration — window recompute, batch throttle,
|
|
2
|
+
// viewability, edge-reached, initial-scroll, MVCP, the imperative scrolls — is the framework-agnostic
|
|
3
|
+
// `reduceList` state machine in @symbiote-native/components (state/virtualized-list-reducer), shared verbatim
|
|
4
|
+
// with React and Angular. Here Vue supplies only the reactivity: it turns native events into ACTIONS,
|
|
5
|
+
// holds ONE plain state cell (listState), runs the returned EFFECTS with Vue primitives (a native
|
|
6
|
+
// scrollTo, an emit, a setTimeout, a version bump), and builds the cell/spacer VNodes off the plan.
|
|
7
|
+
// A single `metrics` computed derives the window once per render (refresh-metrics); a single
|
|
8
|
+
// post-flush watcher on the windowing signature runs the after-commit `commit` pass. This is the Vue
|
|
9
|
+
// twin of the React adapter's one-state-cell + dispatch + runEffects. It drives the Vue ScrollView,
|
|
9
10
|
// exactly as the React list drives the React ScrollView.
|
|
10
11
|
//
|
|
11
12
|
// Typed-emits generic component (mirrors FlatList): a GENERIC setup function
|
|
@@ -23,9 +24,10 @@
|
|
|
23
24
|
//
|
|
24
25
|
// Reactivity gotcha: the ScrollView's exposed handle is held in a shallowRef so the engine
|
|
25
26
|
// node it closes over is reached by identity; a deep ref would proxy it and the imperative
|
|
26
|
-
// scroll commands would silently no-op.
|
|
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.
|
|
27
29
|
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,
|
|
30
|
+
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, buildListPlan, buildViewabilityPairs, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readScrollOffset, reduceList, resolveItemKey, } from '@symbiote-native/components';
|
|
29
31
|
import { dlog, } from '@symbiote-native/engine';
|
|
30
32
|
import { ScrollView } from '../scroll-view';
|
|
31
33
|
import { RefreshControl } from '../refresh-control.js';
|
|
@@ -120,17 +122,13 @@ function isScrollViewHandle(value) {
|
|
|
120
122
|
return isRecord(value) && typeof value.scrollTo === 'function';
|
|
121
123
|
}
|
|
122
124
|
export const VirtualizedList = defineComponent((props, { attrs, expose, emit, slots, }) => {
|
|
123
|
-
//
|
|
124
|
-
// re-
|
|
125
|
-
const
|
|
126
|
-
const
|
|
125
|
+
// Bumped when a transition changes render-relevant state, so the `metrics` computed and the
|
|
126
|
+
// render fn re-run (listState is plain, non-reactive, so mutating it triggers nothing itself).
|
|
127
|
+
const version = ref(EMPTY_OFFSET);
|
|
128
|
+
const separatorVersion = ref(EMPTY_OFFSET);
|
|
127
129
|
// The offset we are imperatively driving native to. A fresh object identity each push so the
|
|
128
130
|
// commit path re-applies it even when the numeric value repeats. undefined = none pending.
|
|
129
131
|
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
132
|
// shallowRef, NOT ref: the ScrollView handle closes over the engine scroll node, reached by
|
|
135
133
|
// identity through the engine's WeakMap mirror. A deep ref would proxy the object and every
|
|
136
134
|
// imperative scroll (scrollToOffset/Index/…) would miss the node and silently no-op.
|
|
@@ -147,20 +145,12 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
147
145
|
const vnodeProps = instance?.vnode.props;
|
|
148
146
|
return vnodeProps != null && typeof vnodeProps[onName] === 'function';
|
|
149
147
|
};
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let committedWindow = { first: FIRST_INDEX, last: -1 };
|
|
155
|
-
let sentEndForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
156
|
-
let sentStartForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
157
|
-
let lastViewable = new Map();
|
|
148
|
+
// The one folded state cell (the Vue twin of React's stateRef); plus the per-gap separator
|
|
149
|
+
// overrides + the adapter-owned debounce/fill timers.
|
|
150
|
+
const listState = createInitialListState();
|
|
151
|
+
const separatorOverrides = new Map();
|
|
158
152
|
let viewableTimer = null;
|
|
159
153
|
let batchTimer = null;
|
|
160
|
-
let hasInteracted = false;
|
|
161
|
-
const separatorOverrides = new Map();
|
|
162
|
-
let appliedInitialScroll = false;
|
|
163
|
-
let firstVisibleKey = null;
|
|
164
154
|
const narrowed = computed(() => {
|
|
165
155
|
// extraData has no field of its own; reading it tracks it so a change forces a re-render
|
|
166
156
|
// (RN's extraData contract).
|
|
@@ -217,47 +207,34 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
217
207
|
forwarded: forwardAttrs(folded),
|
|
218
208
|
};
|
|
219
209
|
});
|
|
220
|
-
|
|
210
|
+
// The reducer inputs, folded off `narrowed`. Rebuilt per call (cheap); the viewability pairs are
|
|
211
|
+
// rebuilt here so the reduce and the effect fire against the same set.
|
|
212
|
+
const buildInputs = () => {
|
|
221
213
|
const p = narrowed.value;
|
|
222
|
-
const item = p.getItem(p.data, index);
|
|
223
|
-
return p.keyExtractor ? p.keyExtractor(item, index) : String(index);
|
|
224
|
-
};
|
|
225
|
-
// The windowing math, run through the shared @symbiote-native/components functions. It mutates the
|
|
226
|
-
// non-reactive committedWindow as it throttles (the controlled side effect React runs during
|
|
227
|
-
// render): committedWindow is plain state, so the write triggers no reactivity loop.
|
|
228
|
-
const metrics = computed(() => {
|
|
229
|
-
const p = narrowed.value;
|
|
230
|
-
void measureVersion.value;
|
|
231
|
-
const count = p.getItemCount(p.data);
|
|
232
|
-
const gil = p.getItemLayout;
|
|
233
|
-
const data = p.data;
|
|
234
|
-
const fixedLayout = gil !== undefined
|
|
235
|
-
? (index) => {
|
|
236
|
-
const layout = gil(data, index);
|
|
237
|
-
return { length: layout.length, offset: layout.offset };
|
|
238
|
-
}
|
|
239
|
-
: undefined;
|
|
240
|
-
const averageLength = fixedLayout !== undefined
|
|
241
|
-
? count > FIRST_INDEX
|
|
242
|
-
? fixedLayout(FIRST_INDEX).length
|
|
243
|
-
: EMPTY_OFFSET
|
|
244
|
-
: averageMeasuredLength(measured);
|
|
245
|
-
const { offsets, lengths, total } = buildOffsets(count, measured, fixedLayout, averageLength);
|
|
246
|
-
const target = computeWindow(count, offsets, lengths, scrollOffset.value, viewportLength.value, p.windowSize, p.initialNumToRender);
|
|
247
|
-
const throttled = throttleWindow(target, committedWindow, p.maxToRenderPerBatch);
|
|
248
|
-
committedWindow = throttled;
|
|
249
214
|
return {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
215
|
+
data: p.data,
|
|
216
|
+
getItem: p.getItem,
|
|
217
|
+
getItemCount: p.getItemCount,
|
|
218
|
+
keyExtractor: p.keyExtractor,
|
|
219
|
+
getItemLayout: p.getItemLayout,
|
|
220
|
+
horizontal: p.horizontal,
|
|
221
|
+
windowSize: p.windowSize,
|
|
222
|
+
initialNumToRender: p.initialNumToRender,
|
|
223
|
+
maxToRenderPerBatch: p.maxToRenderPerBatch,
|
|
224
|
+
updateCellsBatchingPeriod: p.updateCellsBatchingPeriod,
|
|
225
|
+
onEndReachedThreshold: p.onEndReachedThreshold,
|
|
226
|
+
onStartReachedThreshold: p.onStartReachedThreshold,
|
|
227
|
+
onEndReachedActive: p.onEndReached !== undefined,
|
|
228
|
+
onStartReachedActive: p.onStartReached !== undefined,
|
|
229
|
+
viewabilityPairs: buildViewabilityPairs(p.onViewableItemsChanged, p.viewabilityConfig, p.viewabilityConfigCallbackPairs),
|
|
230
|
+
maintainVisibleContentPosition: p.maintainVisibleContentPosition,
|
|
231
|
+
initialScrollIndex: p.initialScrollIndex,
|
|
259
232
|
};
|
|
260
|
-
}
|
|
233
|
+
};
|
|
234
|
+
const keyFor = (index) => {
|
|
235
|
+
const p = narrowed.value;
|
|
236
|
+
return resolveItemKey(p.getItem(p.data, index), index, p.keyExtractor);
|
|
237
|
+
};
|
|
261
238
|
const scrollToPixel = (offset, animated) => {
|
|
262
239
|
const p = narrowed.value;
|
|
263
240
|
const clamped = Math.max(EMPTY_OFFSET, offset);
|
|
@@ -273,21 +250,92 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
273
250
|
dlog(`Vue VirtualizedList scrollTo offset=${clamped} pending-ref`);
|
|
274
251
|
commandedOffset.value = targetOffset;
|
|
275
252
|
};
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
253
|
+
// Execute the reducer's effects with Vue primitives. `inputs` is threaded through so a
|
|
254
|
+
// fire-viewable fires against the same pairs the reduce used.
|
|
255
|
+
const runEffects = (effects, inputs) => {
|
|
256
|
+
const p = narrowed.value;
|
|
257
|
+
for (const effect of effects) {
|
|
258
|
+
switch (effect.kind) {
|
|
259
|
+
case 'scroll-to':
|
|
260
|
+
scrollToPixel(effect.offset, effect.animated);
|
|
261
|
+
break;
|
|
262
|
+
case 'fire-end-reached':
|
|
263
|
+
p.onEndReached?.({ distanceFromEnd: effect.distanceFromEnd });
|
|
264
|
+
break;
|
|
265
|
+
case 'fire-start-reached':
|
|
266
|
+
p.onStartReached?.({ distanceFromStart: effect.distanceFromStart });
|
|
267
|
+
break;
|
|
268
|
+
case 'fire-scroll-to-index-failed':
|
|
269
|
+
p.onScrollToIndexFailed?.({
|
|
270
|
+
index: effect.index,
|
|
271
|
+
highestMeasuredFrameIndex: effect.highestMeasuredFrameIndex,
|
|
272
|
+
averageItemLength: effect.averageItemLength,
|
|
273
|
+
});
|
|
274
|
+
break;
|
|
275
|
+
case 'schedule-refill': {
|
|
276
|
+
if (batchTimer !== null)
|
|
277
|
+
clearTimeout(batchTimer);
|
|
278
|
+
batchTimer = setTimeout(() => {
|
|
279
|
+
batchTimer = null;
|
|
280
|
+
dispatch({ kind: 'batch-tick' });
|
|
281
|
+
}, effect.delay);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case 'fire-viewable': {
|
|
285
|
+
const pairs = inputs.viewabilityPairs;
|
|
286
|
+
const info = effect.info;
|
|
287
|
+
const map = effect.map;
|
|
288
|
+
const fire = () => {
|
|
289
|
+
for (const pair of pairs)
|
|
290
|
+
pair.onViewableItemsChanged(info);
|
|
291
|
+
dispatch({ kind: 'viewable-fired', map });
|
|
292
|
+
};
|
|
293
|
+
if (viewableTimer !== null) {
|
|
294
|
+
clearTimeout(viewableTimer);
|
|
295
|
+
viewableTimer = null;
|
|
296
|
+
}
|
|
297
|
+
if (effect.delay > EMPTY_OFFSET) {
|
|
298
|
+
viewableTimer = setTimeout(() => {
|
|
299
|
+
viewableTimer = null;
|
|
300
|
+
fire();
|
|
301
|
+
}, effect.delay);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
fire();
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const dispatch = (action) => {
|
|
312
|
+
const inputs = buildInputs();
|
|
313
|
+
const result = reduceList(listState, action, inputs);
|
|
314
|
+
runEffects(result.effects, inputs);
|
|
315
|
+
if (result.changed)
|
|
316
|
+
version.value += 1;
|
|
279
317
|
};
|
|
318
|
+
// The single derive-per-render: the window is recomputed once here (refresh-metrics) and cached
|
|
319
|
+
// by the computed until `version` or the narrowed props change, so the render fn and the commit
|
|
320
|
+
// signature both read it without re-deriving (which would double-advance the throttle).
|
|
321
|
+
const metrics = computed(() => {
|
|
322
|
+
void version.value;
|
|
323
|
+
reduceList(listState, { kind: 'refresh-metrics' }, buildInputs());
|
|
324
|
+
return listState.metrics;
|
|
325
|
+
});
|
|
326
|
+
const commitSignature = computed(() => {
|
|
327
|
+
void metrics.value;
|
|
328
|
+
return listEffectSignature(listState);
|
|
329
|
+
});
|
|
280
330
|
const onScroll = (event) => {
|
|
281
331
|
const p = narrowed.value;
|
|
282
332
|
const offset = readScrollOffset(event, p.horizontal);
|
|
283
333
|
if (offset === undefined)
|
|
284
334
|
return;
|
|
285
335
|
dlog(`Vue VirtualizedList onScroll offset=${offset}`);
|
|
286
|
-
// First scroll is the interaction that ungates waitForInteraction configs.
|
|
287
|
-
hasInteracted = true;
|
|
288
|
-
scrollOffset.value = offset;
|
|
289
336
|
// A real native scroll supersedes any pending commanded offset.
|
|
290
337
|
commandedOffset.value = undefined;
|
|
338
|
+
dispatch({ kind: 'scroll', offset });
|
|
291
339
|
// Compose, don't clobber: internal windowing ran first, now the user's onScroll.
|
|
292
340
|
if (p.userOnScroll !== undefined)
|
|
293
341
|
p.userOnScroll(event);
|
|
@@ -297,24 +345,17 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
297
345
|
if (length === undefined)
|
|
298
346
|
return;
|
|
299
347
|
dlog(`Vue VirtualizedList onLayout viewport=${length}`);
|
|
300
|
-
|
|
348
|
+
dispatch({ kind: 'layout', length });
|
|
301
349
|
};
|
|
302
350
|
const makeCellMeasure = (index) => (event) => {
|
|
303
|
-
const
|
|
304
|
-
if (p.getItemLayout !== undefined)
|
|
305
|
-
return;
|
|
306
|
-
const length = readLayoutLength(event, p.horizontal);
|
|
351
|
+
const length = readLayoutLength(event, narrowed.value.horizontal);
|
|
307
352
|
if (length === undefined)
|
|
308
353
|
return;
|
|
309
|
-
if (measured.get(index) === length)
|
|
310
|
-
return;
|
|
311
|
-
measured.set(index, length);
|
|
312
354
|
dlog(`Vue VirtualizedList cell ${index} measured length=${length}`);
|
|
313
|
-
|
|
355
|
+
dispatch({ kind: 'measure', index, length });
|
|
314
356
|
};
|
|
315
357
|
const mergeSeparator = (gapIndex, patch) => {
|
|
316
|
-
|
|
317
|
-
if (gapIndex < FIRST_INDEX || gapIndex > count - 2)
|
|
358
|
+
if (!isSeparatorGapInRange(gapIndex, listState.metrics.count))
|
|
318
359
|
return;
|
|
319
360
|
separatorOverrides.set(gapIndex, { ...separatorOverrides.get(gapIndex), ...patch });
|
|
320
361
|
separatorVersion.value += 1;
|
|
@@ -337,38 +378,31 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
337
378
|
// ---- imperative handle (the shared IVirtualizedListHandle surface) ------
|
|
338
379
|
const handle = {
|
|
339
380
|
scrollToOffset: (params) => {
|
|
340
|
-
|
|
381
|
+
dispatch({
|
|
382
|
+
kind: 'scroll-to-offset',
|
|
383
|
+
offset: params.offset,
|
|
384
|
+
animated: params.animated ?? true,
|
|
385
|
+
});
|
|
341
386
|
},
|
|
342
387
|
scrollToIndex: (params) => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
p.onScrollToIndexFailed?.({
|
|
351
|
-
index: params.index,
|
|
352
|
-
highestMeasuredFrameIndex: highestMeasuredIndex(measured),
|
|
353
|
-
averageItemLength: m.averageLength,
|
|
354
|
-
});
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
scrollToPixel(offsetForIndexLocal(params.index, params.viewPosition ?? FIRST_INDEX, params.viewOffset ?? EMPTY_OFFSET), params.animated ?? true);
|
|
388
|
+
dispatch({
|
|
389
|
+
kind: 'scroll-to-index',
|
|
390
|
+
index: params.index,
|
|
391
|
+
animated: params.animated ?? true,
|
|
392
|
+
viewPosition: params.viewPosition ?? FIRST_INDEX,
|
|
393
|
+
viewOffset: params.viewOffset ?? EMPTY_OFFSET,
|
|
394
|
+
});
|
|
358
395
|
},
|
|
359
396
|
scrollToItem: (params) => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
dlog('Vue VirtualizedList scrollToItem: item not found');
|
|
397
|
+
dispatch({
|
|
398
|
+
kind: 'scroll-to-item',
|
|
399
|
+
item: params.item,
|
|
400
|
+
animated: params.animated ?? true,
|
|
401
|
+
viewPosition: params.viewPosition ?? FIRST_INDEX,
|
|
402
|
+
});
|
|
369
403
|
},
|
|
370
404
|
scrollToEnd: (params) => {
|
|
371
|
-
|
|
405
|
+
dispatch({ kind: 'scroll-to-end', animated: params?.animated ?? true });
|
|
372
406
|
},
|
|
373
407
|
flashScrollIndicators: () => {
|
|
374
408
|
scrollHandle.value?.flashScrollIndicators();
|
|
@@ -378,167 +412,17 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
378
412
|
getScrollResponder: () => scrollHandle.value,
|
|
379
413
|
getScrollNode: () => scrollHandle.value?.getScrollNode() ?? null,
|
|
380
414
|
recordInteraction: () => {
|
|
381
|
-
|
|
415
|
+
dispatch({ kind: 'record-interaction' });
|
|
382
416
|
},
|
|
383
417
|
};
|
|
384
418
|
expose(handle);
|
|
385
|
-
// ---- after-commit
|
|
386
|
-
//
|
|
387
|
-
// the
|
|
388
|
-
watch(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
if (m.first <= m.target.first && m.last >= m.target.last)
|
|
394
|
-
return;
|
|
395
|
-
batchTimer = setTimeout(() => {
|
|
396
|
-
batchTimer = null;
|
|
397
|
-
measureVersion.value += 1;
|
|
398
|
-
}, narrowed.value.updateCellsBatchingPeriod);
|
|
399
|
-
}, { flush: 'post' });
|
|
400
|
-
// onEndReached: fire only when the actual last cell is rendered AND within threshold; dedup by
|
|
401
|
-
// content length; re-arm on scroll away from the end (RN _maybeCallOnEdgeReached).
|
|
402
|
-
watch(() => [narrowed.value, scrollOffset.value, viewportLength.value, metrics.value], () => {
|
|
403
|
-
const p = narrowed.value;
|
|
404
|
-
if (p.onEndReached === undefined || viewportLength.value <= EMPTY_OFFSET)
|
|
405
|
-
return;
|
|
406
|
-
const m = metrics.value;
|
|
407
|
-
const { distanceFromEnd, withinThreshold } = computeEndReached(m.total, scrollOffset.value, viewportLength.value, p.onEndReachedThreshold);
|
|
408
|
-
const lastCellRendered = m.last === m.count - 1;
|
|
409
|
-
if (withinThreshold && lastCellRendered && sentEndForContentLength !== m.total) {
|
|
410
|
-
sentEndForContentLength = m.total;
|
|
411
|
-
dlog(`Vue VirtualizedList onEndReached distanceFromEnd=${distanceFromEnd} ` +
|
|
412
|
-
`(last=${m.last} of ${m.count}, contentLength=${m.total})`);
|
|
413
|
-
p.onEndReached({ distanceFromEnd });
|
|
414
|
-
}
|
|
415
|
-
if (!withinThreshold)
|
|
416
|
-
sentEndForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
417
|
-
}, { flush: 'post' });
|
|
418
|
-
// onStartReached: the top-edge twin of onEndReached.
|
|
419
|
-
watch(() => [narrowed.value, scrollOffset.value, viewportLength.value, metrics.value], () => {
|
|
420
|
-
const p = narrowed.value;
|
|
421
|
-
if (p.onStartReached === undefined || viewportLength.value <= EMPTY_OFFSET)
|
|
422
|
-
return;
|
|
423
|
-
const m = metrics.value;
|
|
424
|
-
const { distanceFromStart, withinThreshold } = computeStartReached(scrollOffset.value, viewportLength.value, p.onStartReachedThreshold);
|
|
425
|
-
const firstCellRendered = m.first === FIRST_INDEX;
|
|
426
|
-
if (withinThreshold && firstCellRendered && sentStartForContentLength !== m.total) {
|
|
427
|
-
sentStartForContentLength = m.total;
|
|
428
|
-
dlog(`Vue VirtualizedList onStartReached distanceFromStart=${distanceFromStart} ` +
|
|
429
|
-
`(first=${m.first}, contentLength=${m.total})`);
|
|
430
|
-
p.onStartReached({ distanceFromStart });
|
|
431
|
-
}
|
|
432
|
-
if (!withinThreshold)
|
|
433
|
-
sentStartForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
434
|
-
}, { flush: 'post' });
|
|
435
|
-
// Viewability: recompute which rendered cells clear the threshold and, if the viewable set
|
|
436
|
-
// changed, fire onViewableItemsChanged + every config/callback pair, honoring minimumViewTime.
|
|
437
|
-
watch(() => [narrowed.value, scrollOffset.value, viewportLength.value, metrics.value], () => {
|
|
438
|
-
const p = narrowed.value;
|
|
439
|
-
const m = metrics.value;
|
|
440
|
-
const pairs = buildViewabilityPairs(p.onViewableItemsChanged, p.viewabilityConfig, p.viewabilityConfigCallbackPairs);
|
|
441
|
-
if (pairs.length === EMPTY_OFFSET || viewportLength.value <= EMPTY_OFFSET)
|
|
442
|
-
return;
|
|
443
|
-
if (m.count === FIRST_INDEX)
|
|
444
|
-
return;
|
|
445
|
-
const { tokens, map } = computeViewableSet({
|
|
446
|
-
first: m.first,
|
|
447
|
-
last: m.last,
|
|
448
|
-
count: m.count,
|
|
449
|
-
offsets: m.offsets,
|
|
450
|
-
lengths: m.lengths,
|
|
451
|
-
scrollOffset: scrollOffset.value,
|
|
452
|
-
viewportLength: viewportLength.value,
|
|
453
|
-
data: p.data,
|
|
454
|
-
getItem: p.getItem,
|
|
455
|
-
keyExtractor: p.keyExtractor,
|
|
456
|
-
pairs,
|
|
457
|
-
hasInteracted,
|
|
458
|
-
});
|
|
459
|
-
const diff = diffViewable(lastViewable, map, tokens);
|
|
460
|
-
if (!diff.hasChanged)
|
|
461
|
-
return;
|
|
462
|
-
const commitAndFire = () => {
|
|
463
|
-
lastViewable = map;
|
|
464
|
-
dlog(`Vue VirtualizedList viewable=${tokens.length} changed=${diff.changed.length} ` +
|
|
465
|
-
`(window [${m.first}, ${m.last}])`);
|
|
466
|
-
const info = {
|
|
467
|
-
viewableItems: tokens,
|
|
468
|
-
changed: diff.changed,
|
|
469
|
-
};
|
|
470
|
-
for (const pair of pairs)
|
|
471
|
-
pair.onViewableItemsChanged(info);
|
|
472
|
-
};
|
|
473
|
-
const minimumViewTime = maxMinimumViewTime(pairs);
|
|
474
|
-
if (viewableTimer !== null) {
|
|
475
|
-
clearTimeout(viewableTimer);
|
|
476
|
-
viewableTimer = null;
|
|
477
|
-
}
|
|
478
|
-
if (minimumViewTime > EMPTY_OFFSET) {
|
|
479
|
-
dlog(`Vue VirtualizedList viewability debounce ${minimumViewTime}ms (window [${m.first}, ${m.last}])`);
|
|
480
|
-
viewableTimer = setTimeout(() => {
|
|
481
|
-
viewableTimer = null;
|
|
482
|
-
commitAndFire();
|
|
483
|
-
}, minimumViewTime);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
commitAndFire();
|
|
487
|
-
}, { flush: 'post' });
|
|
488
|
-
// initialScrollIndex: once the first viewport is known, jump to that index a single time.
|
|
489
|
-
watch(() => [narrowed.value, viewportLength.value, metrics.value], () => {
|
|
490
|
-
const p = narrowed.value;
|
|
491
|
-
if (p.initialScrollIndex === undefined || appliedInitialScroll)
|
|
492
|
-
return;
|
|
493
|
-
if (viewportLength.value <= EMPTY_OFFSET || metrics.value.count === FIRST_INDEX)
|
|
494
|
-
return;
|
|
495
|
-
appliedInitialScroll = true;
|
|
496
|
-
// The initial jump is instant (RN does not animate initialScrollIndex).
|
|
497
|
-
scrollToPixel(offsetForIndexLocal(p.initialScrollIndex, FIRST_INDEX, EMPTY_OFFSET), false);
|
|
498
|
-
}, { flush: 'post' });
|
|
499
|
-
// maintainVisibleContentPosition JS anchor adjustment: track the key of the item at
|
|
500
|
-
// minIndexForVisible; when a prepend moves it down, add the inserted extent in the leading
|
|
501
|
-
// SPACER (the off-window items native MVCP cannot see) to scrollOffset so the anchored item
|
|
502
|
-
// does not jump (RN getDerivedStateFromProps). Runs post-flush so the correction lands fast.
|
|
503
|
-
watch(() => [narrowed.value, metrics.value, scrollOffset.value], () => {
|
|
504
|
-
const p = narrowed.value;
|
|
505
|
-
const m = metrics.value;
|
|
506
|
-
if (p.maintainVisibleContentPosition === undefined || m.count === FIRST_INDEX) {
|
|
507
|
-
firstVisibleKey = null;
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
const minIndexForVisible = p.maintainVisibleContentPosition.minIndexForVisible;
|
|
511
|
-
const newFirstVisibleKey = m.count > minIndexForVisible ? keyFor(minIndexForVisible) : null;
|
|
512
|
-
const prevKey = firstVisibleKey;
|
|
513
|
-
if (prevKey !== null && newFirstVisibleKey !== null && prevKey !== newFirstVisibleKey) {
|
|
514
|
-
let anchorIndex = -1;
|
|
515
|
-
for (let index = minIndexForVisible; index < m.count; index += 1) {
|
|
516
|
-
if (keyFor(index) === prevKey) {
|
|
517
|
-
anchorIndex = index;
|
|
518
|
-
break;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
if (anchorIndex > minIndexForVisible) {
|
|
522
|
-
const spacerEnd = Math.min(anchorIndex, committedWindow.first);
|
|
523
|
-
const insertedExtent = spacerEnd > minIndexForVisible
|
|
524
|
-
? m.offsets[spacerEnd] - m.offsets[minIndexForVisible]
|
|
525
|
-
: EMPTY_OFFSET;
|
|
526
|
-
if (insertedExtent > EMPTY_OFFSET) {
|
|
527
|
-
const autoThreshold = p.maintainVisibleContentPosition.autoscrollToTopThreshold;
|
|
528
|
-
const anchoredNearTop = autoThreshold !== undefined && scrollOffset.value <= autoThreshold;
|
|
529
|
-
if (anchoredNearTop) {
|
|
530
|
-
dlog(`Vue VirtualizedList MVCP autoscroll-to-top (offset=${scrollOffset.value} <= ${autoThreshold})`);
|
|
531
|
-
scrollToPixel(EMPTY_OFFSET, true);
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
dlog(`Vue VirtualizedList MVCP adjust +${insertedExtent}px ` +
|
|
535
|
-
`(anchor "${prevKey}" moved ${minIndexForVisible}->${anchorIndex})`);
|
|
536
|
-
scrollToPixel(scrollOffset.value + insertedExtent, false);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
firstVisibleKey = newFirstVisibleKey;
|
|
419
|
+
// ---- after-commit pass (one post-flush watcher, the Vue twin of the layout effect) ----
|
|
420
|
+
// Runs the deferred effects (batch fill, edge-reached, viewability, initial-scroll, MVCP)
|
|
421
|
+
// whenever the windowing signature changes — the same dedup key every adapter shares.
|
|
422
|
+
watch(commitSignature, () => {
|
|
423
|
+
const inputs = buildInputs();
|
|
424
|
+
const result = reduceList(listState, { kind: 'commit' }, inputs);
|
|
425
|
+
runEffects(result.effects, inputs);
|
|
542
426
|
}, { flush: 'post' });
|
|
543
427
|
onBeforeUnmount(() => {
|
|
544
428
|
if (viewableTimer !== null)
|
|
@@ -554,7 +438,7 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
554
438
|
if (p.renderItem === undefined)
|
|
555
439
|
dlog('Vue VirtualizedList: no #item slot provided — cells render empty');
|
|
556
440
|
dlog(`Vue VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
557
|
-
`(offset=${scrollOffset
|
|
441
|
+
`(offset=${listState.scrollOffset}, viewport=${listState.viewportLength}, rendered=${Math.max(0, m.last - m.first + 1)})`);
|
|
558
442
|
const children = [];
|
|
559
443
|
const stickySet = p.stickyHeaderIndices !== undefined ? new Set(p.stickyHeaderIndices) : undefined;
|
|
560
444
|
const header = resolveElement(p.listHeaderComponent);
|
|
@@ -593,8 +477,9 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
593
477
|
? INVERTED_X_STYLE
|
|
594
478
|
: INVERTED_Y_STYLE
|
|
595
479
|
: undefined;
|
|
596
|
-
|
|
597
|
-
|
|
480
|
+
// Wraps one cell identically whether it's the force-mounted sticky cell ahead of the
|
|
481
|
+
// window or an in-window cell from the loop below — same measuring View, same key shape.
|
|
482
|
+
const pushCell = (cell) => {
|
|
598
483
|
const item = p.getItem(p.data, cell.index);
|
|
599
484
|
const content = p.renderItem?.({
|
|
600
485
|
item,
|
|
@@ -606,8 +491,24 @@ export const VirtualizedList = defineComponent((props, { attrs, expose, emit, sl
|
|
|
606
491
|
onLayout: makeCellMeasure(cell.index),
|
|
607
492
|
style: cellInverted,
|
|
608
493
|
}, [content]));
|
|
494
|
+
};
|
|
495
|
+
// The nearest sticky section header below the window (RN _ensureClosestStickyHeader):
|
|
496
|
+
// force-mounted so it never gets destroyed/recreated while pinned off-window. Rendered
|
|
497
|
+
// ahead of the main window loop, with its own gap spacer to the window's first cell.
|
|
498
|
+
if (plan.forcedStickyCell !== undefined) {
|
|
499
|
+
pushCell(plan.forcedStickyCell);
|
|
500
|
+
if (plan.gapExtent > EMPTY_OFFSET) {
|
|
501
|
+
children.push(h('symbiote-view', {
|
|
502
|
+
key: 'spacer-gap',
|
|
503
|
+
style: p.horizontal ? { width: plan.gapExtent } : { height: plan.gapExtent },
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
for (let cellPos = 0; cellPos < plan.cells.length; cellPos += 1) {
|
|
508
|
+
const cell = plan.cells[cellPos];
|
|
509
|
+
pushCell(cell);
|
|
609
510
|
if (cell.index < m.last) {
|
|
610
|
-
const separator = renderSeparatorElement(p.itemSeparatorComponent,
|
|
511
|
+
const separator = renderSeparatorElement(p.itemSeparatorComponent, p.getItem(p.data, cell.index), p.getItem(p.data, cell.index + 1), separatorOverrides.get(cell.index));
|
|
611
512
|
if (separator !== undefined) {
|
|
612
513
|
children.push(h('symbiote-view', { key: `sep-${cell.key}` }, [separator]));
|
|
613
514
|
}
|
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
// VirtualizedSectionList, the Vue wrapper that flattens sections into one virtualized stream
|
|
2
|
-
// over VirtualizedList
|
|
3
|
-
// (RN counts 2 per section)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// The Vue twin of the React adapter's VirtualizedSectionList.
|
|
2
|
+
// over VirtualizedList: each section contributes a header row, its item rows, then a footer row
|
|
3
|
+
// (RN counts 2 per section), and the flattened tagged sequence is windowed by the same
|
|
4
|
+
// machinery as one list. Flattening, entry keying, separator-item unwrap, and scrollToLocation
|
|
5
|
+
// mapping are shared from @symbiote-native/components; this file wires Vue lifecycle
|
|
6
|
+
// (typed-prop inputs + handle re-expose + per-entry render dispatch).
|
|
8
7
|
//
|
|
9
|
-
// Typed-emits generic component (mirrors FlatList
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// React's section list exposes) stay GATED on listener presence: each emit bridge is wired ONLY
|
|
16
|
-
// when the consumer listens (read off the instance vnode props), so the inner VirtualizedList keeps
|
|
17
|
-
// building RefreshControl / computing edge-reached strictly on demand.
|
|
8
|
+
// Typed-emits generic component (mirrors FlatList/VirtualizedList): a GENERIC setup function so
|
|
9
|
+
// section inputs infer ItemT at the call site. Those inputs are read from typed `props`
|
|
10
|
+
// (declared in the runtime `props` array); the VirtualizedList passthrough tail rides through
|
|
11
|
+
// $attrs onto the inner list. The three adapter-synthesized events (endReached/startReached/
|
|
12
|
+
// refresh) stay GATED on listener presence so the inner VirtualizedList keeps building
|
|
13
|
+
// RefreshControl / computing edge-reached strictly on demand.
|
|
18
14
|
import { defineComponent, getCurrentInstance, h, shallowRef, } from '@vue/runtime-core';
|
|
19
|
-
import { flattenSections, scrollLocationToFlatIndex, sectionEntryKey, unwrapEntryItem, } from '@symbiote-native/components';
|
|
15
|
+
import { flattenSections, resolveStickySectionHeaders, scrollLocationToFlatIndex, sectionEntryKey, unwrapEntryItem, } from '@symbiote-native/components';
|
|
20
16
|
import { Platform, dlog, } from '@symbiote-native/engine';
|
|
21
17
|
import { VirtualizedList } from '../virtualized-list/index.js';
|
|
22
18
|
import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
|
|
23
|
-
// VirtualizedList
|
|
24
|
-
//
|
|
19
|
+
// VirtualizedList's generic construct signature can't be resolved by h()'s overloads, so drive
|
|
20
|
+
// it through a loose functional-component handle instead.
|
|
25
21
|
const VirtualizedListHost = VirtualizedList;
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// (keyof can't derive it: the index signature widens keyof to `string`). The three emit events are
|
|
29
|
-
// deliberately absent (declared as emits below).
|
|
22
|
+
// Listed for the runtime `props` declaration (keyof can't derive it: the index signature widens
|
|
23
|
+
// keyof to `string`). The three emit events are deliberately absent (declared as emits below).
|
|
30
24
|
const PROP_KEYS = ['sections', 'keyExtractor', 'stickySectionHeadersEnabled'];
|
|
31
25
|
const EMIT_KEYS = ['endReached', 'startReached', 'refresh'];
|
|
32
26
|
function isRecord(value) {
|
|
@@ -68,10 +62,8 @@ export const VirtualizedSectionList = defineComponent((props, { attrs, expose, e
|
|
|
68
62
|
// headerIndices change each render (sections may change); the delegate reads them lazily.
|
|
69
63
|
let headerIndices = [];
|
|
70
64
|
expose(buildSectionDelegate(() => inner.value, () => headerIndices));
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// FlatList/VirtualizedList, so an emit bridge is wired ONLY when the consumer actually listens
|
|
74
|
-
// (preserving the inner list's RefreshControl / edge-reached gating).
|
|
65
|
+
// Emits are stripped from $attrs by Vue; detect listener presence off the instance's own
|
|
66
|
+
// vnode props instead, so an emit bridge is wired ONLY when the consumer actually listens.
|
|
75
67
|
const instance = getCurrentInstance();
|
|
76
68
|
const listens = (onName) => {
|
|
77
69
|
const vnodeProps = instance?.vnode.props;
|
|
@@ -88,9 +80,7 @@ export const VirtualizedSectionList = defineComponent((props, { attrs, expose, e
|
|
|
88
80
|
const { entries, headerIndices: indices } = flattenSections(sections, slots.sectionSeparator !== undefined);
|
|
89
81
|
headerIndices = indices;
|
|
90
82
|
// RN sticks section headers by default only on iOS; Android does not unless asked.
|
|
91
|
-
const
|
|
92
|
-
const stickyEnabled = props.stickySectionHeadersEnabled ?? stickyDefault;
|
|
93
|
-
const stickyHeaderIndices = stickyEnabled ? indices : undefined;
|
|
83
|
+
const stickyHeaderIndices = resolveStickySectionHeaders(props.stickySectionHeadersEnabled, indices, Platform.OS);
|
|
94
84
|
dlog(`Vue VirtualizedSectionList: ${sections.length} sections flattened to ${entries.length} entries`);
|
|
95
85
|
// The inner list's #item slot: dispatch each flattened entry to the matching consumer slot,
|
|
96
86
|
// returning its VNode[] (empty when the consumer left that slot off).
|