@pdanpdan/virtual-scroll 0.10.0 → 0.10.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/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +23 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +337 -311
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/composables/useVirtualScroll.ts +47 -27
- package/src/extensions/index.ts +2 -2
- package/src/extensions/snapping.ts +15 -2
- package/src/types.ts +20 -1
- package/src/utils/scroll.ts +1 -1
- package/src/utils/virtual-scroll-logic.ts +22 -13
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ScrollDetails,
|
|
7
7
|
ScrollDirection,
|
|
8
8
|
ScrollToIndexOptions,
|
|
9
|
+
ScrollToIndexResult,
|
|
9
10
|
VirtualScrollProps,
|
|
10
11
|
} from '../types';
|
|
11
12
|
/* global ScrollToOptions */
|
|
@@ -112,10 +113,10 @@ export function useVirtualScroll<T = unknown>(
|
|
|
112
113
|
pendingScroll.value = null;
|
|
113
114
|
};
|
|
114
115
|
|
|
115
|
-
/**
|
|
116
|
-
let
|
|
117
|
-
/**
|
|
118
|
-
let
|
|
116
|
+
/** Horizontal virtual scroll position at the start of the current scroll interaction (VU). */
|
|
117
|
+
let scrollStartX = 0;
|
|
118
|
+
/** Vertical virtual scroll position at the start of the current scroll interaction (VU). */
|
|
119
|
+
let scrollStartY = 0;
|
|
119
120
|
|
|
120
121
|
// --- Computed Config ---
|
|
121
122
|
/** Validated scroll direction. */
|
|
@@ -348,10 +349,9 @@ export function useVirtualScroll<T = unknown>(
|
|
|
348
349
|
rowIndex?: number | null,
|
|
349
350
|
colIndex?: number | null,
|
|
350
351
|
options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions,
|
|
351
|
-
) {
|
|
352
|
-
const isCorrection = (
|
|
353
|
-
|
|
354
|
-
: false;
|
|
352
|
+
): ScrollToIndexResult {
|
|
353
|
+
const isCorrection = isScrollToIndexOptions(options) ? options.isCorrection : false;
|
|
354
|
+
const dryRun = isScrollToIndexOptions(options) ? options.dryRun : false;
|
|
355
355
|
|
|
356
356
|
const container = props.value.container || window;
|
|
357
357
|
|
|
@@ -393,7 +393,7 @@ export function useVirtualScroll<T = unknown>(
|
|
|
393
393
|
paddingEndY: paddingEndY.value,
|
|
394
394
|
});
|
|
395
395
|
|
|
396
|
-
if (!isCorrection) {
|
|
396
|
+
if (!isCorrection && !dryRun) {
|
|
397
397
|
const behavior = isScrollToIndexOptions(options) ? options.behavior : undefined;
|
|
398
398
|
pendingScroll.value = {
|
|
399
399
|
rowIndex,
|
|
@@ -416,14 +416,18 @@ export function useVirtualScroll<T = unknown>(
|
|
|
416
416
|
}
|
|
417
417
|
const scrollBehavior = isCorrection ? 'auto' : (behavior || 'smooth');
|
|
418
418
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
419
|
+
if (!dryRun) {
|
|
420
|
+
if (scrollBehavior === 'smooth' || !isCorrection) {
|
|
421
|
+
isProgrammaticScroll.value = true;
|
|
422
|
+
clearTimeout(programmaticScrollTimer);
|
|
423
|
+
if (scrollBehavior === 'smooth') {
|
|
424
|
+
programmaticScrollTimer = setTimeout(() => {
|
|
425
|
+
isProgrammaticScroll.value = false;
|
|
426
|
+
programmaticScrollTimer = undefined;
|
|
427
|
+
checkPendingScroll();
|
|
428
|
+
}, 1000);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
427
431
|
}
|
|
428
432
|
|
|
429
433
|
const scrollOptions: ScrollToOptions = { behavior: scrollBehavior };
|
|
@@ -433,9 +437,12 @@ export function useVirtualScroll<T = unknown>(
|
|
|
433
437
|
if (rowIndex !== null && rowIndex !== undefined) {
|
|
434
438
|
scrollOptions.top = Math.max(0, finalY);
|
|
435
439
|
}
|
|
436
|
-
scrollTo(container, scrollOptions);
|
|
437
440
|
|
|
438
|
-
if (
|
|
441
|
+
if (!isCorrection && !dryRun) {
|
|
442
|
+
scrollTo(container, scrollOptions);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!dryRun && (scrollBehavior === 'auto' || scrollBehavior === undefined)) {
|
|
439
446
|
if (colIndex !== null && colIndex !== undefined) {
|
|
440
447
|
scrollX.value = (isRtl.value ? finalX : Math.max(0, finalX));
|
|
441
448
|
internalScrollX.value = targetX;
|
|
@@ -445,6 +452,8 @@ export function useVirtualScroll<T = unknown>(
|
|
|
445
452
|
internalScrollY.value = targetY;
|
|
446
453
|
}
|
|
447
454
|
}
|
|
455
|
+
|
|
456
|
+
return { targetX, targetY, displayTargetX, displayTargetY };
|
|
448
457
|
}
|
|
449
458
|
|
|
450
459
|
function scrollToOffset(x?: number | null, y?: number | null, options?: { behavior?: 'auto' | 'smooth'; }) {
|
|
@@ -456,7 +465,7 @@ export function useVirtualScroll<T = unknown>(
|
|
|
456
465
|
isProgrammaticScroll.value = false;
|
|
457
466
|
programmaticScrollTimer = undefined;
|
|
458
467
|
checkPendingScroll();
|
|
459
|
-
},
|
|
468
|
+
}, 1000);
|
|
460
469
|
}
|
|
461
470
|
pendingScroll.value = null;
|
|
462
471
|
|
|
@@ -759,14 +768,23 @@ export function useVirtualScroll<T = unknown>(
|
|
|
759
768
|
const scrollValueX = isRtl.value ? Math.abs(scrollX.value) : scrollX.value;
|
|
760
769
|
const virtualX = displayToVirtual(scrollValueX, componentOffset.x, scaleX.value);
|
|
761
770
|
const virtualY = displayToVirtual(scrollY.value, componentOffset.y, scaleY.value);
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
771
|
+
|
|
772
|
+
if (!isProgrammaticScroll.value) {
|
|
773
|
+
if (!isScrolling.value) {
|
|
774
|
+
scrollStartX = internalScrollX.value;
|
|
775
|
+
scrollStartY = internalScrollY.value;
|
|
776
|
+
}
|
|
777
|
+
const deltaX = virtualX - scrollStartX;
|
|
778
|
+
const deltaY = virtualY - scrollStartY;
|
|
779
|
+
|
|
780
|
+
if (Math.abs(deltaX) > 0.5) {
|
|
781
|
+
scrollDirectionX.value = deltaX > 0 ? 'end' : 'start';
|
|
782
|
+
}
|
|
783
|
+
if (Math.abs(deltaY) > 0.5) {
|
|
784
|
+
scrollDirectionY.value = deltaY > 0 ? 'end' : 'start';
|
|
785
|
+
}
|
|
769
786
|
}
|
|
787
|
+
|
|
770
788
|
internalScrollX.value = virtualX;
|
|
771
789
|
internalScrollY.value = virtualY;
|
|
772
790
|
if (!isProgrammaticScroll.value) {
|
|
@@ -1085,5 +1103,7 @@ export function useVirtualScroll<T = unknown>(
|
|
|
1085
1103
|
getRowIndexAt,
|
|
1086
1104
|
/** Helper to get the column index at a specific virtual offset (VU). */
|
|
1087
1105
|
getColIndexAt,
|
|
1106
|
+
/** @internal */
|
|
1107
|
+
__internalState: ctx.internalState,
|
|
1088
1108
|
};
|
|
1089
1109
|
}
|
package/src/extensions/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RenderedItem, ScrollAlignment, ScrollAlignmentOptions, ScrollDetails, ScrollToIndexOptions, Size, VirtualScrollProps } from '../types';
|
|
1
|
+
import type { RenderedItem, ScrollAlignment, ScrollAlignmentOptions, ScrollDetails, ScrollToIndexOptions, ScrollToIndexResult, Size, VirtualScrollProps } from '../types';
|
|
2
2
|
import type { Ref } from 'vue';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -51,7 +51,7 @@ export interface ExtensionContext<T = unknown> {
|
|
|
51
51
|
/** Direct access to core component methods. */
|
|
52
52
|
methods: {
|
|
53
53
|
/** Scroll to a specific row and/or column. */
|
|
54
|
-
scrollToIndex: (rowIndex?: number | null, colIndex?: number | null, options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions) =>
|
|
54
|
+
scrollToIndex: (rowIndex?: number | null, colIndex?: number | null, options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions) => ScrollToIndexResult;
|
|
55
55
|
/** Scroll to a specific virtual pixel offset. */
|
|
56
56
|
scrollToOffset: (x?: number | null, y?: number | null, options?: { behavior?: 'auto' | 'smooth'; }) => void;
|
|
57
57
|
/** Detect and update text direction. */
|
|
@@ -72,10 +72,23 @@ export function useSnappingExtension<T = unknown>(): VirtualScrollExtension<T> {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
if (shouldSnap) {
|
|
75
|
-
ctx.methods.scrollToIndex(targetRow, targetCol, {
|
|
75
|
+
const { targetX, targetY } = ctx.methods.scrollToIndex(targetRow, targetCol, {
|
|
76
76
|
align: { x: alignX, y: alignY },
|
|
77
|
-
|
|
77
|
+
dryRun: true,
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
const currentX = ctx.internalState.internalScrollX.value;
|
|
81
|
+
const currentY = ctx.internalState.internalScrollY.value;
|
|
82
|
+
|
|
83
|
+
const diffX = (targetCol !== null) ? Math.abs(targetX - currentX) : 0;
|
|
84
|
+
const diffY = (targetRow !== null) ? Math.abs(targetY - currentY) : 0;
|
|
85
|
+
|
|
86
|
+
if (diffX > 0.5 || diffY > 0.5) {
|
|
87
|
+
ctx.methods.scrollToIndex(targetRow, targetCol, {
|
|
88
|
+
align: { x: alignX, y: alignY },
|
|
89
|
+
behavior: 'smooth',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
79
92
|
}
|
|
80
93
|
},
|
|
81
94
|
};
|
package/src/types.ts
CHANGED
|
@@ -87,6 +87,25 @@ export interface ScrollToIndexOptions {
|
|
|
87
87
|
* @internal
|
|
88
88
|
*/
|
|
89
89
|
isCorrection?: boolean;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* If true, only calculates the target position without performing the actual scroll.
|
|
93
|
+
* Useful for extensions that need to validate if a snap is necessary.
|
|
94
|
+
* @default false
|
|
95
|
+
*/
|
|
96
|
+
dryRun?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Result of the `scrollToIndex` method. */
|
|
100
|
+
export interface ScrollToIndexResult {
|
|
101
|
+
/** Target relative horizontal position in virtual units (VU). */
|
|
102
|
+
targetX: number;
|
|
103
|
+
/** Target relative vertical position in virtual units (VU). */
|
|
104
|
+
targetY: number;
|
|
105
|
+
/** Target display horizontal position (DU). */
|
|
106
|
+
displayTargetX: number;
|
|
107
|
+
/** Target display vertical position (DU). */
|
|
108
|
+
displayTargetY: number;
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
/** Represents an item currently rendered in the virtual scroll area. */
|
|
@@ -566,7 +585,7 @@ export interface VirtualScrollInstance<T = unknown> extends VirtualScrollCompone
|
|
|
566
585
|
/** The tag used for rendering items. */
|
|
567
586
|
itemTag: string;
|
|
568
587
|
/** Programmatically scroll to a specific row and/or column. */
|
|
569
|
-
scrollToIndex: (rowIndex?: number | null, colIndex?: number | null, options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions) =>
|
|
588
|
+
scrollToIndex: (rowIndex?: number | null, colIndex?: number | null, options?: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions) => ScrollToIndexResult;
|
|
570
589
|
/** Programmatically scroll to a specific pixel offset. */
|
|
571
590
|
scrollToOffset: (x?: number | null, y?: number | null, options?: { behavior?: 'auto' | 'smooth'; }) => void;
|
|
572
591
|
/** Resets all dynamic measurements and re-initializes from props. */
|
package/src/utils/scroll.ts
CHANGED
|
@@ -95,7 +95,7 @@ export function scrollTo(container: HTMLElement | Window | null | undefined, opt
|
|
|
95
95
|
* @returns `true` if the options object contains scroll-to-index specific properties.
|
|
96
96
|
*/
|
|
97
97
|
export function isScrollToIndexOptions(options: unknown): options is ScrollToIndexOptions {
|
|
98
|
-
return typeof options === 'object' && options != null && ('align' in options || 'behavior' in options || 'isCorrection' in options);
|
|
98
|
+
return typeof options === 'object' && options != null && ('align' in options || 'behavior' in options || 'isCorrection' in options || 'dryRun' in options);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
@@ -1190,35 +1190,44 @@ export function resolveSnap(
|
|
|
1190
1190
|
}
|
|
1191
1191
|
|
|
1192
1192
|
if (mode === 'next') {
|
|
1193
|
-
if (dir === '
|
|
1194
|
-
|
|
1195
|
-
} else if (dir === 'end') {
|
|
1196
|
-
effectiveMode = 'start';
|
|
1197
|
-
} else {
|
|
1198
|
-
return null;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
if (effectiveMode === 'start') {
|
|
1202
|
-
// Scrolling towards end (dir === 'end') -> snap to NEXT item start
|
|
1193
|
+
if (dir === 'end') {
|
|
1194
|
+
// Scrolling items UP (towards end of list) -> snap to NEXT item start
|
|
1203
1195
|
const size = getSize(currentIdx);
|
|
1204
1196
|
if (size > viewSize) {
|
|
1205
1197
|
return null;
|
|
1206
1198
|
}
|
|
1199
|
+
const scrolledOut = relScroll - getQuery(currentIdx);
|
|
1200
|
+
const threshold = Math.min(5, size * 0.1);
|
|
1201
|
+
if (scrolledOut <= threshold) {
|
|
1202
|
+
return {
|
|
1203
|
+
index: currentIdx,
|
|
1204
|
+
align: 'start' as const,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
1207
|
return {
|
|
1208
1208
|
index: Math.min(count - 1, currentIdx + 1),
|
|
1209
1209
|
align: 'start' as const,
|
|
1210
1210
|
};
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
// Scrolling towards start (dir === 'start') -> snap to PREVIOUS item end
|
|
1211
|
+
} else if (dir === 'start') {
|
|
1212
|
+
// Scrolling items DOWN (towards start of list) -> snap to PREVIOUS item end
|
|
1214
1213
|
const size = getSize(currentEndIdx);
|
|
1215
1214
|
if (size > viewSize) {
|
|
1216
1215
|
return null;
|
|
1217
1216
|
}
|
|
1217
|
+
const scrolledOutBottom = (getQuery(currentEndIdx) + size) - (relScroll + viewSize);
|
|
1218
|
+
const threshold = Math.min(5, size * 0.1);
|
|
1219
|
+
if (scrolledOutBottom <= threshold) {
|
|
1220
|
+
return {
|
|
1221
|
+
index: currentEndIdx,
|
|
1222
|
+
align: 'end' as const,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1218
1225
|
return {
|
|
1219
1226
|
index: Math.max(0, currentEndIdx - 1),
|
|
1220
1227
|
align: 'end' as const,
|
|
1221
1228
|
};
|
|
1229
|
+
} else {
|
|
1230
|
+
return null;
|
|
1222
1231
|
}
|
|
1223
1232
|
}
|
|
1224
1233
|
|