问题提出

今天准备写一个通过博客内容生成目录的功能,在github以及码云上找了很久没有找到合适的一个插件,于是,自己造轮子

效果图

在这里插入图片描述

问题解决

第一步 生成目录树结构

通过扫描文章的内容,找出所有的h标签,加入集合,同时给每个h标签按照从上往下的顺序加上id,比如:‘h-1’,‘h-2’

这一部分代码我借用了别人的实现

前端薛小帅的代码
中间有个小bug我改了一下,我的实现

// 将一个集合的数据变成一个树形的数据结构
toTree(data){
      // 删除 所有 children,以防止多次调用
      data.forEach(function (item) {
        delete item.children;
      });

      // 将数据存储为 以 id 为 KEY 的 map 索引数据列
      var map = {};
      data.forEach(function (item) {
        map[item.id] = item;
      });
      var val = [];
      data.forEach(function (item) {
        // 以当前遍历项的pid,去map对象中找到索引的id
        var parent = map[item.p_id];
        // 好绕啊,如果找到索引,那么说明此项不在顶级当中,那么需要把此项添加到,他对应的父级中
        if (parent) {
          (parent.children || (parent.children = [])).push(item);
        } else {
          //如果没有在map中找到对应的索引ID,那么直接把 当前的item添加到 val结果集中,作为顶级
          val.push(item);
        }
      });
      return val;
    },
    /**
     * 生成目录
     * */
    makeToc(){
    // 获取所有的h标签,给他们加上id,同时创建符合toTree方法要求的对象
    //{
    //          id:'',// 抛出id
   //           tag:'',// 抛出标签名称
    //          label:'',// 抛出标题
    //          p_id:'',// 抛出父级id
           // }
  
          // 定义参与目录生成的标签
          const tocTags = ["H1","H2","H3","H4","H5","H6"];

          // 目录树结果
          const tocArr = [];

          // 获取所有标题标签
          const headDoms = Array.from(this.$refs.aContent.childNodes).filter(item => tocTags.includes(item.tagName));

          // 遍历标题标签
          headDoms.forEach((item,index,arr) => {
            // 给标题添加id
            item.id = `h-${index + 1}`;
            // 获取当前节点前面的节点
            let prevs = arr.filter((i,j) => j < index);
            // 过滤前面的节点为合理节点
            // 如 h3节点前  只能为 h1 h2 h3
            prevs = prevs.filter(i => tocTags.filter((i,j) => j <= tocTags.findIndex(i => i == item.tagName)).includes(i.tagName));
            // 对前面的节点进行排序,距离自身节点近的排在前面
            // 如 div > p > span > img  当前为img
            // 常规获取节点为 [div,p,span,img]
            // 排序后获取节点为 [img,span,p,div]
            prevs = prevs.sort((a,b) => -(a.id.replace('h-','')) - b.id.replace('h-',''));
            // 查询距离自身节点最近的不同于当前标签的节点
            const prev = prevs.find(i => i.tagName != item.tagName);
            this.maxum = Math.max(this.maxum,index + 1)
            tocArr.push({
              id:index + 1,// 抛出id
              tag:item.tagName,// 抛出标签名称
              label:item.innerText,// 抛出标题
              p_id:item.tagName == "H1" || prev == null ? 0 : Number(prev.id.replace("h-",'')),// 抛出父级id
            })
          })

          // 使用上述方法生成树 最后在el-tree的data中使用 tocData即可
          this.tocData = this.toTree(tocArr);
           console.log(this.tocData)
    },

第二步 渲染目录树到页面

 updated() {
 // 需要在data里面加上flag,只触发一次里面内容
    if (this.flag) {
      this.makeToc()
      this.flag = false
   }
  },

这里我做的是只渲染顶层标签,以及二级标签,三级标签

 <div style="background-color: white;position: fixed;width: 16vw;">
          <el-row style="font-size: 17px;border-bottom: 1px solid #f5f5f5;padding: 1em;" class="catalog">
            博客目录
          </el-row>
          <template v-for="item in tocData">
          <el-row style="padding-top: 1em;padding-bottom: 1em;" >
            <div class="log-back" style="padding-left: 1em;padding-right: 1em;">
              <div style="font-size: 16px;cursor: pointer" @click="handleNodeClick(item.id,$event)" class="log-item">{{item.label}}</div>
            </div>
                <template  v-for="a in item.children">
            <div class="log-back" style="padding-left: 1em;padding-right: 1em;">
              <div style="font-size: 16px;margin-left: 1em;margin-top: 1.2em;cursor: pointer" @click="handleNodeClick(a.id,$event)" class="log-item">{{a.label}}</div>
            </div>
                  <template v-for="b in a.children">
                   <div class="log-back" style="padding-left: 1em;padding-right: 1em;">
                     <div style="font-size: 16px;margin-left: 2em;margin-top: 1.2em;cursor: pointer" @click="handleNodeClick(b.id,$event)" class="log-item">{{b.label}}</div>
                   </div>
                  </template>
                </template>
          </el-row>
          </template>
        </div>

第三步 做点击跳转指定位置

点击哪条目录,就将这条目录的id传过来,id 对应标签h-id

  handleNodeClick(data,event) {

    //  实现跳转锚点

      let id = data
      let tag = 'h-'+id
      let anchorH = document.getElementById(tag).offsetTop // 得到h标签的位置

      this.$emit('changeHeight', anchorH); // 我的页面滚动通过父组件控制,所以发射出去让父组件操作
    },
changeHeight(record){
    
      let el  = this.$refs.scroll.wrap
      let cur = el.scrollTop  // el-scrollbar获取页面当前位置的属性,定位也是通过改变这个
      let flag = false
      
      let step = 20
      // 由于有可能是向上滑,有可能向下滑,所以需要考虑移动的正负
      if (record < cur) {
        step = -step;
        flag = true
      }else if (record == cur) {
        return
      }

		// 设置动画
		// 具体含义就是按照每8毫秒执行一次向上移动指定的步长,这样就是一个动画
      var timer = setInterval(()=>{
        if (flag) {
        //  说明小于0
          if (cur + step < record) {
          // 说明移动多了,这时候我们改变移动距离,直接移动到指定位置即可
            el.scrollTop = record
            clearInterval(timer)
          }else {
            el.scrollTop = cur + step
          }
        }else{
          if (cur + step > record) {
            // 说明移动多了,这时候我们改变移动距离,直接移动到指定位置即可
            el.scrollTop = record
            clearInterval(timer)
          }else {
            el.scrollTop = cur + step
          }
        }

        cur += step 
      },8)

    },

第四步 监听当前位置,改变目录的高亮

分成两小步,第一步是将当前每个标签,以及他们对应的位置交给父级组件

还是在updated里面

 updated() {
    if (this.flag) {
      this.makeToc()
      this.flag = false

      let values = []
      for (let i = 1;i <=this.maxum;i++) {
        let tag = 'h-'+i
        let anchorH = document.getElementById(tag).offsetTop
        console.log(anchorH)
        values.push({id:i,height:anchorH})
      }
      this.$emit('refreshScroll',values)
   }
  },
// 父组件 ,对这些数据进行保存
 refreshScroll(values){
      this.values = values
    },

第二步是对滚动进行监听,然后改变高亮

// 开启监听
mounted(){
	 window.addEventListener('scroll',this.handleScroll,true)
}
  handleScroll(){
        this.$emit('handleScroll') // 同样交给父组件处理
    },
// 父组件
handleScroll(){
        let  top = this.$refs.scroll.wrap.scrollTop
        for (let i = 0;i < this.values.length - 1;i ++) {
        // 判断当前页面的位置 应该属于哪个标签,我这里是标签下的内容没有结束,目录都是这个标签高亮
          if (top >= this.values[i].height - 1 && top < this.values[i + 1].height) {
			// -1 是我发现点击目录滑动的时候,会有<1 的误差,导致目录高亮不改变
			
            //  让之变成
            this.$refs.detail.changeItem(i)  // 通过这个方法,改变目录标签高亮
          }
        }
        // 特殊处理 第一个和最后一个
        if (top< this.values[0].height ) this.$refs.detail.changeItem(0)
        if (top >= this.values[this.values.length - 1].height - 1 ) this.$refs.detail.changeItem(this.values.length - 1)

    },

子组件的 changeItem方法

changeItem(index){
      var commentsInputs = document.getElementsByClassName('log-item') // 是背景色容器
      var items = document.getElementsByClassName('log-back') // 文字容器
      for (let i = 0;i < commentsInputs.length;i ++) {
        commentsInputs[i].style.color = 'black'
        items[i].style.background = 'white'
      }
      commentsInputs[index].style.color = 'red'
      items[index].style.background = '#f5f5f5'

    }

综上,一个文章的TOC目录就此完成

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐