use-tabs-view-scroll.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. scrollToActiveIntoView();
  59. }, 100),
  60. );
  61. resizeObserver.observe(viewportEl);
  62. tabItemCount = props.tabs?.length || 0;
  63. mutationObserver?.disconnect();
  64. // 使用 MutationObserver 仅监听子节点数量变化
  65. mutationObserver = new MutationObserver(() => {
  66. const count = viewportEl.querySelectorAll(
  67. `div[data-tab-item="true"]`,
  68. ).length;
  69. if (count > tabItemCount) {
  70. scrollToActiveIntoView();
  71. }
  72. if (count !== tabItemCount) {
  73. calcShowScrollbarButton();
  74. tabItemCount = count;
  75. }
  76. });
  77. // 配置为仅监听子节点的添加和移除
  78. mutationObserver.observe(viewportEl, {
  79. attributes: false,
  80. childList: true,
  81. subtree: true,
  82. });
  83. }
  84. async function scrollToActiveIntoView() {
  85. if (!scrollViewportEl.value) {
  86. return;
  87. }
  88. await nextTick();
  89. const viewportEl = scrollViewportEl.value;
  90. const { scrollbarWidth } = getScrollClientWidth();
  91. const { scrollWidth } = viewportEl;
  92. if (scrollbarWidth >= scrollWidth) {
  93. return;
  94. }
  95. requestAnimationFrame(() => {
  96. const activeItem = viewportEl?.querySelector('.is-active');
  97. activeItem?.scrollIntoView({ behavior: 'smooth', inline: 'start' });
  98. });
  99. }
  100. /**
  101. * 计算tabs 宽度,用于判断是否显示左右滚动按钮
  102. */
  103. async function calcShowScrollbarButton() {
  104. if (!scrollViewportEl.value) {
  105. return;
  106. }
  107. const { scrollbarWidth } = getScrollClientWidth();
  108. showScrollButton.value =
  109. scrollViewportEl.value.scrollWidth > scrollbarWidth;
  110. }
  111. const handleScrollAt = useDebounceFn(({ left, right }) => {
  112. scrollIsAtLeft.value = left;
  113. scrollIsAtRight.value = right;
  114. }, 100);
  115. watch(
  116. () => props.active,
  117. async () => {
  118. // 200为了等待 tab 切换动画完成
  119. // setTimeout(() => {
  120. scrollToActiveIntoView();
  121. // }, 300);
  122. },
  123. {
  124. flush: 'post',
  125. },
  126. );
  127. // watch(
  128. // () => props.tabs?.length,
  129. // async () => {
  130. // await nextTick();
  131. // calcShowScrollbarButton();
  132. // },
  133. // {
  134. // flush: 'post',
  135. // },
  136. // );
  137. watch(
  138. () => props.styleType,
  139. () => {
  140. initScrollbar();
  141. },
  142. );
  143. onMounted(initScrollbar);
  144. onUnmounted(() => {
  145. resizeObserver?.disconnect();
  146. mutationObserver?.disconnect();
  147. resizeObserver = null;
  148. mutationObserver = null;
  149. });
  150. return {
  151. handleScrollAt,
  152. initScrollbar,
  153. scrollbarRef,
  154. scrollDirection,
  155. scrollIsAtLeft,
  156. scrollIsAtRight,
  157. showScrollButton,
  158. };
  159. }