useCarouselIndicator.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { nextTick, onBeforeUnmount, onMounted, ref, unref, watch } from "vue";
  2. const ITEM_MODE_RATIO = 0.8;
  3. const closestCardIndex = (cards, scrollLeft) => {
  4. const origin = cards[0].offsetLeft;
  5. let closest = 0;
  6. let closestDistance = Infinity;
  7. cards.forEach((card, index) => {
  8. const distance = Math.abs(card.offsetLeft - origin - scrollLeft);
  9. if (distance < closestDistance) {
  10. closestDistance = distance;
  11. closest = index;
  12. }
  13. });
  14. return closest;
  15. };
  16. /**
  17. * @param {Ref<Array>|Function} source - The rendered items (ref or getter)
  18. * @returns {{ trackRef: Ref, activeIndex: Ref<number>, pageCount: Ref<number>, onTrackScroll: Function }}
  19. */
  20. export const useCarouselIndicator = (source) => {
  21. const activeIndex = ref(0);
  22. const pageCount = ref(0);
  23. const trackRef = ref(null);
  24. let resizeObserver = null;
  25. const resolveItems = () => {
  26. const value = typeof source === "function" ? source() : unref(source);
  27. return Array.isArray(value) ? value : [];
  28. };
  29. const measure = () => {
  30. const track = trackRef.value;
  31. if (!track || !track.clientWidth) return;
  32. const cards = Array.from(track.children).filter(
  33. (element) => element.nodeType === Node.ELEMENT_NODE,
  34. );
  35. if (!cards.length) {
  36. activeIndex.value = 0;
  37. pageCount.value = 0;
  38. return;
  39. }
  40. const isItemMode =
  41. cards[0].offsetWidth >= track.clientWidth * ITEM_MODE_RATIO;
  42. pageCount.value = isItemMode
  43. ? cards.length
  44. : Math.max(1, Math.ceil(track.scrollWidth / track.clientWidth));
  45. if (pageCount.value <= 1) {
  46. activeIndex.value = 0;
  47. return;
  48. }
  49. const maxScroll = track.scrollWidth - track.clientWidth;
  50. if (maxScroll > 0 && track.scrollLeft >= maxScroll - 1) {
  51. activeIndex.value = pageCount.value - 1;
  52. return;
  53. }
  54. const index = isItemMode
  55. ? closestCardIndex(cards, track.scrollLeft)
  56. : Math.round(track.scrollLeft / track.clientWidth);
  57. activeIndex.value = Math.min(Math.max(index, 0), pageCount.value - 1);
  58. };
  59. const stopObserving = () => {
  60. resizeObserver?.disconnect();
  61. resizeObserver = null;
  62. };
  63. const startObserving = (track) => {
  64. if (!track || typeof ResizeObserver === "undefined") return;
  65. resizeObserver = new ResizeObserver(() => measure());
  66. resizeObserver.observe(track);
  67. };
  68. watch(
  69. trackRef,
  70. (track) => {
  71. stopObserving();
  72. if (track) startObserving(track);
  73. },
  74. { flush: "post" },
  75. );
  76. watch(
  77. () => resolveItems().length,
  78. () => nextTick(measure),
  79. );
  80. onMounted(() => nextTick(measure));
  81. onBeforeUnmount(stopObserving);
  82. return {
  83. activeIndex,
  84. onTrackScroll: measure,
  85. pageCount,
  86. trackRef,
  87. };
  88. };