@pdanpdan/virtual-scroll 0.8.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 +104 -11
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +299 -32
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +954 -898
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -1
- package/package.json +1 -1
- package/src/components/VirtualScroll.vue +301 -209
- package/src/components/VirtualScrollbar.vue +8 -8
- package/src/composables/useVirtualScroll.ts +277 -524
- package/src/composables/useVirtualScrollSizes.ts +459 -0
- package/src/composables/useVirtualScrollbar.ts +30 -35
- package/src/index.ts +1 -0
- package/src/types.ts +25 -2
- package/src/utils/scroll.ts +14 -14
- package/src/utils/virtual-scroll-logic.ts +316 -3
|
@@ -11,7 +11,6 @@ import type {
|
|
|
11
11
|
ScrollbarSlotProps,
|
|
12
12
|
ScrollDetails,
|
|
13
13
|
ScrollToIndexOptions,
|
|
14
|
-
VirtualScrollbarProps,
|
|
15
14
|
VirtualScrollComponentProps,
|
|
16
15
|
VirtualScrollProps,
|
|
17
16
|
} from '../types';
|
|
@@ -23,8 +22,13 @@ import {
|
|
|
23
22
|
useVirtualScroll,
|
|
24
23
|
} from '../composables/useVirtualScroll';
|
|
25
24
|
import { useVirtualScrollbar } from '../composables/useVirtualScrollbar';
|
|
26
|
-
import { getPaddingX, getPaddingY } from '../utils/scroll';
|
|
27
|
-
import {
|
|
25
|
+
import { getPaddingX, getPaddingY, isWindowLike } from '../utils/scroll';
|
|
26
|
+
import {
|
|
27
|
+
calculateInertiaStep,
|
|
28
|
+
calculateInstantaneousVelocity,
|
|
29
|
+
calculateItemStyle,
|
|
30
|
+
displayToVirtual,
|
|
31
|
+
} from '../utils/virtual-scroll-logic';
|
|
28
32
|
import VirtualScrollbar from './VirtualScrollbar.vue';
|
|
29
33
|
|
|
30
34
|
export interface Props<T = unknown> extends VirtualScrollComponentProps<T> {}
|
|
@@ -110,9 +114,7 @@ const effectiveContainer = computed(() => (props.container === undefined ? hostR
|
|
|
110
114
|
|
|
111
115
|
const isHeaderFooterInsideContainer = computed(() => {
|
|
112
116
|
const container = effectiveContainer.value;
|
|
113
|
-
|
|
114
|
-
return container === hostRef.value
|
|
115
|
-
|| (typeof window !== 'undefined' && (container === window || container === null));
|
|
117
|
+
return container === hostRef.value || isWindowLike(container);
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
const virtualScrollProps = computed(() => {
|
|
@@ -167,6 +169,7 @@ const virtualScrollProps = computed(() => {
|
|
|
167
169
|
defaultItemSize: props.defaultItemSize,
|
|
168
170
|
defaultColumnWidth: props.defaultColumnWidth,
|
|
169
171
|
debug: props.debug,
|
|
172
|
+
snap: props.snap,
|
|
170
173
|
} as VirtualScrollProps<T>;
|
|
171
174
|
});
|
|
172
175
|
|
|
@@ -197,6 +200,8 @@ const {
|
|
|
197
200
|
componentOffset,
|
|
198
201
|
renderedVirtualWidth,
|
|
199
202
|
renderedVirtualHeight,
|
|
203
|
+
getRowIndexAt,
|
|
204
|
+
getColIndexAt,
|
|
200
205
|
} = useVirtualScroll(virtualScrollProps);
|
|
201
206
|
|
|
202
207
|
const useVirtualScrolling = computed(() => scaleX.value !== 1 || scaleY.value !== 1);
|
|
@@ -230,25 +235,25 @@ function handleHorizontalScrollbarScrollToOffset(offset: number) {
|
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
|
|
233
|
-
const verticalScrollbar = useVirtualScrollbar({
|
|
234
|
-
axis: 'vertical',
|
|
235
|
-
totalSize: renderedHeight,
|
|
236
|
-
position:
|
|
237
|
-
viewportSize:
|
|
238
|
+
const verticalScrollbar = useVirtualScrollbar(computed(() => ({
|
|
239
|
+
axis: 'vertical' as const,
|
|
240
|
+
totalSize: renderedHeight.value,
|
|
241
|
+
position: scrollDetails.value.displayScrollOffset.y,
|
|
242
|
+
viewportSize: scrollDetails.value.displayViewportSize.height,
|
|
238
243
|
scrollToOffset: handleVerticalScrollbarScrollToOffset,
|
|
239
|
-
containerId,
|
|
240
|
-
isRtl,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
const horizontalScrollbar = useVirtualScrollbar({
|
|
244
|
-
axis: 'horizontal',
|
|
245
|
-
totalSize: renderedWidth,
|
|
246
|
-
position:
|
|
247
|
-
viewportSize:
|
|
244
|
+
containerId: containerId.value,
|
|
245
|
+
isRtl: isRtl.value,
|
|
246
|
+
})));
|
|
247
|
+
|
|
248
|
+
const horizontalScrollbar = useVirtualScrollbar(computed(() => ({
|
|
249
|
+
axis: 'horizontal' as const,
|
|
250
|
+
totalSize: renderedWidth.value,
|
|
251
|
+
position: scrollDetails.value.displayScrollOffset.x,
|
|
252
|
+
viewportSize: scrollDetails.value.displayViewportSize.width,
|
|
248
253
|
scrollToOffset: handleHorizontalScrollbarScrollToOffset,
|
|
249
|
-
containerId,
|
|
250
|
-
isRtl,
|
|
251
|
-
});
|
|
254
|
+
containerId: containerId.value,
|
|
255
|
+
isRtl: isRtl.value,
|
|
256
|
+
})));
|
|
252
257
|
|
|
253
258
|
const slotColumnRange = computed(() => {
|
|
254
259
|
if (props.direction !== 'both') {
|
|
@@ -414,10 +419,7 @@ onMounted(() => {
|
|
|
414
419
|
|
|
415
420
|
// Re-observe items that were set before observer was ready
|
|
416
421
|
for (const el of itemRefs.values()) {
|
|
417
|
-
|
|
418
|
-
if (props.direction === 'both') {
|
|
419
|
-
el.querySelectorAll('[data-col-index]').forEach((c) => itemResizeObserver?.observe(c));
|
|
420
|
-
}
|
|
422
|
+
observeItem(el, true);
|
|
421
423
|
}
|
|
422
424
|
});
|
|
423
425
|
|
|
@@ -440,6 +442,20 @@ watch([ hostRef, useVirtualScrolling ], ([ host, virtual ], [ oldHost, oldVirtua
|
|
|
440
442
|
}
|
|
441
443
|
}, { immediate: true });
|
|
442
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Helper to manage ResizeObserver for an item and its optional cells.
|
|
447
|
+
*
|
|
448
|
+
* @param el - The item element.
|
|
449
|
+
* @param isObserve - True to observe, false to unobserve.
|
|
450
|
+
*/
|
|
451
|
+
function observeItem(el: HTMLElement, isObserve: boolean) {
|
|
452
|
+
const method = isObserve ? 'observe' : 'unobserve';
|
|
453
|
+
itemResizeObserver?.[ method ](el);
|
|
454
|
+
if (props.direction === 'both' && el.children.length > 0) {
|
|
455
|
+
el.querySelectorAll('[data-col-index]').forEach((c) => itemResizeObserver?.[ method ](c));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
443
459
|
/**
|
|
444
460
|
* Callback ref to track and measure item elements.
|
|
445
461
|
*
|
|
@@ -448,19 +464,13 @@ watch([ hostRef, useVirtualScrolling ], ([ host, virtual ], [ oldHost, oldVirtua
|
|
|
448
464
|
*/
|
|
449
465
|
function setItemRef(el: unknown, index: number) {
|
|
450
466
|
if (el) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (props.direction === 'both') {
|
|
455
|
-
(el as HTMLElement).querySelectorAll('[data-col-index]').forEach((c) => itemResizeObserver?.observe(c));
|
|
456
|
-
}
|
|
467
|
+
const htmlEl = el as HTMLElement;
|
|
468
|
+
itemRefs.set(index, htmlEl);
|
|
469
|
+
observeItem(htmlEl, true);
|
|
457
470
|
} else {
|
|
458
471
|
const oldEl = itemRefs.get(index);
|
|
459
472
|
if (oldEl) {
|
|
460
|
-
|
|
461
|
-
if (props.direction === 'both') {
|
|
462
|
-
oldEl.querySelectorAll('[data-col-index]').forEach((c) => itemResizeObserver?.unobserve(c));
|
|
463
|
-
}
|
|
473
|
+
observeItem(oldEl, false);
|
|
464
474
|
itemRefs.delete(index);
|
|
465
475
|
}
|
|
466
476
|
}
|
|
@@ -487,18 +497,17 @@ const MIN_VELOCITY = 0.1;
|
|
|
487
497
|
*/
|
|
488
498
|
function startInertiaAnimation() {
|
|
489
499
|
const step = () => {
|
|
490
|
-
|
|
491
|
-
velocity.x
|
|
492
|
-
velocity.y
|
|
500
|
+
const { nextVelocity, delta } = calculateInertiaStep(velocity, FRICTION);
|
|
501
|
+
velocity.x = nextVelocity.x;
|
|
502
|
+
velocity.y = nextVelocity.y;
|
|
493
503
|
|
|
494
504
|
// Calculate the new scroll offset
|
|
495
|
-
const currentX = scrollDetails.value.scrollOffset
|
|
496
|
-
const currentY = scrollDetails.value.scrollOffset.y;
|
|
505
|
+
const { x: currentX, y: currentY } = scrollDetails.value.scrollOffset;
|
|
497
506
|
|
|
498
507
|
// Move the scroll position by the current velocity
|
|
499
508
|
scrollToOffset(
|
|
500
|
-
currentX +
|
|
501
|
-
currentY +
|
|
509
|
+
currentX + delta.x,
|
|
510
|
+
currentY + delta.y,
|
|
502
511
|
{ behavior: 'auto' },
|
|
503
512
|
);
|
|
504
513
|
|
|
@@ -569,12 +578,11 @@ function handlePointerMove(event: PointerEvent) {
|
|
|
569
578
|
|
|
570
579
|
if (dt > 0) {
|
|
571
580
|
// Calculate instantaneous velocity (pixels per millisecond)
|
|
572
|
-
const
|
|
573
|
-
const instantVelocityY = (lastPointerPos.y - event.clientY) / dt;
|
|
581
|
+
const instantVelocity = calculateInstantaneousVelocity(lastPointerPos, { x: event.clientX, y: event.clientY }, dt);
|
|
574
582
|
|
|
575
583
|
// Use a moving average for smoother velocity tracking
|
|
576
|
-
velocity.x = velocity.x * 0.2 +
|
|
577
|
-
velocity.y = velocity.y * 0.2 +
|
|
584
|
+
velocity.x = velocity.x * 0.2 + instantVelocity.x * 0.8;
|
|
585
|
+
velocity.y = velocity.y * 0.2 + instantVelocity.y * 0.8;
|
|
578
586
|
}
|
|
579
587
|
|
|
580
588
|
lastPointerPos = { x: event.clientX, y: event.clientY };
|
|
@@ -632,18 +640,14 @@ function handleWheel(event: WheelEvent) {
|
|
|
632
640
|
event.preventDefault();
|
|
633
641
|
|
|
634
642
|
// For large content we manually scroll to keep precision/control
|
|
635
|
-
let deltaX = event
|
|
636
|
-
let deltaY = event.deltaY;
|
|
643
|
+
let { deltaX, deltaY } = event;
|
|
637
644
|
|
|
638
645
|
if (event.shiftKey && deltaX === 0) {
|
|
639
646
|
deltaX = deltaY;
|
|
640
647
|
deltaY = 0;
|
|
641
648
|
}
|
|
642
649
|
|
|
643
|
-
|
|
644
|
-
const targetY = scrollOffset.y + deltaY;
|
|
645
|
-
|
|
646
|
-
scrollToOffset(targetX, targetY, { behavior: 'auto' });
|
|
650
|
+
scrollToOffset(scrollOffset.x + deltaX, scrollOffset.y + deltaY, { behavior: 'auto' });
|
|
647
651
|
}
|
|
648
652
|
}
|
|
649
653
|
|
|
@@ -657,8 +661,149 @@ function handleKeyDown(event: KeyboardEvent) {
|
|
|
657
661
|
const isHorizontal = props.direction !== 'vertical';
|
|
658
662
|
const isVertical = props.direction !== 'horizontal';
|
|
659
663
|
|
|
660
|
-
const
|
|
661
|
-
const
|
|
664
|
+
const vProps = virtualScrollProps.value;
|
|
665
|
+
const sStart = vProps.stickyStart as { x: number; y: number; };
|
|
666
|
+
const sEnd = vProps.stickyEnd as { x: number; y: number; };
|
|
667
|
+
const pStart = vProps.scrollPaddingStart as { x: number; y: number; };
|
|
668
|
+
const pEnd = vProps.scrollPaddingEnd as { x: number; y: number; };
|
|
669
|
+
|
|
670
|
+
const snapModeProp = props.snap === true ? 'auto' : props.snap;
|
|
671
|
+
const snapMode = (snapModeProp && snapModeProp !== 'auto')
|
|
672
|
+
? snapModeProp as 'start' | 'center' | 'end'
|
|
673
|
+
: null;
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Helper to find center index.
|
|
677
|
+
*/
|
|
678
|
+
const getCenterIndex = (isX: boolean) => {
|
|
679
|
+
const centerPos = (isX ? scrollOffset.x : scrollOffset.y) + (isX ? viewportSize.width : viewportSize.height) / 2;
|
|
680
|
+
return isX ? getColIndexAt(centerPos) : getRowIndexAt(centerPos);
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const { currentIndex, currentEndIndex, currentColIndex, currentEndColIndex } = scrollDetails.value;
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Helper to calculate the target index for PageUp/PageDown.
|
|
687
|
+
*
|
|
688
|
+
* @param isVerticalAxis - True for vertical, false for horizontal.
|
|
689
|
+
* @param isForward - True for forward (PageDown), false for backward (PageUp).
|
|
690
|
+
*/
|
|
691
|
+
const getPageTarget = (isVerticalAxis: boolean, isForward: boolean) => {
|
|
692
|
+
const isHorizontalAxis = !isVerticalAxis;
|
|
693
|
+
const startIdx = isVerticalAxis ? currentIndex : currentColIndex;
|
|
694
|
+
const endIdx = isVerticalAxis ? currentEndIndex : currentEndColIndex;
|
|
695
|
+
const pageSize = Math.max(1, endIdx - startIdx);
|
|
696
|
+
const maxIdx = isVerticalAxis
|
|
697
|
+
? props.items.length - 1
|
|
698
|
+
: (props.columnCount ? props.columnCount - 1 : props.items.length - 1);
|
|
699
|
+
|
|
700
|
+
if (isForward) {
|
|
701
|
+
if (snapMode === 'center') {
|
|
702
|
+
return Math.min(maxIdx, getCenterIndex(isHorizontalAxis) + pageSize);
|
|
703
|
+
}
|
|
704
|
+
if (snapMode === 'end') {
|
|
705
|
+
return Math.min(maxIdx, endIdx + pageSize);
|
|
706
|
+
}
|
|
707
|
+
return endIdx; // default or snapMode === 'start'
|
|
708
|
+
} else {
|
|
709
|
+
// backward
|
|
710
|
+
if (snapMode === 'center') {
|
|
711
|
+
return Math.max(0, getCenterIndex(isHorizontalAxis) - pageSize);
|
|
712
|
+
}
|
|
713
|
+
if (snapMode === 'start') {
|
|
714
|
+
return Math.max(0, startIdx - pageSize);
|
|
715
|
+
}
|
|
716
|
+
return startIdx; // default or snapMode === 'end'
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Performs keyboard navigation for arrow keys.
|
|
722
|
+
*
|
|
723
|
+
* @param isVerticalAxis - True for vertical, false for horizontal.
|
|
724
|
+
* @param isForward - True for forward direction (Down/Right), false for backward.
|
|
725
|
+
*/
|
|
726
|
+
const navigate = (isVerticalAxis: boolean, isForward: boolean) => {
|
|
727
|
+
const isHorizontalAxis = !isVerticalAxis;
|
|
728
|
+
|
|
729
|
+
if (snapMode === 'center') {
|
|
730
|
+
const centerIdx = getCenterIndex(isHorizontalAxis);
|
|
731
|
+
const maxIdx = isHorizontalAxis
|
|
732
|
+
? (props.columnCount ? props.columnCount - 1 : props.items.length - 1)
|
|
733
|
+
: props.items.length - 1;
|
|
734
|
+
const targetIdx = isForward ? Math.min(maxIdx, centerIdx + 1) : Math.max(0, centerIdx - 1);
|
|
735
|
+
scrollToIndex(isVerticalAxis ? targetIdx : null, isHorizontalAxis ? targetIdx : null, { align: 'center' });
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (isVerticalAxis) {
|
|
740
|
+
if (isForward) {
|
|
741
|
+
if (snapMode === 'start') {
|
|
742
|
+
scrollToIndex(Math.min(props.items.length - 1, currentIndex + 1), null, { align: 'start' });
|
|
743
|
+
} else {
|
|
744
|
+
const align = snapMode || 'end';
|
|
745
|
+
const viewportBottom = scrollOffset.y + viewportSize.height - (sEnd.y + pEnd.y);
|
|
746
|
+
const itemBottom = getRowOffset(currentEndIndex) + getRowHeight(currentEndIndex);
|
|
747
|
+
|
|
748
|
+
if (itemBottom > viewportBottom + 1) {
|
|
749
|
+
scrollToIndex(currentEndIndex, null, { align });
|
|
750
|
+
} else if (currentEndIndex < props.items.length - 1) {
|
|
751
|
+
scrollToIndex(currentEndIndex + 1, null, { align });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
// backward
|
|
756
|
+
if (snapMode === 'end') {
|
|
757
|
+
scrollToIndex(Math.max(0, currentEndIndex - 1), null, { align: 'end' });
|
|
758
|
+
} else {
|
|
759
|
+
const align = snapMode || 'start';
|
|
760
|
+
const viewportTop = scrollOffset.y + sStart.y + pStart.y;
|
|
761
|
+
const itemPos = getRowOffset(currentIndex);
|
|
762
|
+
|
|
763
|
+
if (itemPos < viewportTop - 1) {
|
|
764
|
+
scrollToIndex(currentIndex, null, { align });
|
|
765
|
+
} else if (currentIndex > 0) {
|
|
766
|
+
scrollToIndex(currentIndex - 1, null, { align });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
// Horizontal axis
|
|
772
|
+
const maxColIdx = props.columnCount ? props.columnCount - 1 : props.items.length - 1;
|
|
773
|
+
const isLogicalForward = isRtl.value ? !isForward : isForward;
|
|
774
|
+
|
|
775
|
+
if (isLogicalForward) {
|
|
776
|
+
if (snapMode === 'start') {
|
|
777
|
+
scrollToIndex(null, Math.min(maxColIdx, currentColIndex + 1), { align: 'start' });
|
|
778
|
+
} else {
|
|
779
|
+
const align = snapMode || 'end';
|
|
780
|
+
const viewportRight = scrollOffset.x + viewportSize.width - (sEnd.x + pEnd.x);
|
|
781
|
+
const colEndPos = (props.columnCount ? getColumnOffset(currentEndColIndex) + getColumnWidth(currentEndColIndex) : getItemOffset(currentEndColIndex) + getItemSize(currentEndColIndex));
|
|
782
|
+
|
|
783
|
+
if (colEndPos > viewportRight + 1) {
|
|
784
|
+
scrollToIndex(null, currentEndColIndex, { align });
|
|
785
|
+
} else if (currentEndColIndex < maxColIdx) {
|
|
786
|
+
scrollToIndex(null, currentEndColIndex + 1, { align });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
// backward
|
|
791
|
+
if (snapMode === 'end') {
|
|
792
|
+
scrollToIndex(null, Math.max(0, currentEndColIndex - 1), { align: 'end' });
|
|
793
|
+
} else {
|
|
794
|
+
const align = snapMode || 'start';
|
|
795
|
+
const viewportLeft = scrollOffset.x + sStart.x + pStart.x;
|
|
796
|
+
const colStartPos = (props.columnCount ? getColumnOffset(currentColIndex) : getItemOffset(currentColIndex));
|
|
797
|
+
|
|
798
|
+
if (colStartPos < viewportLeft - 1) {
|
|
799
|
+
scrollToIndex(null, currentColIndex, { align });
|
|
800
|
+
} else if (currentColIndex > 0) {
|
|
801
|
+
scrollToIndex(null, currentColIndex - 1, { align });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
};
|
|
662
807
|
|
|
663
808
|
switch (event.key) {
|
|
664
809
|
case 'Home': {
|
|
@@ -696,127 +841,51 @@ function handleKeyDown(event: KeyboardEvent) {
|
|
|
696
841
|
}
|
|
697
842
|
break;
|
|
698
843
|
}
|
|
699
|
-
case 'ArrowUp':
|
|
844
|
+
case 'ArrowUp':
|
|
700
845
|
event.preventDefault();
|
|
701
846
|
stopProgrammaticScroll();
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const { currentIndex, scrollOffset } = scrollDetails.value;
|
|
707
|
-
const viewportTop = scrollOffset.y + sStart.y + (virtualScrollProps.value.scrollPaddingStart as { x: number; y: number; }).y;
|
|
708
|
-
const itemPos = getRowOffset(currentIndex);
|
|
709
|
-
|
|
710
|
-
if (itemPos < viewportTop - 1) {
|
|
711
|
-
scrollToIndex(currentIndex, null, { align: 'start' });
|
|
712
|
-
} else if (currentIndex > 0) {
|
|
713
|
-
scrollToIndex(currentIndex - 1, null, { align: 'start' });
|
|
847
|
+
if (isVertical) {
|
|
848
|
+
navigate(true, false);
|
|
714
849
|
}
|
|
715
850
|
break;
|
|
716
|
-
|
|
717
|
-
case 'ArrowDown': {
|
|
851
|
+
case 'ArrowDown':
|
|
718
852
|
event.preventDefault();
|
|
719
853
|
stopProgrammaticScroll();
|
|
720
|
-
if (
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const { currentEndIndex } = scrollDetails.value;
|
|
725
|
-
const viewportBottom = scrollOffset.y + viewportSize.height - (sEnd.y + (virtualScrollProps.value.scrollPaddingEnd as { x: number; y: number; }).y);
|
|
726
|
-
const itemBottom = getRowOffset(currentEndIndex) + getRowHeight(currentEndIndex);
|
|
727
|
-
|
|
728
|
-
if (itemBottom > viewportBottom + 1) {
|
|
729
|
-
scrollToIndex(currentEndIndex, null, { align: 'end' });
|
|
730
|
-
} else if (currentEndIndex < props.items.length - 1) {
|
|
731
|
-
scrollToIndex(currentEndIndex + 1, null, { align: 'end' });
|
|
854
|
+
if (isVertical) {
|
|
855
|
+
navigate(true, true);
|
|
732
856
|
}
|
|
733
857
|
break;
|
|
734
|
-
|
|
735
|
-
case 'ArrowLeft': {
|
|
858
|
+
case 'ArrowLeft':
|
|
736
859
|
event.preventDefault();
|
|
737
860
|
stopProgrammaticScroll();
|
|
738
|
-
if (
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const { currentColIndex, currentEndColIndex } = scrollDetails.value;
|
|
743
|
-
|
|
744
|
-
if (isRtl.value) {
|
|
745
|
-
// RTL ArrowLeft -> towards logical END (Left)
|
|
746
|
-
const viewportLeft = scrollOffset.x + viewportSize.width - (sEnd.x + (virtualScrollProps.value.scrollPaddingEnd as { x: number; y: number; }).x);
|
|
747
|
-
const colEndPos = (props.columnCount ? getColumnOffset(currentEndColIndex) + getColumnWidth(currentEndColIndex) : getItemOffset(currentEndColIndex) + getItemSize(currentEndColIndex));
|
|
748
|
-
|
|
749
|
-
if (colEndPos > viewportLeft + 1) {
|
|
750
|
-
scrollToIndex(null, currentEndColIndex, { align: 'end' });
|
|
751
|
-
} else {
|
|
752
|
-
const maxColIdx = props.columnCount ? props.columnCount - 1 : props.items.length - 1;
|
|
753
|
-
if (currentEndColIndex < maxColIdx) {
|
|
754
|
-
scrollToIndex(null, currentEndColIndex + 1, { align: 'end' });
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
} else {
|
|
758
|
-
// LTR ArrowLeft -> towards logical START (Left)
|
|
759
|
-
const viewportLeft = scrollOffset.x + sStart.x + (virtualScrollProps.value.scrollPaddingStart as { x: number; y: number; }).x;
|
|
760
|
-
const colStartPos = (props.columnCount ? getColumnOffset(currentColIndex) : getItemOffset(currentColIndex));
|
|
761
|
-
|
|
762
|
-
if (colStartPos < viewportLeft - 1) {
|
|
763
|
-
scrollToIndex(null, currentColIndex, { align: 'start' });
|
|
764
|
-
} else if (currentColIndex > 0) {
|
|
765
|
-
scrollToIndex(null, currentColIndex - 1, { align: 'start' });
|
|
766
|
-
}
|
|
861
|
+
if (isHorizontal) {
|
|
862
|
+
navigate(false, false);
|
|
767
863
|
}
|
|
768
864
|
break;
|
|
769
|
-
|
|
770
|
-
case 'ArrowRight': {
|
|
865
|
+
case 'ArrowRight':
|
|
771
866
|
event.preventDefault();
|
|
772
867
|
stopProgrammaticScroll();
|
|
773
|
-
if (
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const { currentColIndex, currentEndColIndex } = scrollDetails.value;
|
|
778
|
-
|
|
779
|
-
if (isRtl.value) {
|
|
780
|
-
// RTL ArrowRight -> towards logical START (Right)
|
|
781
|
-
const viewportRight = scrollOffset.x + sStart.x + (virtualScrollProps.value.scrollPaddingStart as { x: number; y: number; }).x;
|
|
782
|
-
const colStartPos = (props.columnCount ? getColumnOffset(currentColIndex) : getItemOffset(currentColIndex));
|
|
783
|
-
|
|
784
|
-
if (colStartPos < viewportRight - 1) {
|
|
785
|
-
scrollToIndex(null, currentColIndex, { align: 'start' });
|
|
786
|
-
} else if (currentColIndex > 0) {
|
|
787
|
-
scrollToIndex(null, currentColIndex - 1, { align: 'start' });
|
|
788
|
-
}
|
|
789
|
-
} else {
|
|
790
|
-
// LTR ArrowRight -> towards logical END (Right)
|
|
791
|
-
const viewportRight = scrollOffset.x + viewportSize.width - (sEnd.x + (virtualScrollProps.value.scrollPaddingEnd as { x: number; y: number; }).x);
|
|
792
|
-
const colEndPos = (props.columnCount ? getColumnOffset(currentEndColIndex) + getColumnWidth(currentEndColIndex) : getItemOffset(currentEndColIndex) + getItemSize(currentEndColIndex));
|
|
793
|
-
|
|
794
|
-
if (colEndPos > viewportRight + 1) {
|
|
795
|
-
scrollToIndex(null, currentEndColIndex, { align: 'end' });
|
|
796
|
-
} else {
|
|
797
|
-
const maxColIdx = props.columnCount ? props.columnCount - 1 : props.items.length - 1;
|
|
798
|
-
if (currentEndColIndex < maxColIdx) {
|
|
799
|
-
scrollToIndex(null, currentEndColIndex + 1, { align: 'end' });
|
|
800
|
-
}
|
|
801
|
-
}
|
|
868
|
+
if (isHorizontal) {
|
|
869
|
+
navigate(false, true);
|
|
802
870
|
}
|
|
803
871
|
break;
|
|
804
|
-
}
|
|
805
872
|
case 'PageUp':
|
|
806
873
|
event.preventDefault();
|
|
807
874
|
stopProgrammaticScroll();
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
875
|
+
if (props.direction === 'horizontal') {
|
|
876
|
+
scrollToIndex(null, getPageTarget(false, false), { align: snapMode || 'end' });
|
|
877
|
+
} else {
|
|
878
|
+
scrollToIndex(getPageTarget(true, false), null, { align: snapMode || 'end' });
|
|
879
|
+
}
|
|
812
880
|
break;
|
|
813
881
|
case 'PageDown':
|
|
814
882
|
event.preventDefault();
|
|
815
883
|
stopProgrammaticScroll();
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
884
|
+
if (props.direction === 'horizontal') {
|
|
885
|
+
scrollToIndex(null, getPageTarget(false, true), { align: snapMode || 'start' });
|
|
886
|
+
} else {
|
|
887
|
+
scrollToIndex(getPageTarget(true, true), null, { align: snapMode || 'start' });
|
|
888
|
+
}
|
|
820
889
|
break;
|
|
821
890
|
}
|
|
822
891
|
}
|
|
@@ -855,70 +924,79 @@ const containerStyle = computed(() => {
|
|
|
855
924
|
return base;
|
|
856
925
|
});
|
|
857
926
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
927
|
+
/**
|
|
928
|
+
* Internal helper to generate consistent ScrollbarSlotProps.
|
|
929
|
+
*
|
|
930
|
+
* @param axis - The scroll axis.
|
|
931
|
+
* @param totalSize - Total scrollable size (DU).
|
|
932
|
+
* @param position - Current scroll position (DU).
|
|
933
|
+
* @param viewportSize - Current viewport size (DU).
|
|
934
|
+
* @param scrollToOffsetCallback - Callback to perform scroll.
|
|
935
|
+
* @param scrollbar - Scrollbar state from useVirtualScrollbar.
|
|
936
|
+
* @returns Props for the scrollbar slot or null if content fits.
|
|
937
|
+
*/
|
|
938
|
+
function getScrollbarSlotProps(
|
|
939
|
+
axis: 'vertical' | 'horizontal',
|
|
940
|
+
totalSize: number,
|
|
941
|
+
position: number,
|
|
942
|
+
viewportSize: number,
|
|
943
|
+
scrollToOffsetCallback: (offset: number) => void,
|
|
944
|
+
scrollbar: ReturnType<typeof useVirtualScrollbar>,
|
|
945
|
+
): ScrollbarSlotProps | null {
|
|
946
|
+
if (totalSize <= viewportSize) {
|
|
864
947
|
return null;
|
|
865
948
|
}
|
|
866
949
|
|
|
867
|
-
const scrollbarProps: VirtualScrollbarProps = {
|
|
868
|
-
axis: 'vertical',
|
|
869
|
-
totalSize: renderedHeight.value,
|
|
870
|
-
position: displayScrollOffset.y,
|
|
871
|
-
viewportSize: displayViewportSize.height,
|
|
872
|
-
scrollToOffset: handleVerticalScrollbarScrollToOffset,
|
|
873
|
-
containerId: containerId.value,
|
|
874
|
-
isRtl: isRtl.value,
|
|
875
|
-
ariaLabel: 'Vertical scroll',
|
|
876
|
-
};
|
|
877
|
-
|
|
878
950
|
return {
|
|
879
|
-
axis
|
|
880
|
-
positionPercent:
|
|
881
|
-
viewportPercent:
|
|
882
|
-
thumbSizePercent:
|
|
883
|
-
thumbPositionPercent:
|
|
884
|
-
trackProps:
|
|
885
|
-
thumbProps:
|
|
886
|
-
scrollbarProps
|
|
887
|
-
|
|
951
|
+
axis,
|
|
952
|
+
positionPercent: scrollbar.positionPercent.value,
|
|
953
|
+
viewportPercent: scrollbar.viewportPercent.value,
|
|
954
|
+
thumbSizePercent: scrollbar.thumbSizePercent.value,
|
|
955
|
+
thumbPositionPercent: scrollbar.thumbPositionPercent.value,
|
|
956
|
+
trackProps: scrollbar.trackProps.value,
|
|
957
|
+
thumbProps: scrollbar.thumbProps.value,
|
|
958
|
+
scrollbarProps: {
|
|
959
|
+
axis,
|
|
960
|
+
totalSize,
|
|
961
|
+
position,
|
|
962
|
+
viewportSize,
|
|
963
|
+
scrollToOffset: scrollToOffsetCallback,
|
|
964
|
+
containerId: containerId.value,
|
|
965
|
+
isRtl: isRtl.value,
|
|
966
|
+
ariaLabel: `${ axis === 'vertical' ? 'Vertical' : 'Horizontal' } scroll`,
|
|
967
|
+
},
|
|
968
|
+
isDragging: scrollbar.isDragging.value,
|
|
888
969
|
};
|
|
889
|
-
}
|
|
970
|
+
}
|
|
890
971
|
|
|
891
|
-
const
|
|
892
|
-
if (props.direction === '
|
|
972
|
+
const verticalScrollbarProps = computed(() => {
|
|
973
|
+
if (props.direction === 'horizontal') {
|
|
893
974
|
return null;
|
|
894
975
|
}
|
|
895
976
|
const { displayViewportSize, displayScrollOffset } = scrollDetails.value;
|
|
896
|
-
|
|
977
|
+
return getScrollbarSlotProps(
|
|
978
|
+
'vertical',
|
|
979
|
+
renderedHeight.value,
|
|
980
|
+
displayScrollOffset.y,
|
|
981
|
+
displayViewportSize.height,
|
|
982
|
+
handleVerticalScrollbarScrollToOffset,
|
|
983
|
+
verticalScrollbar,
|
|
984
|
+
);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
const horizontalScrollbarProps = computed(() => {
|
|
988
|
+
if (props.direction === 'vertical') {
|
|
897
989
|
return null;
|
|
898
990
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
ariaLabel: 'Horizontal scroll',
|
|
909
|
-
};
|
|
910
|
-
|
|
911
|
-
return {
|
|
912
|
-
axis: 'horizontal',
|
|
913
|
-
positionPercent: horizontalScrollbar.positionPercent.value,
|
|
914
|
-
viewportPercent: horizontalScrollbar.viewportPercent.value,
|
|
915
|
-
thumbSizePercent: horizontalScrollbar.thumbSizePercent.value,
|
|
916
|
-
thumbPositionPercent: horizontalScrollbar.thumbPositionPercent.value,
|
|
917
|
-
trackProps: horizontalScrollbar.trackProps.value,
|
|
918
|
-
thumbProps: horizontalScrollbar.thumbProps.value,
|
|
919
|
-
scrollbarProps,
|
|
920
|
-
isDragging: horizontalScrollbar.isDragging.value,
|
|
921
|
-
};
|
|
991
|
+
const { displayViewportSize, displayScrollOffset } = scrollDetails.value;
|
|
992
|
+
return getScrollbarSlotProps(
|
|
993
|
+
'horizontal',
|
|
994
|
+
renderedWidth.value,
|
|
995
|
+
displayScrollOffset.x,
|
|
996
|
+
displayViewportSize.width,
|
|
997
|
+
handleHorizontalScrollbarScrollToOffset,
|
|
998
|
+
horizontalScrollbar,
|
|
999
|
+
);
|
|
922
1000
|
});
|
|
923
1001
|
|
|
924
1002
|
const wrapperStyle = computed(() => {
|
|
@@ -1169,11 +1247,25 @@ defineExpose({
|
|
|
1169
1247
|
*/
|
|
1170
1248
|
getItemSize,
|
|
1171
1249
|
|
|
1250
|
+
/**
|
|
1251
|
+
* Helper to get the row (or item) index at a specific vertical (or horizontal in horizontal mode) virtual offset (VU).
|
|
1252
|
+
* @param offset - The virtual pixel offset.
|
|
1253
|
+
* @see useVirtualScroll
|
|
1254
|
+
*/
|
|
1255
|
+
getRowIndexAt,
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Helper to get the column index at a specific horizontal virtual offset (VU).
|
|
1259
|
+
* @param offset - The virtual pixel offset.
|
|
1260
|
+
* @see useVirtualScroll
|
|
1261
|
+
*/
|
|
1262
|
+
getColIndexAt,
|
|
1263
|
+
|
|
1172
1264
|
/**
|
|
1173
1265
|
* Programmatically scroll to a specific row and/or column.
|
|
1174
1266
|
*
|
|
1175
|
-
* @param rowIndex - The row index to scroll to. Pass null to only scroll horizontally.
|
|
1176
|
-
* @param colIndex - The column index to scroll to. Pass null to only scroll vertically.
|
|
1267
|
+
* @param rowIndex - The row index to scroll to. Pass null to only scroll horizontally. Optional.
|
|
1268
|
+
* @param colIndex - The column index to scroll to. Pass null to only scroll vertically. Optional.
|
|
1177
1269
|
* @param options - Alignment and behavior options. Defaults to { align: 'auto', behavior: 'auto' }.
|
|
1178
1270
|
* @see ScrollAlignment
|
|
1179
1271
|
* @see ScrollToIndexOptions
|