Riunshow / NoteBook

人生不如意事十之九八,可与人言者并无二三
3 stars 0 forks source link

[react] hash route 时伪锚点实现 #28

Open Riunshow opened 3 years ago

Riunshow commented 3 years ago
/**
 * hack 锚点

 // usage
 const anchorProps = {
    type: 'scrollTop',
    container: '#container',
    interval: 0
  }
 or
 const anchorProps = {
    type: 'scrollIntoView'
  }

 <Anchor name="xxx" {...anchorProps}>xxx</Anchor>
 */
import React from 'react'
import PropTypes from 'prop-types'

export const SCROLL_INTO_VIEW = 'scrollIntoView'
export const SCROLL_TOP = 'scrollTop'

const getSearch = () => {
  const { location } = window
  const result = location.href.split('?')[1]

  if (result) {
    return `?${result}`
  }
}

const getSearchParams = (key) => {
  const params = new URLSearchParams(getSearch())

  return params.get(key)
}

const isNumber = (val) => {
  // IE9 toString.call() 报错:调用的对象无效
  // 应为 window.toString !== Object.prototype.toString
  if (Object.prototype.toString.call(val) === '[object Number]') {
    if (isNaN(val)) {
      return false
    }

    return true
  }
  return false
}

// 为了兼容 IE Edge Chrome
const setScrollTop = (val) => {
  console.log(val)
  document.documentElement.scrollTop = val
  window.pageYOffset = val
  document.body.scrollTop = val
}

class Anchor extends React.Component {
  constructor(props) {
    super(props)

    this.anchorRef = React.createRef()
    this.handleHashChange = this.handleHashChange.bind(this)
    this.scroll = this.scroll.bind(this)
    this.scrollIntoView = this.scrollIntoView.bind(this)
    this.scrollTop = this.scrollTop.bind(this)
  }

  componentDidMount() {
    const { anchorKey } = this.props

    if (getSearchParams(anchorKey)) {
      this.scroll()
    }

    // Chrome keeps track of where you've been
    // https://developer.mozilla.org/en-US/docs/Web/API/History
    if ('scrollRestoration' in history) {
      history.scrollRestoration = 'manual'
    }

    window.addEventListener('hashchange', this.handleHashChange)

    this.props.onGetBoundingClientRect && document.querySelector('#container').addEventListener('scroll', this.handleDOMMouseScroll, false)
  }

  componentWillUnmount() {
    if ('scrollRestoration' in history) {
      history.scrollRestoration = 'auto'
    }

    window.removeEventListener('hashchange', this.handleHashChange)
    this.props.onGetBoundingClientRect && document.querySelector('#container').removeEventListener('scroll', this.handleDOMMouseScroll)
  }

  handleHashChange() {
    this.scroll()
  }

  // 获取当前可视区域的 name
  handleDOMMouseScroll = () => {
    const dom = this.anchorRef.current
    const { name } = this.props

    if (dom && dom.getBoundingClientRect().top < 200 && dom.getBoundingClientRect().top > 50) {
      if (this.props.onGetBoundingClientRect) {
        this.props.onGetBoundingClientRect(name)
      }
    }
  }

  scroll() {
    const { type } = this.props

    if (type === SCROLL_INTO_VIEW) {
      this.scrollIntoView()
    }

    if (type === SCROLL_TOP) {
      this.scrollTop()
    }
  }

  scrollIntoView() {
    const { name, anchorKey, scrollIntoViewOption } = this.props
    const anchor = getSearchParams(anchorKey)

    if (name === anchor) {
      const dom = this.anchorRef.current

      if (dom.scrollIntoView) {
        setTimeout(() => {
          dom.scrollIntoView(scrollIntoViewOption)
        }, 0)
      }
    }
  }

  scrollTop() {
    const { name, anchorKey, container, interval } = this.props
    const anchor = getSearchParams(anchorKey)

    if (name === anchor) {
      if (!isNumber(interval)) {
        throw new Error('interval must be a number')
      }
      const dom = this.anchorRef.current
      const scrollTop = dom.offsetTop + Number(interval)

      if (container) {
        const cont = document.querySelector(container)

        if (!cont) {
          throw new Error('container can\'t match any element')
        }

        setTimeout(() => {
          cont.scrollTop = scrollTop
        }, 0)
      } else {
        setTimeout(() => {
          setScrollTop(scrollTop)
        }, 0)
      }
    }
  }

  render() {
    const { children } = this.props

    return (
      <div ref={this.anchorRef}>
        {children}
      </div>
    )
  }
}

Anchor.defaultProps = {
  anchorKey: '_to',
  type: SCROLL_INTO_VIEW,
  scrollIntoViewOption: true,
  interval: 0
}

Anchor.protoTypes = {
  anchorKey: PropTypes.string,
  type: PropTypes.oneOf([SCROLL_INTO_VIEW, SCROLL_TOP]),
  scrollIntoViewOption: PropTypes.oneOf([
    PropTypes.bool,
    PropTypes.object
  ]),
  container: PropTypes.string,
  interval: PropTypes.number,
  onGetBoundingClientRect: PropTypes.func
}

export default Anchor