vuetify
Version:
Vue Material Component Framework
145 lines (144 loc) • 5.43 kB
JavaScript
import { createVNode as _createVNode } from "vue";
// Styles
import "./VVirtualScroll.css";
// Components
import { VVirtualScrollItem } from "./VVirtualScrollItem.mjs"; // Composables
import { makeDimensionProps, useDimension } from "../../composables/dimensions.mjs";
import { useDisplay } from "../../composables/display.mjs";
import { useResizeObserver } from "../../composables/resizeObserver.mjs"; // Utilities
import { computed, onMounted, ref, watch, watchEffect } from 'vue';
import { clamp, convertToUnit, createRange, genericComponent, useRender } from "../../util/index.mjs"; // Types
const UP = -1;
const DOWN = 1;
export const VVirtualScroll = genericComponent()({
name: 'VVirtualScroll',
props: {
items: {
type: Array,
default: () => []
},
itemHeight: [Number, String],
visibleItems: [Number, String],
...makeDimensionProps()
},
setup(props, _ref) {
let {
slots
} = _ref;
const first = ref(0);
const baseItemHeight = ref(props.itemHeight);
const itemHeight = computed({
get: () => parseInt(baseItemHeight.value ?? 0, 10),
set(val) {
baseItemHeight.value = val;
}
});
const rootEl = ref();
const {
resizeRef,
contentRect
} = useResizeObserver();
watchEffect(() => {
resizeRef.value = rootEl.value;
});
const display = useDisplay();
const sizeMap = new Map();
let sizes = createRange(props.items.length).map(() => itemHeight.value);
const visibleItems = computed(() => {
return props.visibleItems ? parseInt(props.visibleItems, 10) : Math.max(12, Math.ceil((contentRect.value?.height ?? display.height.value) / itemHeight.value * 1.7 + 1));
});
function handleItemResize(index, height) {
itemHeight.value = Math.max(itemHeight.value, height);
sizes[index] = height;
sizeMap.set(props.items[index], height);
}
function calculateOffset(index) {
return sizes.slice(0, index).reduce((curr, value) => curr + (value || itemHeight.value), 0);
}
function calculateMidPointIndex(scrollTop) {
let start = 0;
let end = props.items.length;
while (start <= end) {
const middle = start + Math.floor((end - start) / 2);
const middleOffset = calculateOffset(middle);
if (middleOffset === scrollTop) {
return middle;
} else if (middleOffset < scrollTop) {
start = middle + 1;
} else if (middleOffset > scrollTop) {
end = middle - 1;
}
}
return start;
}
let lastScrollTop = 0;
function handleScroll() {
if (!rootEl.value || !contentRect.value) return;
const height = contentRect.value.height;
const scrollTop = rootEl.value.scrollTop;
const direction = scrollTop < lastScrollTop ? UP : DOWN;
const midPointIndex = calculateMidPointIndex(scrollTop + height / 2);
const buffer = Math.round(visibleItems.value / 3);
if (direction === UP && midPointIndex <= first.value + buffer * 2 - 1) {
first.value = clamp(midPointIndex - buffer, 0, props.items.length);
} else if (direction === DOWN && midPointIndex >= first.value + buffer * 2 - 1) {
first.value = clamp(midPointIndex - buffer, 0, props.items.length - visibleItems.value);
}
lastScrollTop = rootEl.value.scrollTop;
}
function scrollToIndex(index) {
if (!rootEl.value) return;
const offset = calculateOffset(index);
rootEl.value.scrollTop = offset;
}
const last = computed(() => Math.min(props.items.length, first.value + visibleItems.value));
const computedItems = computed(() => props.items.slice(first.value, last.value));
const paddingTop = computed(() => calculateOffset(first.value));
const paddingBottom = computed(() => calculateOffset(props.items.length) - calculateOffset(last.value));
const {
dimensionStyles
} = useDimension(props);
onMounted(() => {
if (!itemHeight.value) {
// If itemHeight prop is not set, then calculate an estimated height from the average of inital items
itemHeight.value = sizes.slice(first.value, last.value).reduce((curr, height) => curr + height, 0) / visibleItems.value;
}
});
watch(() => props.items.length, () => {
sizes = createRange(props.items.length).map(() => itemHeight.value);
sizeMap.forEach((height, item) => {
const index = props.items.indexOf(item);
if (index === -1) {
sizeMap.delete(item);
} else {
sizes[index] = height;
}
});
});
useRender(() => _createVNode("div", {
"ref": rootEl,
"class": "v-virtual-scroll",
"onScroll": handleScroll,
"style": dimensionStyles.value
}, [_createVNode("div", {
"class": "v-virtual-scroll__container",
"style": {
paddingTop: convertToUnit(paddingTop.value),
paddingBottom: convertToUnit(paddingBottom.value)
}
}, [computedItems.value.map((item, index) => _createVNode(VVirtualScrollItem, {
"key": index,
"dynamicHeight": !props.itemHeight,
"onUpdate:height": height => handleItemResize(index + first.value, height)
}, {
default: () => [slots.default?.({
item,
index: index + first.value
})]
}))])]));
return {
scrollToIndex
};
}
});
//# sourceMappingURL=VVirtualScroll.mjs.map