vue+IntersectionObserver + scrollIntoView 实现电梯导航
一、电梯导航
电梯导航也被称为锚点导航,当点击锚点元素时,页面内相应标记的元素滚动到视口。而且页面内元素滚动时相应锚点也会高亮。电梯导航一般把锚点放在左右两侧,类似电梯一样。
二、scrollIntoView() 介绍
scrollIntoView()
方法会滚动元素的父容器,使元素出现在可视区域。默认是立即滚动,没有动画效果。
如果要添加动画效果,可以这么做:
scrollIntoView({ behavior: 'smooth' // instant 为立即滚动 })
它还有两个可选参数 block
和 inline
。
block
表示元素出现在视口时垂直方向与父容器的对齐方式,
inline
表示元素出现在视口时水平方向与父容器的对齐方式。
他们同样都有四个值可选 start
、center
、end
、nearest
。默认为 start
;
scrollIntoView({ behavior: 'smooth', block:'center', inline:'center', })
对于 block
-
start
将元素的顶部和滚动容器的顶部对齐。 -
center
将元素的中心和滚动容器的中心垂直对齐。 -
end
将元素的底部和滚动容器的底部对齐。
对于 inline
-
start
将元素的左侧和滚动容器的左侧对齐。 -
center
将元素的中心和滚动容器的中心水平对齐。 -
end
将元素的右侧和容器的右侧对齐。
而 nearest
不论是垂直方向还是水平方向,只要出现在视口任务就完成了。可以理解为以最小移动量让元素出现在视口,(慵懒移动)。如果元素已经完全出现在视口中,则不会发生变化。
在这里可以查看这个完整例子 scrollIntoView 可选项参数实践(codepen)
而且 scrollIntoView 兼容性也很好
三、IntersectionObserver 介绍
Intersection Observer API(交叉观察器 API) 提供了一种异步检测目标元素与祖先元素或顶级文档的视口相交情况变化的方法。也就是能判断元素是否在视口中,并且能监听元素在视口中出现的可见部分的比例,从而可以执行我们自定义的逻辑。
由于是异步,也就不会阻塞主线程,性能自然比之前的频繁执行 getBoundingClientRect()
判断元素是否在视口中要好。
创建一个 IntersectionObserver
let options = {
root: document.querySelector(selector),
rootMargin: "0",
threshold: 1.0,
};
let observer = new IntersectionObserver(callback, options);
let target = document.querySelector(selector);
observer.observe(target); //监听目标元素
通过调用 IntersectionObserver
构造函数可以创建一个交叉观察器,构造函数接收两个参数,一个回调函数和一个可选项。上面例子中,当元素完全出现(100%)在视口中时会调用回调函数。
可选项
-
root
用作视口的元素,必须是目标的祖先。默认为浏览器视口。 -
rootMargin
根周围的边距,也就是可以限制根元素检测视口的大小。值的方向大小和平常用的margin
一样,例如"10px 20px 30px 40px"
(上、右、下、左)。只不过正数是增大根元素检测范围,负数是减小检测范围。
比如设置一个可以滚动的 div 容器为根元素,宽高都为1000px。 此时设置 rootMargin:0
表示根元素检测视口大小就是当前根元素可视区域大小,也就是 1000px * 1000px。设置 rootMargin:25% 0 25% 0
表示上下边距为 25%,那么检测视口大小就是 1000px * 500px。
threshold
一个数字或一个数字数组,表示目标出现在视口中达到多少百分比时,观察器的回调就应该执行。如果只想在能见度超过 50% 时检测,可以使用 0.5 的值。如果希望每次能见度超过 25% 时都执行回调,则需要指定数组 [0, 0.25, 0.5, 0.75, 1]。默认值为 0(这意味着只要有一个像素可见,回调就会运行)。值为 1.0 意味着在每个像素都可见之前,阈值不会被认为已通过。
回调函数
当目标元素匹配了可选项中的配置后,会触发我们定义的回调函数
let options = {
root: document.querySelector(selector),
rootMargin: "0",
threshold: 1.0,
};
let observer = new IntersectionObserver(function (entries) { entries.forEach(entry => {
})
}, options);
entries 表示被监听目标元素组成的数组,数组里面每个 entry 都有下列一些值
-
entry.boundingClientRect
返回目标元素的边界信息,值和getBoundingClientRect()
形式一样。 -
entry.intersectionRatio
目标元素和根元素交叉的比例,也就是出现在检测区域的比例。 -
entry.intersectionRect
返回根和目标元素的相交区域的边界信息,值和getBoundingClientRect()
形式一样。 -
entry.isIntersecting
返回true或者fasle,表示是否出现在根元素检测区域内 -
entry.rootBounds
返回根元素的边界信息,值和getBoundingClientRect()
形式一样。 -
entry.target
返回出现在根元素检测区域内的目标元素 -
entry.time
返回从交叉观察器被创建到目标元素出现在检测区域内的时间戳
比如,要检测目标元素有75%出现在检测区域中就可以这样做:
entries.forEach(entry => {
if(entry.isIntersecting && entry.intersectionRatio>0.75){
}
})
监听目标元素
创建一个观察器后,对一个或多个目标元素进行观察。
//单个元素监听
let target = document.querySelector(selector);
observer.observe(target);
//多个元素监听
document.querySelectorAll('div').forEach(el => {
observer.observe(el)
})
掌握了 IntersectionObserver + scrollIntoView
的用法,实现电梯导航就简单了。
四、代码实现
<template>
<div class="navigation">
<div class="navigation_left">
<div v-for="(v, i) in list" :key="i" :id="i" class="navigation_left_box">
<h1 :id="v.href">{{ v.name }}</h1>
<div class="box">
<div v-for="(v1, i1) in v.children" :key="i1" class="box_item">
{{ v1.name }}
</div>
</div>
</div>
</div>
<div class="navigation_right">
<div class="box">
<a class="box_item" :class="listIndex == i ? 'active' : ''" @click="scrollToAnchor(v.href, i)"
v-for="(v, i) in list" :key="i">{{ v.name }}</a>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
list: [
{
name: '一、标题1',
href: '#t1',
children: [
{ name: '内容1' },
{ name: '内容2' },
{ name: '内容3' },
{ name: '内容4' },
{ name: '内容5' },
{ name: '内容6' },
{ name: '内容7' },
{ name: '内容8' },
{ name: '内容9' },
{ name: '内容10' },
{ name: '内容11' },
]
},
{
name: '二、标题2',
href: '#t2',
children: [
{ name: '内容1' },
{ name: '内容2' },
{ name: '内容3' },
{ name: '内容4' },
{ name: '内容5' },
{ name: '内容6' },
{ name: '内容7' },
{ name: '内容8' },
{ name: '内容9' },
{ name: '内容10' },
]
},
{
name: '三、标题3',
href: '#t3',
children: [
{ name: '内容1' },
{ name: '内容2' },
{ name: '内容3' },
{ name: '内容4' },
{ name: '内容5' },
{ name: '内容6' },
{ name: '内容7' },
{ name: '内容8' },
{ name: '内容9' },
{ name: '内容10' },
]
},
{
name: '四、标题4',
href: '#t4',
children: [
{ name: '内容1' },
{ name: '内容2' },
{ name: '内容3' },
{ name: '内容4' },
{ name: '内容5' },
{ name: '内容6' },
{ name: '内容7' },
{ name: '内容8' },
{ name: '内容9' },
{ name: '内容10' },
]
},
{
name: '五、标题5',
href: '#t5',
children: [
{ name: '内容1' },
{ name: '内容2' },
{ name: '内容3' },
{ name: '内容4' },
{ name: '内容5' },
{ name: '内容6' },
{ name: '内容7' },
{ name: '内容8' },
{ name: '内容9' },
{ name: '内容10' },
]
},
{
name: '六、标题6',
href: '#t6',
children: [
{ name: '内容1' },
{ name: '内容2' },
{ name: '内容3' },
{ name: '内容4' },
{ name: '内容5' },
{ name: '内容6' },
{ name: '内容7' },
{ name: '内容8' },
{ name: '内容9' },
{ name: '内容10' },
]
}
],
listIndex: 0,
}
},
mounted () {
// 创建一个IntersectionObserver实例
const observer = new IntersectionObserver((entries) => {
for (let index = 0; index < entries.length; index++) {
if (entries[index].isIntersecting) {
console.log(entries[index]);
// 目标元素进入视窗
// 根据监听元素的属性id来给右侧的元素选中
this.listIndex = entries[index].target.id
}
}
},
{
// root 用作视口的元素,必须是目标的祖先。默认为浏览器视口。
// hreshold 一个数字或一个数字数组,表示目标出现在视口中达到多少百分比时
// 值为 1.0 意味着在每个像素都可见之前,阈值不会被认为已通过。
threshold: 1
}
);
// 选择所有需要观察的元素,并开始观察它们
const elementsToObserve = document.querySelectorAll('.navigation_left_box');
elementsToObserve.forEach(element => {
//监听目标元素
observer.observe(element);
});
},
methods: {
scrollToAnchor (anchorId, i) {
this.listIndex = i
const element = document.getElementById(anchorId);
if (element) {
// 滚动动画 滚动到目前位置
element.scrollIntoView({ behavior: 'smooth' });
}
}
},
}
</script>
<style lang="scss" scoped>
.navigation {
position: relative;
display: flex;
.navigation_left {
flex: 1;
h1 {
padding: 10px;
}
.box {
display: flex;
flex-wrap: wrap;
.box_item {
width: 30%;
height: 100px;
border-radius: 4px;
background-color: #E4CCFF;
line-height: 100px;
text-align: center;
font-size: 20px;
font-weight: 500;
box-sizing: border-box;
--n: 3;
/* 一行几个 */
--space: calc(100% - var(--n) * 30%);
/* 一行减去item的宽度后剩下的间距 */
--leftRight: calc(var(--space) / var(--n) / 2);
/* 每个item左右的间距 */
margin: 10px var(--leftRight);
}
}
}
.navigation_right {
width: 200px;
border-left: solid 1px #eee;
position: relative;
.box {
position: fixed;
top: 70px;
width: 200px;
padding-top: 10px;
z-index: 999;
.box_item {
display: block;
cursor: pointer;
font-size: 16px;
padding: 10px 5px;
text-align: center;
}
.box_item:hover {
background-color: #d5d5d54a;
}
.active {
background-color: #d5d5d54a;
}
}
}
}
</style>