知识库文章实现生成目录锚点导航

2,693 阅读2分钟

介绍

离2022年还剩最后一个月了,近来需求饱和了(bug太多QAQ),没更新,这次分享一个实现知识文章数据生成目录导航的一个功能,方便展示,随机扒了一篇文章做演示

image.png

image.png

不好弄动图~~, 左侧是文章正文,通过编辑好的富文本数据生成,右侧则是目录,通过文章数据生成,可通过目录定位至当前标题位置,在滚动过程也能同时定位当前标题

实现

HTML代码结构如下:

  <div class="relative-position">
    <div v-viewer="defaultOption" class="article" @click="show" v-html="content"></div>
    <div class="docs-aside">
      <span class="aside-title">目录</span>
      <div class="aside-body">
        <ul class="aside-article-catalog">
          <li v-for="(item,index) in docMenu" :key="item.id" :class="`level_${item.level}`">
            <a :href="'#' + item.id" :class="{active: active === index }" @click="handlerSroll($event, item.id,index)">{{ item.text }}</a>
          </li>
        </ul>
      </div>
    </div>
  </div>

在实现逻辑前需了解两个DocumentElement的两个属性

首先是scrollTop 它可以获取或设置一个元素的内容垂直滚动的像素数 链接

定义:一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0

话不多说,通过画图来了解下这个属性 首先是视口和元素

image.png

那么scrollTop就是

image.png

再者就是offsetTop 它返回当前元素相对于其 offsetParent 元素的顶部内边距的距离 链接

image.png

通过理解这两个属性后看下实现逻辑

在文章内容渲染后通过类名获取到元素并对指定的标签赋予标题锚点id,生成目录,通过a标签能够跳转到href中指定的位置(#+下方定位的id),锚点能够跳转到当前页面中指定的位置

initArt() {
      let markMenu = []
      setTimeout(() => {
        const articleDom = document.querySelector('.article')
        if (articleDom) {
          for (let ele of articleDom.children) {
            const i = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].indexOf(ele.tagName)
            if (i > -1 && ele.textContent) {
              ele.setAttribute('id', 'markMenu_' + markMenu.length)
              ele.setAttribute('name', 'markMenu_' + markMenu.length)
              markMenu.push({
                level: i,
                text: ele.textContent,
                id: 'markMenu_' + markMenu.length,
                name:'markMenu_' + markMenu.length
              })
            }
          }
        }
        //docMenu为目录数据
        this.docMenu = markMenu
      })
    },

此时目录都记录了当前文章中标签的锚点id,可以通过点击目录中的标题定位到当前位置

    handlerSroll(e, id) {
      //由于存在头部的关系,会挡住标题,所以还需计算滚动头部的高度
      const element = document.querySelector(`.page-header`);
      const targetDom= document.querySelector(`#${id}`);
      //测试到火狐浏览器存在锚点定位的问题,使用scrollIntoView的方法
      if(navigator.userAgent.indexOf("Firefox")>0){
        e.preventDefault()
        targetDom.scrollIntoView({
          //滚动到指定节点
          block: "start",
          behavior: "auto",
        });
        setTimeout(() => {
          window.scrollBy(0, -element.offsetHeight)
        }, 100)
        return
      }
      window.scrollBy(0, -element.offsetHeight)
    },

最后需要监听到滚动过程中所处的目录位置

  mounted() {
    window.addEventListener('scroll', this.onScroll)
  },
  destroy() {
    window.removeEventListener('scroll', this.onScroll)
  },

监听滚动

onScroll() {
      const element = document.querySelector(`.page-header`);
      // 获取所有锚点元素
      const titleNavList = document.querySelectorAll('.article h1,.article h2,.article h3,.article h4,.article h5,.article h6')
      // 计算所有锚点元素的 offsetTop + 头部的高度
      const offsetTopList = []
      titleNavList.forEach(item => {
        offsetTopList.push(item.offsetTop - element.offsetHeight)
      })
      // 获取当前文档流的 scrollTop
      const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
      // 定义当前所在的目录下标
      let navIndex = 0
      // 比较当前文章滚动的距离scrollTop与各锚点标题的offsetTop ,当scrollTop超过当前元素的scrollTop,则定位到当前标题
      for (let n = 0; n < offsetTopList.length; n++) {
        if (scrollTop >= offsetTopList[n]) {
          navIndex = n
        }
      }
      //当前高亮的目录索引,默认为0
      this.active = navIndex
    }

那么整体实现定位文章标题位置及监听滚动当前文章标题就完成了,关键点通过a标题的锚点作用及计算文章中标题的距离顶部的距离实现.

最后

有缺漏和待优化的地方欢迎各位观众姥爷吐槽提出. 最后补充下样式代码吧

.docs-aside {
  position: fixed;
  top: 0;
  right: 150px;
  display: flex;
  flex-direction: column;
  bottom: 0;
  padding-top: 150px;
  z-index: 100;
  width: 165px;
}

.docs-aside .aside-title {
  border-bottom: 1px solid #d5dbe7;
  font-size: 12px;
  color: #999999;
  line-height: 20px;
  padding: 10px 0;
}

.docs-aside .aside-body {
  flex: 1 1 100%;
  padding: 10px 0;
  overflow-y: auto;
}

.docs-aside .aside-article-catalog {
  list-style: none;
  padding: 0;
  margin: 0;
  @for $i from 1 to 6 {
    & .level_#{$i} {
      padding-left: $i * 10px;
    }
  }
}

.docs-aside .aside-article-catalog > li > a {
  display: block;
  font-size: 14px;
  line-height: 20px;
  padding: 5px 0;
  color: #818991;
}

.docs-aside .aside-article-catalog > li > a:hover,
.docs-aside .aside-article-catalog > li > a.active {
  color: #1672FA;
}