useVirtualTreeList.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { computed, shallowRef, toValue } from "vue";
  2. import type { MaybeRefOrGetter } from "vue";
  3. export interface UseVirtualTreeListOptions<T> {
  4. items: MaybeRefOrGetter<readonly T[]>;
  5. virtual: MaybeRefOrGetter<boolean | undefined>;
  6. itemHeight: MaybeRefOrGetter<number | undefined>;
  7. height: MaybeRefOrGetter<number | undefined>;
  8. overscan: MaybeRefOrGetter<number | undefined>;
  9. }
  10. export interface UniTreeVirtualScrollEvent {
  11. detail?: {
  12. scrollTop?: number;
  13. };
  14. }
  15. export function useVirtualTreeList<T>(options: UseVirtualTreeListOptions<T>) {
  16. const scrollTop = shallowRef(0);
  17. const itemHeight = computed(() => normalizePositiveNumber(options.itemHeight));
  18. const height = computed(() => normalizePositiveNumber(options.height));
  19. const virtualEnabled = computed(() => {
  20. return Boolean(toValue(options.virtual) && itemHeight.value > 0 && height.value > 0);
  21. });
  22. const overscan = computed(() => {
  23. const value = Number(toValue(options.overscan));
  24. return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
  25. });
  26. const totalCount = computed(() => toValue(options.items).length);
  27. const startIndex = computed(() => {
  28. if (!virtualEnabled.value || totalCount.value === 0) {
  29. return 0;
  30. }
  31. const rawStart = Math.floor(scrollTop.value / itemHeight.value) - overscan.value;
  32. return Math.min(Math.max(0, rawStart), totalCount.value - 1);
  33. });
  34. const endIndex = computed(() => {
  35. if (!virtualEnabled.value) {
  36. return totalCount.value;
  37. }
  38. const visibleCount = Math.ceil(height.value / itemHeight.value) + overscan.value * 2;
  39. return Math.min(totalCount.value, startIndex.value + visibleCount);
  40. });
  41. const renderedItems = computed(() => {
  42. const items = toValue(options.items);
  43. if (!virtualEnabled.value) {
  44. return items;
  45. }
  46. return items.slice(startIndex.value, endIndex.value);
  47. });
  48. const topPadding = computed(() => {
  49. return virtualEnabled.value ? startIndex.value * itemHeight.value : 0;
  50. });
  51. const bottomPadding = computed(() => {
  52. if (!virtualEnabled.value) {
  53. return 0;
  54. }
  55. return Math.max(0, (totalCount.value - endIndex.value) * itemHeight.value);
  56. });
  57. const scrollViewStyle = computed(() => {
  58. if (!virtualEnabled.value) {
  59. return undefined;
  60. }
  61. return {
  62. height: `${height.value}px`
  63. };
  64. });
  65. function handleScroll(event: UniTreeVirtualScrollEvent) {
  66. if (!virtualEnabled.value) {
  67. return;
  68. }
  69. const nextScrollTop = Number(event.detail?.scrollTop ?? 0);
  70. scrollTop.value = Number.isFinite(nextScrollTop) ? Math.max(0, nextScrollTop) : 0;
  71. }
  72. function scrollToIndex(index: number) {
  73. if (!virtualEnabled.value || totalCount.value === 0) {
  74. return false;
  75. }
  76. const normalizedIndex = Math.min(Math.max(0, Math.floor(index)), totalCount.value - 1);
  77. scrollTop.value = normalizedIndex * itemHeight.value;
  78. return true;
  79. }
  80. return {
  81. scrollTop,
  82. virtualEnabled,
  83. startIndex,
  84. endIndex,
  85. renderedItems,
  86. topPadding,
  87. bottomPadding,
  88. scrollViewStyle,
  89. handleScroll,
  90. scrollToIndex
  91. };
  92. }
  93. function normalizePositiveNumber(value: MaybeRefOrGetter<number | undefined>) {
  94. const numericValue = Number(toValue(value));
  95. return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : 0;
  96. }