virtual.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. // Composables
  2. import { useDisplay } from "./display.mjs";
  3. import { useResizeObserver } from "./resizeObserver.mjs"; // Utilities
  4. import { computed, ref, shallowRef, watch, watchEffect } from 'vue';
  5. import { clamp, createRange, propsFactory } from "../util/index.mjs"; // Types
  6. const UP = -1;
  7. const DOWN = 1;
  8. export const makeVirtualProps = propsFactory({
  9. itemHeight: {
  10. type: [Number, String],
  11. default: 48
  12. }
  13. }, 'virtual');
  14. export function useVirtual(props, items, offset) {
  15. const first = shallowRef(0);
  16. const baseItemHeight = shallowRef(props.itemHeight);
  17. const itemHeight = computed({
  18. get: () => parseInt(baseItemHeight.value ?? 0, 10),
  19. set(val) {
  20. baseItemHeight.value = val;
  21. }
  22. });
  23. const containerRef = ref();
  24. const {
  25. resizeRef,
  26. contentRect
  27. } = useResizeObserver();
  28. watchEffect(() => {
  29. resizeRef.value = containerRef.value;
  30. });
  31. const display = useDisplay();
  32. const sizeMap = new Map();
  33. let sizes = Array.from({
  34. length: items.value.length
  35. });
  36. const visibleItems = computed(() => {
  37. const height = (!contentRect.value || containerRef.value === document.documentElement ? display.height.value : contentRect.value.height) - (offset?.value ?? 0);
  38. return Math.ceil(height / itemHeight.value * 1.7 + 1);
  39. });
  40. function handleItemResize(index, height) {
  41. itemHeight.value = Math.max(itemHeight.value, height);
  42. sizes[index] = height;
  43. sizeMap.set(items.value[index], height);
  44. }
  45. function calculateOffset(index) {
  46. return sizes.slice(0, index).reduce((acc, val) => acc + (val || itemHeight.value), 0);
  47. }
  48. function calculateMidPointIndex(scrollTop) {
  49. const end = items.value.length;
  50. let middle = 0;
  51. let middleOffset = 0;
  52. while (middleOffset < scrollTop && middle < end) {
  53. middleOffset += sizes[middle++] || itemHeight.value;
  54. }
  55. return middle - 1;
  56. }
  57. let lastScrollTop = 0;
  58. function handleScroll() {
  59. if (!containerRef.value || !contentRect.value) return;
  60. const height = contentRect.value.height - 56;
  61. const scrollTop = containerRef.value.scrollTop;
  62. const direction = scrollTop < lastScrollTop ? UP : DOWN;
  63. const midPointIndex = calculateMidPointIndex(scrollTop + height / 2);
  64. const buffer = Math.round(visibleItems.value / 3);
  65. const firstIndex = midPointIndex - buffer;
  66. const lastIndex = first.value + buffer * 2 - 1;
  67. if (direction === UP && midPointIndex <= lastIndex) {
  68. first.value = clamp(firstIndex, 0, items.value.length);
  69. } else if (direction === DOWN && midPointIndex >= lastIndex) {
  70. first.value = clamp(firstIndex, 0, items.value.length - visibleItems.value);
  71. }
  72. lastScrollTop = scrollTop;
  73. }
  74. function scrollToIndex(index) {
  75. if (!containerRef.value) return;
  76. const offset = calculateOffset(index);
  77. containerRef.value.scrollTop = offset;
  78. }
  79. const last = computed(() => Math.min(items.value.length, first.value + visibleItems.value));
  80. const computedItems = computed(() => {
  81. return items.value.slice(first.value, last.value).map((item, index) => ({
  82. raw: item,
  83. index: index + first.value
  84. }));
  85. });
  86. const paddingTop = computed(() => calculateOffset(first.value));
  87. const paddingBottom = computed(() => calculateOffset(items.value.length) - calculateOffset(last.value));
  88. watch(() => items.value.length, () => {
  89. sizes = createRange(items.value.length).map(() => itemHeight.value);
  90. sizeMap.forEach((height, item) => {
  91. const index = items.value.indexOf(item);
  92. if (index === -1) {
  93. sizeMap.delete(item);
  94. } else {
  95. sizes[index] = height;
  96. }
  97. });
  98. });
  99. return {
  100. containerRef,
  101. computedItems,
  102. itemHeight,
  103. paddingTop,
  104. paddingBottom,
  105. scrollToIndex,
  106. handleScroll,
  107. handleItemResize
  108. };
  109. }
  110. //# sourceMappingURL=virtual.mjs.map