这是我参与「第四届青训营 」笔记创作活动的第6天
仿掘金项目之实现文章预览|青训营笔记 - 掘金 (juejin.cn)
仿掘金项目之文章目录实现|青训营笔记 - 掘金 (juejin.cn)
仿掘金项目之文章目录滚动高亮实现 | 青训营笔记 - 掘金 (juejin.cn)
书接上文
前面我已经实现了基本的目录滚动高亮效果,但是还有个大bug没有解决,这个bug就是当目录内容过多,部分目录要通过滚动条的形式才能显示.也就是说有一部分目录会被隐藏起来.
那么当文章滚动到对应的目录隐藏部分时,目录会跟着文章滚动让当前高亮内容显示在可视区域吗?
答案是不会的,所以滚着滚着,高亮元素就看不到了.
所以我们必须自己实现这个功能
获取目录元素
<ul ref="nav" class="nav">
<li v-for="(i, index) in list" :key="index" :title="i.content" @click="jump(index)" :ref="setItemRef"
:class="activeIndex === index ? 'active' : ''">
<div :style="{ marginLeft: size(i.id) }">
{{ i.content }}
</div>
</li>
</ul>
这里通过ref='nav'获取目录元素,通过:ref="setItemRef"这个可以获取到所以li元素,也就是每一条目录所在元素
const nav = ref(null);
//获取nav中的li元素
let itemRefs = [];
const setItemRef = (el) => {
if (el) {
itemRefs.push(el);
}
};
onBeforeUpdate(() => {
itemRefs = [];
});
判断当前高亮目录li元素是否超过目录元素总高度的一半
这里是因为观察掘金的长目录,当高亮元素滚动到总高度一半时,才让nav元素滚动起来.
let mid = nav.value.clientHeight / 2; //滚动元素父元素的高度的一半
let offsetTop = itemRefs[activeIndex.value].offsetTop; //当前激活元素相对于父元素顶部的距离
if (offsetTop > mid ) {
nav.value.scrollBy(0, 32);
}
scrollBy(x,y)接收两个参数,横坐标与纵坐标,是相对于当前位置滚动指定距离
注意这里是让nav元素滚动,不是widow或者document,而且注意nav得有滚动条才有效
这里的32是我一个li元素的高度
判断当前高亮元素与上一个高亮元素的差值
这一点是因为,如果我们滚动的飞快,那么目录滚动的速度会跟不上,比如我们一次滚动跨越了三四个h标签,但是目录滚动只移动了一位,这样积累之下,当前的高亮元素就会消失在可视区域内.
if (oldValue === activeIndex.value) {
return;
}
let difference = activeIndex.value - oldValue //差值
if (offsetTop > mid ) {
nav.value.scrollBy(0, 32 * difference);
}
oldValue = activeIndex.value;
在全局中定义遍历oldValue = 0
如果当前activeIndex等于oldValue,就说明当前高亮元素没改变,不需要移动
通过difference可以知道当前activeIndex和上一个activeIndex相差多少,移动对应的距离,让高亮元素尽量在目录的中间位置.
写文章的时候现场发现一个bug,当文章滚动到最后,再向上滚动时,目录也会跟着移动,这样的话,高亮元素始终在最下面,这样不好看,所以我又改了改
if (oldValue === activeIndex.value) {
return;
}
let difference = activeIndex.value - oldValue //差值
if (offsetTop > itemRefs[itemRefs.length - 1].offsetTop - mid && !isDown) {
}
else if (offsetTop > mid ) {
nav.value.scrollBy(0, 31 * difference);
}
oldValue = activeIndex.value;
当文章在最下面的一部分,并且是向上滚动时,nav不跟着滚动,这个和在最上面没超过nav的一半不滚动是一个道理
这里的isDown表示的是文章是向上滚动还是向下滚动
let isDown = true;
//判断滚动方向
let scrollFunc = function (e) {
e = e || window.event;
if (e.wheelDelta) {
//判断浏览器IE,谷歌滑轮事件
if (e.wheelDelta > 0) {
//当滑轮向上滚动时
// console.log("滑轮向上滚动");
isDown = false;
}
if (e.wheelDelta < 0) {
//当滑轮向下滚动时
// console.log("滑轮向下滚动");
isDown = true;
}
} else if (e.detail) {
//Firefox滑轮事件
if (e.detail > 0) {
//当滑轮向上滚动时
isDown = false;
// console.log("滑轮向上滚动");
}
if (e.detail < 0) {
//当滑轮向下滚动时
isDown = false;
// console.log("滑轮向下滚动");
}
}
};
const mouseWheel = () => {
if (document.addEventListener) {
//火狐使用DOMMouseScroll绑定
document.addEventListener("DOMMouseScroll", scrollFunc, false);
}
//其他浏览器直接绑定滚动事件
document.addEventListener("mousewheel", scrollFunc);
};
到这里长目录跟随滚动的功能基本实现了.
解决点击跳转带来的bug
如果我们不点击跳转,单纯的跟随滚动高亮已经没问题了,但是如果点击跳转会如何?
看过前面内容的应该知道,点击之后高亮点击的目录内容,同时将当前的index值赋值给activeIndex.
前面我们写了oldValue和activeIndex做比较,如果不同就会滚动目录元素.
想象一下,如果我们滚动目录之后点击跳转,此时我们肯定可以看到自己点击的内容高亮了,但是此时activeIndex已经改变,而且因为我们滚动了nav目录,那么这两个值的差就会很大,造成的影响就是高亮内容直接被滚动到非可视区域了,我们看不见了.
这肯定不是我们想要的效果.
我用了一个比较简单的方法解决这个问题
//点击目录跳转
let isJump = false
const jump = (index) => {
activeIndex.value = index;
let target = document.getElementById(index).offsetTop;
if (target) {
window.scrollTo({
top: target - 80,
});
isJump = true
}
};
这是之前写的点击跳转的代码
我在这里加了个flag:isJamp,当我点击之后就设置成true
const watchActive = () => {
if (oldValue === activeIndex.value) {
return;
}
let difference = activeIndex.value - oldValue //差值
let mid = nav.value.clientHeight / 2; //滚动元素父元素的高度的一半
let offsetTop = itemRefs[activeIndex.value].offsetTop; //当前激活元素相对于父元素顶部的距离
oldValue = activeIndex.value;
if (isJump) {
isJump = false
return
}
if (offsetTop > itemRefs[itemRefs.length - 1].offsetTop - mid && !isDown) {
}
else if (offsetTop > mid ) {
nav.value.scrollBy(0, 31 * difference);
}
if (activeIndex.value === 0) {//后面解释这个
nav.value.scrollTo({
top: 0
})
}
};
这里是前面写的代码的整体内容,在scroll事件的回调函数中调用就行了.
这里我通过isJump的值来判断是不是点击跳转导致的activeIndex变化,如果是,后面滚动nav的代码就不会执行.这样就解决了bug.
但是不够完美,如果我点击的是偏下的目录,那么高亮的元素就一直在偏下的位置,不会在滚动中移动到中间位置
这里有个想法,还是根据高亮元素的位置改变移动距离的大小,让其始终在中间位置. 偷懒不写了.感觉有点麻烦
解决点击回到顶部带来的bug
前面提到点击回到顶部的功能也是我写的,所以我在测试时发现,如果我点击回到顶部之后,nav目录可视区域还是在点击回到顶部之前所对应的区域,也就是我看不到当前的高亮元素.
这个bug是怎么造成的呢.
if (offsetTop > mid ) {
nav.value.scrollBy(0, 31 * difference);
}
只有滚动元素所在位置大于nav总高度的一半才能执行滚动的代码.
点击回到顶部时,activeIndex = 0,对应的是第一个li标签,这肯定小于nav的高度的一半,所以这个滚动就不会执行
解决办法就是我前面写的
if (activeIndex.value === 0) {
nav.value.scrollTo({
top: 0
})
}
回到顶部之后,activeIndex = 0,这个时候执行一下scrollTo,将nav滚动到顶就行了.
最后看看效果吧,特意找了个做动图的工具展示效果