admin-vben/packages/@core/ui-kit/tabs-ui/src/use-tabs-view-scroll.ts

195 lines
4.6 KiB
TypeScript

import type { TabsProps } from './types';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { VbenScrollbar } from '@vben-core/shadcn-ui';
import { useDebounceFn } from '@vueuse/core';
type DomElement = Element | null | undefined;
export function useTabsViewScroll(props: TabsProps) {
let resizeObserver: null | ResizeObserver = null;
let mutationObserver: MutationObserver | null = null;
let tabItemCount = 0;
const scrollbarRef = ref<InstanceType<typeof VbenScrollbar> | null>(null);
const scrollViewportEl = ref<DomElement>(null);
const showScrollButton = ref(false);
const scrollIsAtLeft = ref(true);
const scrollIsAtRight = ref(false);
function getScrollClientWidth() {
const scrollbarEl = scrollbarRef.value?.$el;
if (!scrollbarEl || !scrollViewportEl.value) return {};
const scrollbarWidth = scrollbarEl.clientWidth;
const scrollViewWidth = scrollViewportEl.value.clientWidth;
return {
scrollbarWidth,
scrollViewWidth,
};
}
function scrollDirection(
direction: 'left' | 'right',
distance: number = 150,
) {
const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();
if (!scrollbarWidth || !scrollViewWidth) return;
if (scrollbarWidth > scrollViewWidth) return;
scrollViewportEl.value?.scrollBy({
behavior: 'smooth',
left:
direction === 'left'
? -(scrollbarWidth - distance)
: +(scrollbarWidth - distance),
});
}
async function initScrollbar() {
await nextTick();
const scrollbarEl = scrollbarRef.value?.$el;
if (!scrollbarEl) {
return;
}
const viewportEl = scrollbarEl?.querySelector(
'div[data-radix-scroll-area-viewport]',
);
scrollViewportEl.value = viewportEl;
calcShowScrollbarButton();
await nextTick();
scrollToActiveIntoView();
// 监听大小变化
resizeObserver?.disconnect();
resizeObserver = new ResizeObserver(
useDebounceFn((_entries: ResizeObserverEntry[]) => {
calcShowScrollbarButton();
scrollToActiveIntoView();
}, 100),
);
resizeObserver.observe(viewportEl);
tabItemCount = props.tabs?.length || 0;
mutationObserver?.disconnect();
// 使用 MutationObserver 仅监听子节点数量变化
mutationObserver = new MutationObserver(() => {
const count = viewportEl.querySelectorAll(
`div[data-tab-item="true"]`,
).length;
if (count > tabItemCount) {
scrollToActiveIntoView();
}
if (count !== tabItemCount) {
calcShowScrollbarButton();
tabItemCount = count;
}
});
// 配置为仅监听子节点的添加和移除
mutationObserver.observe(viewportEl, {
attributes: false,
childList: true,
subtree: true,
});
}
async function scrollToActiveIntoView() {
if (!scrollViewportEl.value) {
return;
}
await nextTick();
const viewportEl = scrollViewportEl.value;
const { scrollbarWidth } = getScrollClientWidth();
const { scrollWidth } = viewportEl;
if (scrollbarWidth >= scrollWidth) {
return;
}
requestAnimationFrame(() => {
const activeItem = viewportEl?.querySelector('.is-active');
activeItem?.scrollIntoView({ behavior: 'smooth', inline: 'start' });
});
}
/**
* 计算tabs 宽度,用于判断是否显示左右滚动按钮
*/
async function calcShowScrollbarButton() {
if (!scrollViewportEl.value) {
return;
}
const { scrollbarWidth } = getScrollClientWidth();
showScrollButton.value =
scrollViewportEl.value.scrollWidth > scrollbarWidth;
}
const handleScrollAt = useDebounceFn(({ left, right }) => {
scrollIsAtLeft.value = left;
scrollIsAtRight.value = right;
}, 100);
watch(
() => props.active,
async () => {
// 200为了等待 tab 切换动画完成
// setTimeout(() => {
scrollToActiveIntoView();
// }, 300);
},
{
flush: 'post',
},
);
// watch(
// () => props.tabs?.length,
// async () => {
// await nextTick();
// calcShowScrollbarButton();
// },
// {
// flush: 'post',
// },
// );
watch(
() => props.styleType,
() => {
initScrollbar();
},
);
onMounted(initScrollbar);
onUnmounted(() => {
resizeObserver?.disconnect();
mutationObserver?.disconnect();
resizeObserver = null;
mutationObserver = null;
});
return {
handleScrollAt,
initScrollbar,
scrollbarRef,
scrollDirection,
scrollIsAtLeft,
scrollIsAtRight,
showScrollButton,
};
}