生成目录

601 阅读3分钟

前言

主流文档无非富文本或者markdown文档. 如果你有需求自己实现一个右侧的目录导航,typora做到的那种,不过人家是在左侧. 在typora 中输入 [toc] 也能生成文档中的目录

但是如果你想在个人博客,或者公司项目的右侧加一个想掘金这样的目录结构呢 网上搜索的话你可能会发现这个库

markdown-navbar这个库, 好像是三年前的库了, 他有一些bug 比如第一个h1标题检索不到(官方的解释是第一个h1是默认文章标题,不是章节,但有的文章和标题是分开存储的), 标题中有a子标签就检索不到了, 有时候多级子标签同时高亮.

问题不少:故此只能自己手写了

需求: 制作一个目录, 可以锚点定位跳转. 分享的链接点过去直接到达想要分享的内容

参考博客

最初的实现主要参考了这两个博客 百度

整理实现思路

写过富文本中提取img标签给其绑定点击事件的同学应该知道. 通过获得这个内容结点然后使用querySelectorAll()这个方法来获取所有的img标签然后进行相关操作的.

方法类似: 想办法拿到内容的节点,然后获取到属于他的子节点h1 到 h6的标题元素

之后找好你要放目录的地方 渲染titles对象

基础实现方法. (用hooks的)

  1. 在bytemd的 Viewer组件外层套一个div标签绑定ref const artRef = useRef<HTMLDivElement>(null)

  2. 通过ref获得viewer中渲染的标题节点, 给h1~h6增加id属性 并且提炼出生成目录的对象(内容包括id,和title, 和是h几标签) 然后在目录容器绑定点击事件 获取要跳过去的node 使用 scrollIntoView跳过去

let idx = 0
let title: Chapter[] = [] // 目录对象
let offset: number[] = [] // 标题距离顶部高度数组
const nodes = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
props.docNode?.childNodes.forEach(item => {
  if (nodes.includes(item.nodeName)) {
    const node = item as HTMLElement
    const id = 'heading-' + idx
    idx = idx + 1
    node.setAttribute('id', id)
    title.push({
      id: id,
      title: node.innerText,
      level: Number(node.nodeName.substring(1, 2))
    })
    offset.push(node.offsetTop + wrapNode?.offsetTop - 80)
  }
})
  1. 获得所有标题到达顶部的距离,放在数组中 Node.offsetTop,然后改变章节的活跃项
<div
onClick={(e: any) => {
  if (e.target.id) {
    setCur(parseInt(e.target.id)) // cur是活跃项的下标.
    const node = document.getElementById('heading-' + e.target.id)
    node?.scrollIntoView(true)
    setTitleClick(true)
    parNode?.scrollTop < 300 ? setFixed(true) : setFixed(false)
  }
}}
style={{ top: martop }}
className={fixed ? styles.mdNav : styles.fixed}
>
    {titles.map((item, idx) => (
      <a
        onClick={e => e.preventDefault()}//防止a标签的自动跳转,以为我们不用他的锚点定位
        key={item.id}
        className={cur === idx ? styles.active : ''} //表示是否为当前章节
        style={{
          paddingLeft: 15 * item.level,
          fontWeight: item.level < 3 ? 'bold' : 'normal' //这里就体现了提取level的价值
        }}
        id={idx.toString()}
        title={item.title}
        href={'#' + item.id}// 有他的话会让url上加上hash值. 但是上面已经阻止默认事件了所以他是不生效的,我们使用另一种方法
      >
        {item.title}
      </a>
    ))}
</div>
  1. 并且改变地址栏通过window.history.replceState,这是为了分享链接后锚点定位
 useEffect(() => {
    if (offsets.length > 23 && cur > 11) {
      const value = (cur - 11) * 30
      setMartop(props.TOP - value)
    } else setMartop(props.TOP)
    if (cur > -1) {
      window.history.replaceState(
        {}, '', window.location.href.split('#')[0] + '#' + `heading-${cur}`
      )
    }
  }, [cur]) 当活跃的标题变化
  1. 开始需要定位那就是解析url中的hash值 然后就能获得节点并且直接定位展示了
const tempid = window.location.hash.substring(1)
const node = document.getElementById(tempid) as HTMLHeadingElement
node?.scrollIntoView(true)