@pdanpdan/virtual-scroll 0.2.1 → 0.3.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/README.md +182 -88
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +100 -35
- package/dist/index.js +1 -844
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +902 -0
- package/dist/index.mjs.map +1 -0
- package/dist/virtual-scroll.css +2 -0
- package/package.json +8 -5
- package/src/components/VirtualScroll.test.ts +62 -15
- package/src/components/VirtualScroll.vue +83 -19
- package/src/composables/useVirtualScroll.test.ts +501 -68
- package/src/composables/useVirtualScroll.ts +150 -65
- package/dist/index.css +0 -2
|
@@ -6,10 +6,14 @@ import type {
|
|
|
6
6
|
ScrollDetails,
|
|
7
7
|
VirtualScrollProps,
|
|
8
8
|
} from '../composables/useVirtualScroll';
|
|
9
|
+
import type { VNodeChild } from 'vue';
|
|
9
10
|
|
|
10
|
-
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
11
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_ITEM_SIZE,
|
|
15
|
+
useVirtualScroll,
|
|
16
|
+
} from '../composables/useVirtualScroll';
|
|
13
17
|
import { getPaddingX, getPaddingY } from '../utils/scroll';
|
|
14
18
|
|
|
15
19
|
export interface Props<T = unknown> {
|
|
@@ -89,7 +93,7 @@ const props = withDefaults(defineProps<Props<T>>(), {
|
|
|
89
93
|
gap: 0,
|
|
90
94
|
columnGap: 0,
|
|
91
95
|
stickyIndices: () => [],
|
|
92
|
-
loadDistance:
|
|
96
|
+
loadDistance: 200,
|
|
93
97
|
loading: false,
|
|
94
98
|
restoreScrollOnPrepend: false,
|
|
95
99
|
debug: false,
|
|
@@ -101,7 +105,29 @@ const emit = defineEmits<{
|
|
|
101
105
|
(e: 'visibleRangeChange', range: { start: number; end: number; colStart: number; colEnd: number; }): void;
|
|
102
106
|
}>();
|
|
103
107
|
|
|
104
|
-
const
|
|
108
|
+
const slots = defineSlots<{
|
|
109
|
+
/** Content rendered at the top of the scrollable area. Can be made sticky. */
|
|
110
|
+
header?: (props: Record<string, never>) => VNodeChild;
|
|
111
|
+
/** Slot for rendering each individual item. */
|
|
112
|
+
item?: (props: {
|
|
113
|
+
/** The data item being rendered. */
|
|
114
|
+
item: T;
|
|
115
|
+
/** The index of the item in the items array. */
|
|
116
|
+
index: number;
|
|
117
|
+
/** The current visible range of columns (for grid mode). */
|
|
118
|
+
columnRange: { start: number; end: number; padStart: number; padEnd: number; };
|
|
119
|
+
/** Function to get the width of a specific column. */
|
|
120
|
+
getColumnWidth: (index: number) => number;
|
|
121
|
+
/** Whether this item is configured to be sticky. */
|
|
122
|
+
isSticky?: boolean | undefined;
|
|
123
|
+
/** Whether this item is currently in a sticky state. */
|
|
124
|
+
isStickyActive?: boolean | undefined;
|
|
125
|
+
}) => VNodeChild;
|
|
126
|
+
/** Content shown when `loading` prop is true. */
|
|
127
|
+
loading?: (props: Record<string, never>) => VNodeChild;
|
|
128
|
+
/** Content rendered at the bottom of the scrollable area. Can be made sticky. */
|
|
129
|
+
footer?: (props: Record<string, never>) => VNodeChild;
|
|
130
|
+
}>();
|
|
105
131
|
|
|
106
132
|
const hostRef = ref<HTMLElement | null>(null);
|
|
107
133
|
const wrapperRef = ref<HTMLElement | null>(null);
|
|
@@ -172,7 +198,7 @@ const virtualScrollProps = computed(() => {
|
|
|
172
198
|
initialScrollAlign: props.initialScrollAlign,
|
|
173
199
|
defaultItemSize: props.defaultItemSize,
|
|
174
200
|
defaultColumnWidth: props.defaultColumnWidth,
|
|
175
|
-
debug:
|
|
201
|
+
debug: props.debug,
|
|
176
202
|
} as VirtualScrollProps<T>;
|
|
177
203
|
});
|
|
178
204
|
|
|
@@ -188,10 +214,37 @@ const {
|
|
|
188
214
|
scrollToOffset,
|
|
189
215
|
updateHostOffset,
|
|
190
216
|
updateItemSizes,
|
|
191
|
-
refresh,
|
|
217
|
+
refresh: coreRefresh,
|
|
192
218
|
stopProgrammaticScroll,
|
|
193
219
|
} = useVirtualScroll(virtualScrollProps);
|
|
194
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Resets all dynamic measurements and re-initializes from props.
|
|
223
|
+
* Also triggers manual re-measurement of all currently rendered items.
|
|
224
|
+
*/
|
|
225
|
+
function refresh() {
|
|
226
|
+
coreRefresh();
|
|
227
|
+
nextTick(() => {
|
|
228
|
+
const updates: { index: number; inlineSize: number; blockSize: number; element?: HTMLElement; }[] = [];
|
|
229
|
+
|
|
230
|
+
for (const [ index, el ] of itemRefs.entries()) {
|
|
231
|
+
/* v8 ignore else -- @preserve */
|
|
232
|
+
if (el) {
|
|
233
|
+
updates.push({
|
|
234
|
+
index,
|
|
235
|
+
inlineSize: el.offsetWidth,
|
|
236
|
+
blockSize: el.offsetHeight,
|
|
237
|
+
element: el,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (updates.length > 0) {
|
|
243
|
+
updateItemSizes(updates);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
195
248
|
// Watch for scroll details and emit event
|
|
196
249
|
watch(scrollDetails, (details, oldDetails) => {
|
|
197
250
|
if (!isHydrated.value) {
|
|
@@ -412,22 +465,22 @@ function handleKeyDown(event: KeyboardEvent) {
|
|
|
412
465
|
}
|
|
413
466
|
if (event.key === 'ArrowUp') {
|
|
414
467
|
event.preventDefault();
|
|
415
|
-
scrollToOffset(null, scrollOffset.y -
|
|
468
|
+
scrollToOffset(null, scrollOffset.y - DEFAULT_ITEM_SIZE);
|
|
416
469
|
return;
|
|
417
470
|
}
|
|
418
471
|
if (event.key === 'ArrowDown') {
|
|
419
472
|
event.preventDefault();
|
|
420
|
-
scrollToOffset(null, scrollOffset.y +
|
|
473
|
+
scrollToOffset(null, scrollOffset.y + DEFAULT_ITEM_SIZE);
|
|
421
474
|
return;
|
|
422
475
|
}
|
|
423
476
|
if (event.key === 'ArrowLeft') {
|
|
424
477
|
event.preventDefault();
|
|
425
|
-
scrollToOffset(scrollOffset.x -
|
|
478
|
+
scrollToOffset(scrollOffset.x - DEFAULT_ITEM_SIZE, null);
|
|
426
479
|
return;
|
|
427
480
|
}
|
|
428
481
|
if (event.key === 'ArrowRight') {
|
|
429
482
|
event.preventDefault();
|
|
430
|
-
scrollToOffset(scrollOffset.x +
|
|
483
|
+
scrollToOffset(scrollOffset.x + DEFAULT_ITEM_SIZE, null);
|
|
431
484
|
return;
|
|
432
485
|
}
|
|
433
486
|
if (event.key === 'PageUp') {
|
|
@@ -526,10 +579,10 @@ function getItemStyle(item: RenderedItem<T>) {
|
|
|
526
579
|
|
|
527
580
|
if (isDynamic) {
|
|
528
581
|
if (!isVertical) {
|
|
529
|
-
style.minInlineSize =
|
|
582
|
+
style.minInlineSize = '1px';
|
|
530
583
|
}
|
|
531
584
|
if (!isHorizontal) {
|
|
532
|
-
style.minBlockSize =
|
|
585
|
+
style.minBlockSize = '1px';
|
|
533
586
|
}
|
|
534
587
|
}
|
|
535
588
|
|
|
@@ -552,6 +605,11 @@ function getItemStyle(item: RenderedItem<T>) {
|
|
|
552
605
|
return style;
|
|
553
606
|
}
|
|
554
607
|
|
|
608
|
+
const isDebug = computed(() => props.debug);
|
|
609
|
+
const isTable = computed(() => props.containerTag === 'table');
|
|
610
|
+
const headerTag = computed(() => isTable.value ? 'thead' : 'div');
|
|
611
|
+
const footerTag = computed(() => isTable.value ? 'tfoot' : 'div');
|
|
612
|
+
|
|
555
613
|
defineExpose({
|
|
556
614
|
scrollDetails,
|
|
557
615
|
columnRange,
|
|
@@ -573,7 +631,7 @@ defineExpose({
|
|
|
573
631
|
{
|
|
574
632
|
'virtual-scroll--hydrated': isHydrated,
|
|
575
633
|
'virtual-scroll--window': isWindowContainer,
|
|
576
|
-
'virtual-scroll--table':
|
|
634
|
+
'virtual-scroll--table': isTable,
|
|
577
635
|
},
|
|
578
636
|
]"
|
|
579
637
|
:style="containerStyle"
|
|
@@ -583,15 +641,17 @@ defineExpose({
|
|
|
583
641
|
@pointerdown.passive="stopProgrammaticScroll"
|
|
584
642
|
@touchstart.passive="stopProgrammaticScroll"
|
|
585
643
|
>
|
|
644
|
+
<!-- v8 ignore start -->
|
|
586
645
|
<component
|
|
587
|
-
:is="
|
|
588
|
-
v-if="
|
|
646
|
+
:is="headerTag"
|
|
647
|
+
v-if="slots.header"
|
|
589
648
|
ref="headerRef"
|
|
590
649
|
class="virtual-scroll-header"
|
|
591
650
|
:class="{ 'virtual-scroll--sticky': stickyHeader }"
|
|
592
651
|
>
|
|
593
652
|
<slot name="header" />
|
|
594
653
|
</component>
|
|
654
|
+
<!-- v8 ignore stop -->
|
|
595
655
|
|
|
596
656
|
<component
|
|
597
657
|
:is="wrapperTag"
|
|
@@ -600,14 +660,16 @@ defineExpose({
|
|
|
600
660
|
:style="wrapperStyle"
|
|
601
661
|
>
|
|
602
662
|
<!-- Phantom element to push scroll height -->
|
|
663
|
+
<!-- v8 ignore start -->
|
|
603
664
|
<component
|
|
604
665
|
:is="itemTag"
|
|
605
|
-
v-if="
|
|
666
|
+
v-if="isTable"
|
|
606
667
|
class="virtual-scroll-spacer"
|
|
607
668
|
:style="spacerStyle"
|
|
608
669
|
>
|
|
609
670
|
<td style="padding: 0; border: none; block-size: inherit;" />
|
|
610
671
|
</component>
|
|
672
|
+
<!-- v8 ignore stop -->
|
|
611
673
|
|
|
612
674
|
<component
|
|
613
675
|
:is="itemTag"
|
|
@@ -637,8 +699,9 @@ defineExpose({
|
|
|
637
699
|
</component>
|
|
638
700
|
</component>
|
|
639
701
|
|
|
702
|
+
<!-- v8 ignore start -->
|
|
640
703
|
<div
|
|
641
|
-
v-if="loading &&
|
|
704
|
+
v-if="loading && slots.loading"
|
|
642
705
|
class="virtual-scroll-loading"
|
|
643
706
|
:style="loadingStyle"
|
|
644
707
|
>
|
|
@@ -646,14 +709,15 @@ defineExpose({
|
|
|
646
709
|
</div>
|
|
647
710
|
|
|
648
711
|
<component
|
|
649
|
-
:is="
|
|
650
|
-
v-if="
|
|
712
|
+
:is="footerTag"
|
|
713
|
+
v-if="slots.footer"
|
|
651
714
|
ref="footerRef"
|
|
652
715
|
class="virtual-scroll-footer"
|
|
653
716
|
:class="{ 'virtual-scroll--sticky': stickyFooter }"
|
|
654
717
|
>
|
|
655
718
|
<slot name="footer" />
|
|
656
719
|
</component>
|
|
720
|
+
<!-- v8 ignore stop -->
|
|
657
721
|
</component>
|
|
658
722
|
</template>
|
|
659
723
|
|