Vue2根据文章h标签,自动生成一个目录树(Toc),并可以跳转位置(动画),同时可以监听滑动位置,改变对应目录高亮
vue2实现 类似 tocbot的效果,自动生成一个目录树,同时具备动画以及滑动检测高亮的效果
·
问题提出
今天准备写一个通过博客内容生成目录的功能,在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目录就此完成
更多推荐
已为社区贡献1条内容
所有评论(0)