import { ref, watch, computed, nextTick, PropType, Teleport, onMounted, CSSProperties, TeleportProps, defineComponent, ExtractPropTypes, } from 'vue'; // Utils import { isDef, isHidden, truthProp, getScrollTop, preventDefault, createNamespace, getRootScrollTop, setRootScrollTop, ComponentInstance, } from '../utils'; // Composables import { useRect, useChildren, useScrollParent, useEventListener, } from '@vant/use'; import { useTouch } from '../composables/use-touch'; import { useExpose } from '../composables/use-expose'; function genAlphabet() { const charCodeOfA = 'A'.charCodeAt(0); const indexList = Array(26) .fill('') .map((_, i) => String.fromCharCode(charCodeOfA + i)); return indexList; } const [name, bem] = createNamespace('index-bar'); export const INDEX_BAR_KEY = Symbol(name); const props = { sticky: truthProp, zIndex: [Number, String], teleport: [String, Object] as PropType, highlightColor: String, stickyOffsetTop: { type: Number, default: 0, }, indexList: { type: Array as PropType, default: genAlphabet, }, }; export type IndexBarProvide = { props: ExtractPropTypes; }; export default defineComponent({ name, props, emits: ['select', 'change'], setup(props, { emit, slots }) { const root = ref(); const activeAnchor = ref(''); const touch = useTouch(); const scrollParent = useScrollParent(root); const { children, linkChildren } = useChildren( INDEX_BAR_KEY ); linkChildren({ props }); const sidebarStyle = computed(() => { if (isDef(props.zIndex)) { return { zIndex: +props.zIndex + 1, }; } }); const highlightStyle = computed(() => { if (props.highlightColor) { return { color: props.highlightColor, }; } }); const getScrollerRect = () => { if ('getBoundingClientRect' in scrollParent.value!) { return useRect(scrollParent); } return { top: 0, left: 0, }; }; const getActiveAnchor = ( scrollTop: number, rects: Array<{ top: number; height: number }> ) => { for (let i = children.length - 1; i >= 0; i--) { const prevHeight = i > 0 ? rects[i - 1].height : 0; const reachTop = props.sticky ? prevHeight + props.stickyOffsetTop : 0; if (scrollTop + reachTop >= rects[i].top) { return i; } } return -1; }; const onScroll = () => { if (isHidden(root)) { return; } const { sticky, indexList } = props; const scrollTop = getScrollTop(scrollParent.value!); const scrollParentRect = getScrollerRect(); const rects = children.map((item) => item.getRect(scrollParent.value, scrollParentRect) ); const active = getActiveAnchor(scrollTop, rects); activeAnchor.value = indexList[active]; if (sticky) { children.forEach((item, index) => { const { state, $el } = item; if (index === active || index === active - 1) { const rect = $el.getBoundingClientRect(); state.left = rect.left; state.width = rect.width; } else { state.left = null; state.width = null; } if (index === active) { state.active = true; state.top = Math.max(props.stickyOffsetTop, rects[index].top - scrollTop) + scrollParentRect.top; } else if (index === active - 1) { const activeItemTop = rects[active].top - scrollTop; state.active = activeItemTop > 0; state.top = activeItemTop + scrollParentRect.top - rects[index].height; } else { state.active = false; } }); } }; const init = () => nextTick(onScroll); useEventListener('scroll', onScroll, { target: scrollParent }); onMounted(init); watch(() => props.indexList, init); watch(activeAnchor, (value) => { if (value) { emit('change', value); } }); const renderIndexes = () => props.indexList.map((index) => { const active = index === activeAnchor.value; return ( {index} ); }); const scrollTo = (index: string) => { if (!index) { return; } const match = children.find((item) => String(item.index) === index); if (match) { match.$el.scrollIntoView(); if (props.sticky && props.stickyOffsetTop) { setRootScrollTop(getRootScrollTop() - props.stickyOffsetTop); } emit('select', match.index); } }; const scrollToElement = (element: HTMLElement) => { const { index } = element.dataset; if (index) { scrollTo(index); } }; const onClickSidebar = (event: MouseEvent) => { scrollToElement(event.target as HTMLElement); }; let touchActiveIndex: string; const onTouchMove = (event: TouchEvent) => { touch.move(event); if (touch.isVertical()) { preventDefault(event); const { clientX, clientY } = event.touches[0]; const target = document.elementFromPoint( clientX, clientY ) as HTMLElement; if (target) { const { index } = target.dataset; if (index && touchActiveIndex !== index) { touchActiveIndex = index; scrollToElement(target); } } } }; const renderSidebar = () => (
{renderIndexes()}
); useExpose({ scrollTo }); return () => (
{props.teleport ? ( {renderSidebar()} ) : ( renderSidebar() )} {slots.default?.()}
); }, });