import { Children, CSSProperties, ReactNode, ReactElement, cloneElement, useRef, forwardRef, ForwardRefExoticComponent, useImperativeHandle, useEffect, useState, } from 'react' import classNames from 'classnames' import { CommonComponentProps } from '../../utils/types' import './IndexBar.scss' import { IndexBarItem, IndexBarItemProps } from './IndexBarItem' import { useMapSet, useScroll, useSetTimeout, useStrike, useEvent, } from '../../use' import { matchScrollVisible, minmax } from '../../utils' import { PAN_END, PAN_MOVE, PAN_START, StrikePanEvent } from '../../strike' import { pageScrollTop } from '../../utils/dom' import { CSSTransition } from '../transition/CSSTransition' export * from './IndexBarItem' export interface IndexBarImperativeHandle { scrollTo(name: any): void } export interface IndexBarProps extends CommonComponentProps { className?: string style?: CSSProperties children?: ReactNode defaultActiveName?: any activeName?: any offset?: number anchorClass?: string anchorStyle?: CSSProperties } export interface IndexBar extends ForwardRefExoticComponent { Item: IndexBarItem } export const IndexBar = forwardRef( (props, ref) => { const { className, style, children, defaultActiveName, activeName, offset = 0, anchorClass, anchorStyle, ...restProps } = props const offsetRef = useRef(offset) const itemSet = useMapSet([]) const childrenRef = useRef(children) const [innerName, setInnerName] = useState(() => { const firstPane = Children.toArray( children )[0] as ReactElement return activeName ?? defaultActiveName ?? firstPane?.props.name ?? 0 }) const scrollLock = useRef(false) const { reset } = useSetTimeout(() => { scrollLock.current = false }, 250) const scrollTo = useEvent((name: any) => { const el = itemSet.get(name) if (el) { const top = el.getBoundingClientRect().top + window.scrollY - offsetRef.current reset() scrollLock.current = true pageScrollTop(top, false) setInnerName(name) } }) useScroll( () => { if (scrollLock.current) { return } const srcData = itemSet.getData() matchScrollVisible( srcData.map((item) => item[1]), (index) => { setInnerName(srcData[index][0]) }, offsetRef.current ) }, 150, { leading: false, } ) // hint const [hintIn, setTipsIn] = useState(false) const [hintVisible, setHintVisible] = useState(false) const [hintTop, setHintTop] = useState('') const getTipsTop = () => { const index = itemSet.getIndexByName(innerName) const length = Children.count(children) return ((index + 0.5) / length) * 100 + '%' } useEffect(() => { setHintTop(getTipsTop()) }, [innerName]) // 触摸切换 const navRef = useRef(null) const downHeightRef = useRef(0) const itemCount = useRef(0) const scrollByOffset = useEvent((offsetY) => { const index = minmax( Math.floor((offsetY / downHeightRef.current) * itemCount.current), 0, itemCount.current - 1 ) const name = itemSet.getData()[index][0] if (name !== innerName) { scrollTo(name) } }) const handlePanStart = useEvent((event: StrikePanEvent) => { downHeightRef.current = navRef.current?.getBoundingClientRect().height || 0 itemCount.current = Children.count(childrenRef.current) scrollByOffset(event.offsetY) setTipsIn(true) }) const handlePanMove = useEvent((event: StrikePanEvent) => { scrollByOffset(event.offsetY) }) const handlePanEnd = useEvent(() => { setTipsIn(false) }) const navBinding = useStrike( (strike) => { strike.on(PAN_START, handlePanStart) strike.on(PAN_MOVE, handlePanMove) strike.on(PAN_END, handlePanEnd) }, { pan: true, } ) useImperativeHandle(ref, () => ({ scrollTo, })) const indexBarClass = classNames('s-index-bar', className) return (
{Children.map( children as ReactElement, (item: ReactElement, index: number) => { const name = item.props.name ?? index return cloneElement(item, { key: name, name, activeName: innerName, offset, ref: (el: any) => itemSet.set(name, el), anchorClass: classNames(anchorClass, item.props.anchorClass), anchorStyle: { ...anchorStyle, ...item.props.anchorStyle }, }) } )}
{Children.map( children as ReactElement, (item: ReactElement, index: number) => { const name = item.props.name ?? index return (
{name}
) } )} setHintVisible(true)} onExited={() => setHintVisible(false)} >
{innerName}
) } ) as IndexBar IndexBar.Item = IndexBarItem export default IndexBar