@pdanpdan/virtual-scroll 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +479 -499
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/composables/useVirtualScroll.ts +16 -3
- package/src/composables/useVirtualScrollSizes.ts +126 -115
- package/src/utils/virtual-scroll-logic.ts +11 -2
package/package.json
CHANGED
|
@@ -758,7 +758,10 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
758
758
|
)
|
|
759
759
|
: { x: 0, y: 0 };
|
|
760
760
|
|
|
761
|
-
const lastItemsMap = new Map
|
|
761
|
+
const lastItemsMap = new Map<number, RenderedItem<T>>();
|
|
762
|
+
for (const item of lastRenderedItems) {
|
|
763
|
+
lastItemsMap.set(item.index, item);
|
|
764
|
+
}
|
|
762
765
|
|
|
763
766
|
// Optimization: Cache sequential queries to avoid O(log N) tree traversal for every item
|
|
764
767
|
let lastIndexX = -1;
|
|
@@ -795,6 +798,9 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
795
798
|
|
|
796
799
|
const colRange = columnRange.value;
|
|
797
800
|
|
|
801
|
+
// Optimization: track sticky index pointer
|
|
802
|
+
let currentStickyIndexPtr = 0;
|
|
803
|
+
|
|
798
804
|
for (const i of sortedIndices) {
|
|
799
805
|
const item = props.value.items[ i ];
|
|
800
806
|
if (item === undefined) {
|
|
@@ -821,6 +827,12 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
821
827
|
const originalX = x;
|
|
822
828
|
const originalY = y;
|
|
823
829
|
|
|
830
|
+
// Find next sticky index for optimization
|
|
831
|
+
while (currentStickyIndexPtr < stickyIndices.length && stickyIndices[ currentStickyIndexPtr ]! <= i) {
|
|
832
|
+
currentStickyIndexPtr++;
|
|
833
|
+
}
|
|
834
|
+
const nextStickyIndex = currentStickyIndexPtr < stickyIndices.length ? stickyIndices[ currentStickyIndexPtr ] : undefined;
|
|
835
|
+
|
|
824
836
|
const { isStickyActive, isStickyActiveX, isStickyActiveY, stickyOffset } = calculateStickyItem({
|
|
825
837
|
index: i,
|
|
826
838
|
isSticky,
|
|
@@ -836,8 +848,9 @@ export function useVirtualScroll<T = unknown>(propsInput: MaybeRefOrGetter<Virtu
|
|
|
836
848
|
fixedWidth: fixedColumnWidth.value,
|
|
837
849
|
gap: props.value.gap || 0,
|
|
838
850
|
columnGap: props.value.columnGap || 0,
|
|
839
|
-
getItemQueryY:
|
|
840
|
-
getItemQueryX:
|
|
851
|
+
getItemQueryY: (idx) => itemSizesY.query(idx),
|
|
852
|
+
getItemQueryX: (idx) => itemSizesX.query(idx),
|
|
853
|
+
nextStickyIndex,
|
|
841
854
|
});
|
|
842
855
|
|
|
843
856
|
const offsetX = isHydrated.value
|
|
@@ -125,105 +125,144 @@ export function useVirtualScrollSizes<T>(
|
|
|
125
125
|
}
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Helper to initialize measurements for a single axis.
|
|
130
|
+
*/
|
|
131
|
+
const initializeAxis = (
|
|
132
|
+
count: number,
|
|
133
|
+
tree: FenwickTree,
|
|
134
|
+
measured: Uint8Array,
|
|
135
|
+
sizeProp: number | number[] | ((...args: any[]) => number) | null | undefined,
|
|
136
|
+
defaultSize: number,
|
|
137
|
+
gap: number,
|
|
138
|
+
isDynamic: boolean,
|
|
139
|
+
isX: boolean,
|
|
140
|
+
shouldReset: boolean,
|
|
141
|
+
) => {
|
|
142
|
+
let needsRebuild = false;
|
|
143
|
+
|
|
144
|
+
if (shouldReset) {
|
|
145
|
+
for (let i = 0; i < count; i++) {
|
|
146
|
+
if (tree.get(i) !== 0) {
|
|
147
|
+
tree.set(i, 0);
|
|
148
|
+
measured[ i ] = 0;
|
|
149
|
+
needsRebuild = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return needsRebuild;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < count; i++) {
|
|
156
|
+
const current = tree.get(i);
|
|
157
|
+
const isMeasured = measured[ i ] === 1;
|
|
158
|
+
|
|
159
|
+
if (!isDynamic || (!isMeasured && current === 0)) {
|
|
160
|
+
const baseSize = getSizeAt(i, sizeProp, defaultSize, gap, tree, isX) + gap;
|
|
161
|
+
|
|
162
|
+
if (Math.abs(current - baseSize) > 0.5) {
|
|
163
|
+
tree.set(i, baseSize);
|
|
164
|
+
measured[ i ] = isDynamic ? 0 : 1;
|
|
165
|
+
needsRebuild = true;
|
|
166
|
+
} else if (!isDynamic) {
|
|
167
|
+
measured[ i ] = 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return needsRebuild;
|
|
172
|
+
};
|
|
173
|
+
|
|
128
174
|
/**
|
|
129
175
|
* Initializes prefix sum trees from props (fixed sizes, width arrays, or functions).
|
|
130
176
|
*/
|
|
131
177
|
const initializeMeasurements = () => {
|
|
132
178
|
const propsVal = props.value.props;
|
|
133
|
-
const
|
|
134
|
-
const len = newItems.length;
|
|
179
|
+
const len = propsVal.items.length;
|
|
135
180
|
const colCount = propsVal.columnCount || 0;
|
|
136
181
|
const gap = propsVal.gap || 0;
|
|
137
182
|
const columnGap = propsVal.columnGap || 0;
|
|
138
183
|
const cw = propsVal.columnWidth;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
184
|
+
const itemSize = propsVal.itemSize;
|
|
185
|
+
const defaultColWidth = propsVal.defaultColumnWidth || DEFAULT_COLUMN_WIDTH;
|
|
186
|
+
const defaultItemSize = propsVal.defaultItemSize || props.value.defaultSize;
|
|
142
187
|
|
|
143
188
|
// Initialize columns
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (props.value.direction === 'horizontal') {
|
|
182
|
-
if (!props.value.isDynamicItemSize || (!isMeasuredX && currentX === 0)) {
|
|
183
|
-
const baseSize = getItemBaseSize(item as T, i);
|
|
184
|
-
const targetX = baseSize + columnGap;
|
|
185
|
-
if (Math.abs(currentX - targetX) > 0.5) {
|
|
186
|
-
itemSizesX.set(i, targetX);
|
|
187
|
-
measuredItemsX.value[ i ] = props.value.isDynamicItemSize ? 0 : 1;
|
|
188
|
-
itemsNeedRebuild = true;
|
|
189
|
-
} else if (!props.value.isDynamicItemSize) {
|
|
190
|
-
measuredItemsX.value[ i ] = 1;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} else if (currentX !== 0) {
|
|
194
|
-
itemSizesX.set(i, 0);
|
|
195
|
-
measuredItemsX.value[ i ] = 0;
|
|
196
|
-
itemsNeedRebuild = true;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (props.value.direction !== 'horizontal') {
|
|
200
|
-
if (!props.value.isDynamicItemSize || (!isMeasuredY && currentY === 0)) {
|
|
201
|
-
const baseSize = getItemBaseSize(item as T, i);
|
|
202
|
-
const targetY = baseSize + gap;
|
|
203
|
-
if (Math.abs(currentY - targetY) > 0.5) {
|
|
204
|
-
itemSizesY.set(i, targetY);
|
|
205
|
-
measuredItemsY.value[ i ] = props.value.isDynamicItemSize ? 0 : 1;
|
|
206
|
-
itemsNeedRebuild = true;
|
|
207
|
-
} else if (!props.value.isDynamicItemSize) {
|
|
208
|
-
measuredItemsY.value[ i ] = 1;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
} else if (currentY !== 0) {
|
|
212
|
-
itemSizesY.set(i, 0);
|
|
213
|
-
measuredItemsY.value[ i ] = 0;
|
|
214
|
-
itemsNeedRebuild = true;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
189
|
+
const colNeedsRebuild = initializeAxis(
|
|
190
|
+
colCount,
|
|
191
|
+
columnSizes,
|
|
192
|
+
measuredColumns.value,
|
|
193
|
+
cw,
|
|
194
|
+
defaultColWidth,
|
|
195
|
+
columnGap,
|
|
196
|
+
props.value.isDynamicColumnWidth,
|
|
197
|
+
true,
|
|
198
|
+
false,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Initialize items X
|
|
202
|
+
const itemsXNeedsRebuild = initializeAxis(
|
|
203
|
+
len,
|
|
204
|
+
itemSizesX,
|
|
205
|
+
measuredItemsX.value,
|
|
206
|
+
itemSize,
|
|
207
|
+
defaultItemSize,
|
|
208
|
+
columnGap,
|
|
209
|
+
props.value.isDynamicItemSize,
|
|
210
|
+
true,
|
|
211
|
+
props.value.direction !== 'horizontal',
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Initialize items Y
|
|
215
|
+
const itemsYNeedsRebuild = initializeAxis(
|
|
216
|
+
len,
|
|
217
|
+
itemSizesY,
|
|
218
|
+
measuredItemsY.value,
|
|
219
|
+
itemSize,
|
|
220
|
+
defaultItemSize,
|
|
221
|
+
gap,
|
|
222
|
+
props.value.isDynamicItemSize,
|
|
223
|
+
false,
|
|
224
|
+
props.value.direction === 'horizontal',
|
|
225
|
+
);
|
|
217
226
|
|
|
218
227
|
if (colNeedsRebuild) {
|
|
219
228
|
columnSizes.rebuild();
|
|
220
229
|
}
|
|
221
|
-
if (
|
|
230
|
+
if (itemsXNeedsRebuild) {
|
|
222
231
|
itemSizesX.rebuild();
|
|
232
|
+
}
|
|
233
|
+
if (itemsYNeedsRebuild) {
|
|
223
234
|
itemSizesY.rebuild();
|
|
224
235
|
}
|
|
225
236
|
};
|
|
226
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Helper to update a single size in the tree.
|
|
240
|
+
*/
|
|
241
|
+
const updateAxis = (
|
|
242
|
+
index: number,
|
|
243
|
+
newSize: number,
|
|
244
|
+
tree: FenwickTree,
|
|
245
|
+
measured: Uint8Array,
|
|
246
|
+
gap: number,
|
|
247
|
+
firstIndex: number,
|
|
248
|
+
accumulatedDelta: { val: number; },
|
|
249
|
+
) => {
|
|
250
|
+
const oldSize = tree.get(index);
|
|
251
|
+
const targetSize = newSize + gap;
|
|
252
|
+
let updated = false;
|
|
253
|
+
|
|
254
|
+
if (!measured[ index ] || Math.abs(targetSize - oldSize) > 0.1) {
|
|
255
|
+
const d = targetSize - oldSize;
|
|
256
|
+
tree.update(index, d);
|
|
257
|
+
measured[ index ] = 1;
|
|
258
|
+
updated = true;
|
|
259
|
+
if (index < firstIndex && oldSize > 0) {
|
|
260
|
+
accumulatedDelta.val += d;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return updated;
|
|
264
|
+
};
|
|
265
|
+
|
|
227
266
|
/**
|
|
228
267
|
* Initializes or updates sizes based on current props and items.
|
|
229
268
|
* Handles prepending of items by shifting existing measurements.
|
|
@@ -297,8 +336,8 @@ export function useVirtualScrollSizes<T>(
|
|
|
297
336
|
onScrollCorrection: (deltaX: number, deltaY: number) => void,
|
|
298
337
|
) => {
|
|
299
338
|
let needUpdate = false;
|
|
300
|
-
|
|
301
|
-
|
|
339
|
+
const deltaX = { val: 0 };
|
|
340
|
+
const deltaY = { val: 0 };
|
|
302
341
|
const propsVal = props.value.props;
|
|
303
342
|
const gap = propsVal.gap || 0;
|
|
304
343
|
const columnGap = propsVal.columnGap || 0;
|
|
@@ -315,19 +354,8 @@ export function useVirtualScrollSizes<T>(
|
|
|
315
354
|
const tryUpdateColumn = (colIdx: number, width: number) => {
|
|
316
355
|
if (colIdx >= 0 && colIdx < (propsVal.columnCount || 0) && !processedCols.has(colIdx)) {
|
|
317
356
|
processedCols.add(colIdx);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (!measuredColumns.value[ colIdx ] || Math.abs(oldW - targetW) > 0.1) {
|
|
322
|
-
const d = targetW - oldW;
|
|
323
|
-
if (Math.abs(d) > 0.1) {
|
|
324
|
-
columnSizes.update(colIdx, d);
|
|
325
|
-
needUpdate = true;
|
|
326
|
-
if (colIdx < firstColIndex && oldW > 0) {
|
|
327
|
-
deltaX += d;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
measuredColumns.value[ colIdx ] = 1;
|
|
357
|
+
if (updateAxis(colIdx, width, columnSizes, measuredColumns.value, columnGap, firstColIndex, deltaX)) {
|
|
358
|
+
needUpdate = true;
|
|
331
359
|
}
|
|
332
360
|
}
|
|
333
361
|
};
|
|
@@ -342,30 +370,13 @@ export function useVirtualScrollSizes<T>(
|
|
|
342
370
|
if (index >= 0 && !processedRows.has(index) && isMeasurable && blockSize > 0) {
|
|
343
371
|
processedRows.add(index);
|
|
344
372
|
if (isHorizontalMode && inlineSize > 0) {
|
|
345
|
-
|
|
346
|
-
const targetWidth = inlineSize + columnGap;
|
|
347
|
-
if (!measuredItemsX.value[ index ] || Math.abs(targetWidth - oldWidth) > 0.1) {
|
|
348
|
-
const d = targetWidth - oldWidth;
|
|
349
|
-
itemSizesX.update(index, d);
|
|
350
|
-
measuredItemsX.value[ index ] = 1;
|
|
373
|
+
if (updateAxis(index, inlineSize, itemSizesX, measuredItemsX.value, columnGap, firstRowIndex, deltaX)) {
|
|
351
374
|
needUpdate = true;
|
|
352
|
-
if (index < firstRowIndex && oldWidth > 0) {
|
|
353
|
-
deltaX += d;
|
|
354
|
-
}
|
|
355
375
|
}
|
|
356
376
|
}
|
|
357
377
|
if (!isHorizontalMode) {
|
|
358
|
-
|
|
359
|
-
const targetHeight = blockSize + gap;
|
|
360
|
-
|
|
361
|
-
if (!measuredItemsY.value[ index ] || Math.abs(targetHeight - oldHeight) > 0.1) {
|
|
362
|
-
const d = targetHeight - oldHeight;
|
|
363
|
-
itemSizesY.update(index, d);
|
|
364
|
-
measuredItemsY.value[ index ] = 1;
|
|
378
|
+
if (updateAxis(index, blockSize, itemSizesY, measuredItemsY.value, gap, firstRowIndex, deltaY)) {
|
|
365
379
|
needUpdate = true;
|
|
366
|
-
if (index < firstRowIndex && oldHeight > 0) {
|
|
367
|
-
deltaY += d;
|
|
368
|
-
}
|
|
369
380
|
}
|
|
370
381
|
}
|
|
371
382
|
}
|
|
@@ -396,8 +407,8 @@ export function useVirtualScrollSizes<T>(
|
|
|
396
407
|
|
|
397
408
|
if (needUpdate) {
|
|
398
409
|
treeUpdateFlag.value++;
|
|
399
|
-
if (deltaX !== 0 || deltaY !== 0) {
|
|
400
|
-
onScrollCorrection(deltaX, deltaY);
|
|
410
|
+
if (deltaX.val !== 0 || deltaY.val !== 0) {
|
|
411
|
+
onScrollCorrection(deltaX.val, deltaY.val);
|
|
401
412
|
}
|
|
402
413
|
}
|
|
403
414
|
};
|
|
@@ -361,6 +361,7 @@ function calculateAxisTarget({
|
|
|
361
361
|
* @param index - Item index.
|
|
362
362
|
* @param stickyIndices - All sticky indices.
|
|
363
363
|
* @param getNextStickyPos - Resolver for the next sticky item's position.
|
|
364
|
+
* @param nextStickyIndex - Pre-calculated next sticky index (optional).
|
|
364
365
|
* @returns Sticky state for this axis.
|
|
365
366
|
*/
|
|
366
367
|
function calculateAxisSticky(
|
|
@@ -370,12 +371,16 @@ function calculateAxisSticky(
|
|
|
370
371
|
index: number,
|
|
371
372
|
stickyIndices: number[],
|
|
372
373
|
getNextStickyPos: (idx: number) => number,
|
|
374
|
+
nextStickyIndex?: number,
|
|
373
375
|
) {
|
|
374
376
|
if (scrollPos <= originalPos) {
|
|
375
377
|
return { isActive: false, offset: 0 };
|
|
376
378
|
}
|
|
377
379
|
|
|
378
|
-
const nextStickyIdx =
|
|
380
|
+
const nextStickyIdx = nextStickyIndex !== undefined
|
|
381
|
+
? nextStickyIndex
|
|
382
|
+
: findNextStickyIndex(stickyIndices, index);
|
|
383
|
+
|
|
379
384
|
if (nextStickyIdx === undefined) {
|
|
380
385
|
return { isActive: true, offset: 0 };
|
|
381
386
|
}
|
|
@@ -770,6 +775,7 @@ export function calculateColumnRange({
|
|
|
770
775
|
* @param params.columnGap - Column gap (VU).
|
|
771
776
|
* @param params.getItemQueryY - Resolver for vertical offset (VU).
|
|
772
777
|
* @param params.getItemQueryX - Resolver for horizontal offset (VU).
|
|
778
|
+
* @param params.nextStickyIndex - Optional pre-calculated next sticky index.
|
|
773
779
|
* @returns Sticky state and offset (VU).
|
|
774
780
|
* @see StickyParams
|
|
775
781
|
*/
|
|
@@ -789,7 +795,8 @@ export function calculateStickyItem({
|
|
|
789
795
|
columnGap,
|
|
790
796
|
getItemQueryY,
|
|
791
797
|
getItemQueryX,
|
|
792
|
-
|
|
798
|
+
nextStickyIndex,
|
|
799
|
+
}: StickyParams & { nextStickyIndex?: number | undefined; }) {
|
|
793
800
|
let isStickyActiveX = false;
|
|
794
801
|
let isStickyActiveY = false;
|
|
795
802
|
const stickyOffset = { x: 0, y: 0 };
|
|
@@ -807,6 +814,7 @@ export function calculateStickyItem({
|
|
|
807
814
|
index,
|
|
808
815
|
stickyIndices,
|
|
809
816
|
(nextIdx) => (fixedSize !== null ? nextIdx * (fixedSize + gap) : getItemQueryY(nextIdx)),
|
|
817
|
+
nextStickyIndex,
|
|
810
818
|
);
|
|
811
819
|
isStickyActiveY = res.isActive;
|
|
812
820
|
stickyOffset.y = res.offset;
|
|
@@ -821,6 +829,7 @@ export function calculateStickyItem({
|
|
|
821
829
|
index,
|
|
822
830
|
stickyIndices,
|
|
823
831
|
(nextIdx) => (fixedSize !== null ? nextIdx * (fixedSize + columnGap) : getItemQueryX(nextIdx)),
|
|
832
|
+
nextStickyIndex,
|
|
824
833
|
);
|
|
825
834
|
|
|
826
835
|
if (res.isActive) {
|