use-tabs-view-scroll.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import type { TabsProps } from './types';
  2. import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
  3. import { VbenScrollbar } from '@vben-core/shadcn-ui';
  4. import { useDebounceFn } from '@vueuse/core';
  5. type DomElement = Element | null | undefined;
  6. export function useTabsViewScroll(props: TabsProps) {
  7. let resizeObserver: null | ResizeObserver = null;
  8. let mutationObserver: MutationObserver | null = null;
  9. let tabItemCount = 0;
  10. const scrollbarRef = ref<InstanceType<typeof VbenScrollbar> | null>(null);
  11. const scrollViewportEl = ref<DomElement>(null);
  12. const showScrollButton = ref(false);
  13. const scrollIsAtLeft = ref(true);
  14. const scrollIsAtRight = ref(false);
  15. function getScrollClientWidth() {
  16. const scrollbarEl = scrollbarRef.value?.$el;
  17. if (!scrollbarEl || !scrollViewportEl.value) return {};
  18. const scrollbarWidth = scrollbarEl.clientWidth;
  19. const scrollViewWidth = scrollViewportEl.value.clientWidth;
  20. return {
  21. scrollbarWidth,
  22. scrollViewWidth,
  23. };
  24. }
  25. function scrollDirection(
  26. direction: 'left' | 'right',
  27. distance: number = 150,
  28. ) {
  29. const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();
  30. if (!scrollbarWidth || !scrollViewWidth) return;
  31. if (scrollbarWidth > scrollViewWidth) return;
  32. scrollViewportEl.value?.scrollBy({
  33. behavior: 'smooth',
  34. left:
  35. direction === 'left'
  36. ? -(scrollbarWidth - distance)
  37. : +(scrollbarWidth - distance),
  38. });
  39. }
  40. async function initScrollbar() {
  41. await nextTick();
  42. const scrollbarEl = scrollbarRef.value?.$el;
  43. if (!scrollbarEl) {
  44. return;
  45. }
  46. const viewportEl = scrollbarEl?.querySelector(
  47. 'div[data-radix-scroll-area-viewport]',
  48. );
  49. scrollViewportEl.value = viewportEl;
  50. calcShowScrollbarButton();
  51. await nextTick();
  52. scrollToActiveIntoView();
  53. // 监听大小变化
  54. resizeObserver?.disconnect();
  55. resizeObserver = new ResizeObserver(
  56. useDebounceFn((_entries: ResizeObserverEntry[]) => {
  57. calcShowScrollbarButton();
  58. }, 100),
  59. );
  60. resizeObserver.observe(viewportEl);
  61. tabItemCount = props.tabs?.length || 0;
  62. mutationObserver?.disconnect();
  63. // 使用 MutationObserver 仅监听子节点数量变化
  64. mutationObserver = new MutationObserver(() => {
  65. const count = viewportEl.querySelectorAll(
  66. `div[data-tab-item="true"]`,
  67. ).length;
  68. if (count > tabItemCount) {
  69. scrollToActiveIntoView();
  70. }
  71. if (count !== tabItemCount) {
  72. calcShowScrollbarButton();
  73. tabItemCount = count;
  74. }
  75. });
  76. // 配置为仅监听子节点的添加和移除
  77. mutationObserver.observe(viewportEl, {
  78. attributes: false,
  79. childList: true,
  80. subtree: true,
  81. });
  82. }
  83. async function scrollToActiveIntoView() {
  84. if (!scrollViewportEl.value) {
  85. return;
  86. }
  87. await nextTick();
  88. const viewportEl = scrollViewportEl.value;
  89. const { scrollbarWidth } = getScrollClientWidth();
  90. const { scrollWidth } = viewportEl;
  91. if (scrollbarWidth >= scrollWidth) {
  92. return;
  93. }
  94. requestAnimationFrame(() => {
  95. const activeItem = viewportEl?.querySelector('.is-active');
  96. activeItem?.scrollIntoView({ behavior: 'smooth', inline: 'start' });
  97. });
  98. }
  99. /**
  100. * 计算tabs 宽度,用于判断是否显示左右滚动按钮
  101. */
  102. async function calcShowScrollbarButton() {
  103. if (!scrollViewportEl.value) {
  104. return;
  105. }
  106. const { scrollbarWidth } = getScrollClientWidth();
  107. showScrollButton.value =
  108. scrollViewportEl.value.scrollWidth > scrollbarWidth;
  109. }
  110. const handleScrollAt = useDebounceFn(({ left, right }) => {
  111. scrollIsAtLeft.value = left;
  112. scrollIsAtRight.value = right;
  113. }, 100);
  114. watch(
  115. () => props.active,
  116. async () => {
  117. // 200为了等待 tab 切换动画完成
  118. // setTimeout(() => {
  119. scrollToActiveIntoView();
  120. // }, 300);
  121. },
  122. {
  123. flush: 'post',
  124. },
  125. );
  126. // watch(
  127. // () => props.tabs?.length,
  128. // async () => {
  129. // await nextTick();
  130. // calcShowScrollbarButton();
  131. // },
  132. // {
  133. // flush: 'post',
  134. // },
  135. // );
  136. watch(
  137. () => props.styleType,
  138. () => {
  139. initScrollbar();
  140. },
  141. );
  142. onMounted(initScrollbar);
  143. onUnmounted(() => {
  144. resizeObserver?.disconnect();
  145. mutationObserver?.disconnect();
  146. resizeObserver = null;
  147. mutationObserver = null;
  148. });
  149. return {
  150. handleScrollAt,
  151. initScrollbar,
  152. scrollbarRef,
  153. scrollDirection,
  154. scrollIsAtLeft,
  155. scrollIsAtRight,
  156. showScrollButton,
  157. };
  158. }