UNPKG

@indielayer/ui

Version:

Indielayer UI Components with Tailwind CSS build for Vue 3

290 lines (231 loc) 8.12 kB
/** * @file useVirtualList composable adapted from https://github.com/vueuse/vueuse/blob/main/packages/core/useVirtualList/index.ts */ import { computed, ref, shallowRef, unref, watch } from 'vue' import type { Ref, ShallowRef, StyleValue } from 'vue' import type { MaybeRef } from '@vueuse/shared' import { useElementSize } from '@vueuse/core' type UseVirtualListItemSize = number | ((index: number) => number) export type UseVirtualListOptions = { /** * item height, accept a pixel value or a function that returns the height * * @default 0 */ disabled?: boolean; itemHeight: UseVirtualListItemSize; topOffset: number; bottomOffset: number; /** * the extra buffer items outside of the view area * * @default 5 */ overscan?: number; } export interface UseVirtualListItem<T> { data: T; index: number; } export interface UseVirtualListReturn<T> { list: Ref<UseVirtualListItem<T>[]>; scrollTo: (index: number) => void; reset: () => void; containerProps: { ref: Ref<HTMLElement | null>; style: StyleValue; onScroll: () => void; }; wrapperProps: MaybeRef<{ style?: { width: string; height: string; marginTop: string; } | { width: string; height: string; marginLeft: string; display: string; }; }>; } export function useVirtualList<T = any>(list: MaybeRef<T[]>, options: UseVirtualListOptions): UseVirtualListReturn<T> { if (options.disabled) { return { list: computed(() => unref(list).map((data, index) => ({ data, index }))), scrollTo: () => {}, reset: () => {}, containerProps: { ref: ref(null), style: {}, onScroll: () => {}, }, wrapperProps: {}, } } const { containerStyle, wrapperProps, scrollTo, calculateRange, currentList, containerRef } = useVerticalVirtualList(options, list) return { list: currentList, scrollTo, reset: calculateRange, containerProps: { ref: containerRef, onScroll: () => { calculateRange() }, style: containerStyle, }, wrapperProps, } } type UseVirtualListContainerRef = Ref<HTMLElement | null> interface UseVirtualElementSizes { width: Ref<number>; height: Ref<number>; } type UseVirtualListArray<T> = UseVirtualListItem<T>[] type UseVirtualListRefArray<T> = Ref<UseVirtualListArray<T>> type UseVirtualListSource<T> = Ref<T[]> | ShallowRef<T[]> interface UseVirtualListState { start: number; end: number; } type RefState = Ref<UseVirtualListState> interface UseVirtualListResources<T> { state: RefState; source: UseVirtualListSource<T>; currentList: UseVirtualListRefArray<T>; size: UseVirtualElementSizes; containerRef: UseVirtualListContainerRef; } function useVirtualListResources<T>(list: MaybeRef<T[]>): UseVirtualListResources<T> { const containerRef = ref<HTMLElement | null>(null) const size = useElementSize(containerRef) const currentList: Ref<UseVirtualListItem<T>[]> = ref([]) const source = shallowRef(list) const state: Ref<{ start: number; end: number; }> = ref({ start: 0, end: 10 }) return { state, source, currentList, size, containerRef } } function createGetViewCapacity<T>(state: UseVirtualListResources<T>['state'], source: UseVirtualListResources<T>['source'], itemSize: UseVirtualListItemSize) { return (containerSize: number) => { if (typeof itemSize === 'number') return Math.ceil(containerSize / itemSize) const { start = 0 } = state.value let sum = 0 let capacity = 0 for (let i = start; i < source.value.length; i++) { const size = itemSize(i) sum += size capacity = i if (sum > containerSize) break } return capacity - start } } function createGetOffset<T>(source: UseVirtualListResources<T>['source'], itemSize: UseVirtualListItemSize) { return (scrollDirection: number) => { if (typeof itemSize === 'number') return Math.floor(scrollDirection / itemSize) + 1 let sum = 0 let offset = 0 for (let i = 0; i < source.value.length; i++) { const size = itemSize(i) sum += size if (sum >= scrollDirection) { offset = i break } } return offset + 1 } } function createCalculateRange<T>(type: 'horizontal' | 'vertical', overscan: number, getOffset: ReturnType<typeof createGetOffset>, getViewCapacity: ReturnType<typeof createGetViewCapacity>, { containerRef, state, currentList, source }: UseVirtualListResources<T>) { return () => { const element = containerRef.value if (element) { const offset = getOffset(type === 'vertical' ? element.scrollTop : element.scrollLeft) const viewCapacity = getViewCapacity(type === 'vertical' ? element.clientHeight : element.clientWidth) const from = offset - overscan const to = offset + viewCapacity + overscan state.value = { start: from < 0 ? 0 : from, end: to > source.value.length ? source.value.length : to, } currentList.value = source.value .slice(state.value.start, state.value.end) .map((ele, index) => ({ data: ele, index: index + state.value.start, })) } } } function createGetDistance<T>(itemSize: UseVirtualListItemSize, source: UseVirtualListResources<T>['source']) { return (index: number) => { if (typeof itemSize === 'number') { const size = index * itemSize return size } const size = source.value .slice(0, index) .reduce((sum, _, i) => sum + itemSize(i), 0) return size } } function useWatchForSizes<T>(size: UseVirtualElementSizes, list: MaybeRef<T[]>, containerRef: Ref<HTMLElement | null>, calculateRange: () => void) { watch([size.width, size.height, list, containerRef], () => { calculateRange() }) } function createComputedTotalSize<T>(itemSize: UseVirtualListItemSize, source: UseVirtualListResources<T>['source']) { return computed(() => { if (typeof itemSize === 'number') return source.value.length * itemSize return source.value.reduce((sum, _, index) => sum + itemSize(index), 0) }) } const scrollToDictionaryForElementScrollKey = { horizontal: 'scrollLeft', vertical: 'scrollTop', } as const function createScrollTo<T>(type: 'horizontal' | 'vertical', calculateRange: () => void, getDistance: ReturnType<typeof createGetDistance>, containerRef: UseVirtualListResources<T>['containerRef']) { return (index: number) => { if (containerRef.value) { containerRef.value[scrollToDictionaryForElementScrollKey[type]] = getDistance(index) calculateRange() } } } function useVerticalVirtualList<T>(options: UseVirtualListOptions, list: MaybeRef<T[]>) { const resources = useVirtualListResources(list) const { state, source, currentList, size, containerRef } = resources const containerStyle: StyleValue = { overflowY: 'auto' } const { itemHeight, topOffset = 0, bottomOffset = 0, overscan = 5 } = options const getViewCapacity = createGetViewCapacity(state, source, itemHeight) const getOffset = createGetOffset(source, itemHeight) const calculateRange = createCalculateRange('vertical', overscan, getOffset, getViewCapacity, resources) const getDistanceTop = createGetDistance(itemHeight, source) const offsetTop = computed(() => getDistanceTop(state.value.start)) const totalHeight = createComputedTotalSize(itemHeight, source) const scrollTo = createScrollTo('vertical', calculateRange, getDistanceTop, containerRef) useWatchForSizes(size, source, containerRef, calculateRange) const wrapperProps = computed(() => { const total = totalHeight.value + topOffset + bottomOffset const offTop = offsetTop.value > total ? total : offsetTop.value return { style: { width: '100%', height: `${totalHeight.value - offTop + topOffset + bottomOffset}px`, marginTop: `${offTop}px`, }, } }) return { calculateRange, scrollTo, containerStyle, wrapperProps, currentList, containerRef, } }