Vulture/VApp/node_modules/vuetify/lib/composables/virtual.mjs

238 lines
7.6 KiB
JavaScript

// Composables
import { useDisplay } from "./display.mjs";
import { useResizeObserver } from "./resizeObserver.mjs"; // Utilities
import { computed, nextTick, onScopeDispose, ref, shallowRef, watch, watchEffect } from 'vue';
import { clamp, debounce, IN_BROWSER, propsFactory } from "../util/index.mjs"; // Types
const UP = -1;
const DOWN = 1;
/** Determines how large each batch of items should be */
const BUFFER_PX = 100;
export const makeVirtualProps = propsFactory({
itemHeight: {
type: [Number, String],
default: null
},
height: [Number, String]
}, 'virtual');
export function useVirtual(props, items) {
const display = useDisplay();
const itemHeight = shallowRef(0);
watchEffect(() => {
itemHeight.value = parseFloat(props.itemHeight || 0);
});
const first = shallowRef(0);
const last = shallowRef(Math.ceil(
// Assume 16px items filling the entire screen height if
// not provided. This is probably incorrect but it minimises
// the chance of ending up with empty space at the bottom.
// The default value is set here to avoid poisoning getSize()
(parseInt(props.height) || display.height.value) / (itemHeight.value || 16)) || 1);
const paddingTop = shallowRef(0);
const paddingBottom = shallowRef(0);
/** The scrollable element */
const containerRef = ref();
/** An element marking the top of the scrollable area,
* used to add an offset if there's padding or other elements above the virtual list */
const markerRef = ref();
/** markerRef's offsetTop, lazily evaluated */
let markerOffset = 0;
const {
resizeRef,
contentRect
} = useResizeObserver();
watchEffect(() => {
resizeRef.value = containerRef.value;
});
const viewportHeight = computed(() => {
return containerRef.value === document.documentElement ? display.height.value : contentRect.value?.height || parseInt(props.height) || 0;
});
/** All static elements have been rendered and we have an assumed item height */
const hasInitialRender = computed(() => {
return !!(containerRef.value && markerRef.value && viewportHeight.value && itemHeight.value);
});
let sizes = Array.from({
length: items.value.length
});
let offsets = Array.from({
length: items.value.length
});
const updateTime = shallowRef(0);
let targetScrollIndex = -1;
function getSize(index) {
return sizes[index] || itemHeight.value;
}
const updateOffsets = debounce(() => {
const start = performance.now();
offsets[0] = 0;
const length = items.value.length;
for (let i = 1; i <= length - 1; i++) {
offsets[i] = (offsets[i - 1] || 0) + getSize(i - 1);
}
updateTime.value = Math.max(updateTime.value, performance.now() - start);
}, updateTime);
const unwatch = watch(hasInitialRender, v => {
if (!v) return;
// First render is complete, update offsets and visible
// items in case our assumed item height was incorrect
unwatch();
markerOffset = markerRef.value.offsetTop;
updateOffsets.immediate();
calculateVisibleItems();
if (!~targetScrollIndex) return;
nextTick(() => {
IN_BROWSER && window.requestAnimationFrame(() => {
scrollToIndex(targetScrollIndex);
targetScrollIndex = -1;
});
});
});
watch(viewportHeight, (val, oldVal) => {
oldVal && calculateVisibleItems();
});
onScopeDispose(() => {
updateOffsets.clear();
});
function handleItemResize(index, height) {
const prevHeight = sizes[index];
const prevMinHeight = itemHeight.value;
itemHeight.value = prevMinHeight ? Math.min(itemHeight.value, height) : height;
if (prevHeight !== height || prevMinHeight !== itemHeight.value) {
sizes[index] = height;
updateOffsets();
}
}
function calculateOffset(index) {
index = clamp(index, 0, items.value.length - 1);
return offsets[index] || 0;
}
function calculateIndex(scrollTop) {
return binaryClosest(offsets, scrollTop);
}
let lastScrollTop = 0;
let scrollVelocity = 0;
let lastScrollTime = 0;
function handleScroll() {
if (!containerRef.value || !markerRef.value) return;
const scrollTop = containerRef.value.scrollTop;
const scrollTime = performance.now();
const scrollDeltaT = scrollTime - lastScrollTime;
if (scrollDeltaT > 500) {
scrollVelocity = Math.sign(scrollTop - lastScrollTop);
// Not super important, only update at the
// start of a scroll sequence to avoid reflows
markerOffset = markerRef.value.offsetTop;
} else {
scrollVelocity = scrollTop - lastScrollTop;
}
lastScrollTop = scrollTop;
lastScrollTime = scrollTime;
calculateVisibleItems();
}
function handleScrollend() {
if (!containerRef.value || !markerRef.value) return;
scrollVelocity = 0;
lastScrollTime = 0;
calculateVisibleItems();
}
let raf = -1;
function calculateVisibleItems() {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(_calculateVisibleItems);
}
function _calculateVisibleItems() {
if (!containerRef.value || !viewportHeight.value) return;
const scrollTop = lastScrollTop - markerOffset;
const direction = Math.sign(scrollVelocity);
const startPx = Math.max(0, scrollTop - BUFFER_PX);
const start = clamp(calculateIndex(startPx), 0, items.value.length);
const endPx = scrollTop + viewportHeight.value + BUFFER_PX;
const end = clamp(calculateIndex(endPx) + 1, start + 1, items.value.length);
if (
// Only update the side we're scrolling towards,
// the other side will be updated incidentally
(direction !== UP || start < first.value) && (direction !== DOWN || end > last.value)) {
const topOverflow = calculateOffset(first.value) - calculateOffset(start);
const bottomOverflow = calculateOffset(end) - calculateOffset(last.value);
const bufferOverflow = Math.max(topOverflow, bottomOverflow);
if (bufferOverflow > BUFFER_PX) {
first.value = start;
last.value = end;
} else {
// Only update the side that's reached its limit if there's still buffer left
if (start <= 0) first.value = start;
if (end >= items.value.length) last.value = end;
}
}
paddingTop.value = calculateOffset(first.value);
paddingBottom.value = calculateOffset(items.value.length) - calculateOffset(last.value);
}
function scrollToIndex(index) {
const offset = calculateOffset(index);
if (!containerRef.value || index && !offset) {
targetScrollIndex = index;
} else {
containerRef.value.scrollTop = offset;
}
}
const computedItems = computed(() => {
return items.value.slice(first.value, last.value).map((item, index) => ({
raw: item,
index: index + first.value
}));
});
watch(items, () => {
sizes = Array.from({
length: items.value.length
});
offsets = Array.from({
length: items.value.length
});
updateOffsets.immediate();
calculateVisibleItems();
}, {
deep: true
});
return {
containerRef,
markerRef,
computedItems,
paddingTop,
paddingBottom,
scrollToIndex,
handleScroll,
handleScrollend,
handleItemResize
};
}
// https://gist.github.com/robertleeplummerjr/1cc657191d34ecd0a324
function binaryClosest(arr, val) {
let high = arr.length - 1;
let low = 0;
let mid = 0;
let item = null;
let target = -1;
if (arr[high] < val) {
return high;
}
while (low <= high) {
mid = low + high >> 1;
item = arr[mid];
if (item > val) {
high = mid - 1;
} else if (item < val) {
target = mid;
low = mid + 1;
} else if (item === val) {
return mid;
} else {
return low;
}
}
return target;
}
//# sourceMappingURL=virtual.mjs.map