IntersectionObserver + scrollIntoView 实现电梯导航

news/2024/11/20 14:38:44/文章来源:https://www.cnblogs.com/zsxblog/p/18346207

电梯导航也被称为锚点导航,当点击锚点元素时,页面内相应标记的元素滚动到视口。而且页面内元素滚动时相应锚点也会高亮。电梯导航一般把锚点放在左右两侧,类似电梯一样。常见的电梯导航效果如下,比如一些官方文档中:

image

image

之前可能会用 getBoundingClientRect() 判断元素是否在视口中来实现类似效果,但现在有更方便的方法了,那就是 IntersectionObserver + scrollIntoView,轻松实现电梯导航。

scrollIntoView() 介绍

scrollIntoView() 方法会滚动元素的父容器,使元素出现在可视区域。默认是立即滚动,没有动画效果。

如果要添加动画效果,可以这么做:

scrollIntoView({behavior: 'smooth'  // instant 为立即滚动
})

它还有两个可选参数 blockinline

block 表示元素出现在视口时垂直方向与父容器的对齐方式,inline 表示元素出现在视口时水平方向与父容器的对齐方式。

他们同样都有四个值可选 startcenterend 、nearest。默认为 start;

scrollIntoView({behavior: 'smooth',block:'center',inline:'center',
})

对于 block

  • start  将元素的顶部和滚动容器的顶部对齐。

  • center  将元素的中心和滚动容器的中心垂直对齐。

  • end  将元素的底部和滚动容器的底部对齐。

对于 inline

  • start 将元素的左侧和滚动容器的左侧对齐。

  • center  将元素的中心和滚动容器的中心水平对齐。

  • end  将元素的右侧和容器的右侧对齐。

nearest 不论是垂直方向还是水平方向,只要出现在视口任务就完成了。可以理解为以最小移动量让元素出现在视口,(慵懒移动)。如果元素已经完全出现在视口中,则不会发生变化。

通过下面动图来感受这个变化,下面滚动容器中有四行五列,包含了从字母 AT。点击 出现在视口 的按钮会取三个下拉框的值作为参数来调用 scrollIntoView() 方法。

image

再来看看设置为 nearest 后的滚动情况

image

当字母 G 在视口内时,调用方法滚动容器不会发生变化。当 G 不完全在视口内,则会滚动到完全出现在视口内为止。

在这里可以查看这个完整例子 scrollIntoView 可选项参数实践(codepen)

而且 scrollIntoView 兼容性也很好

image

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 的兼容性也很好:

image

掌握了 IntersectionObserver + scrollIntoView 的用法,实现电梯导航就简单了。

简单写一个电梯导航的 htmlcss

<div class="a" style="background:aqua;">第一章</div>
<div class="b" style="background: blueviolet;">第二章</div>
<div class="c" style="background: chartreuse;">第三章</div>
<div class="d" style="background: darkgoldenrod;">第四章</div>
<div class="e" style="background: firebrick;">第五章</div>
<div class="f" style="background: gold;">第六章</div>
<div class="g" style="background: hotpink;">第七章</div>
<ul class="rightBox"><li class="aLi">第一章</li><li class="bLi">第二章</li><li class="cLi">第三章</li><li class="dLi">第四章</li><li class="eLi">第五章</li><li class="fLi">第六章</li><li class="gLi">第七章</li>
</ul>
html,
body {width: 100%;height: 100%;background-color: #fff;
}ul,li{list-style: none;}body {padding: 20px 0;
}div{width: 60%;height: 70%;border-radius: 10px;margin-left: auto;margin-right: auto;opacity: 0.4;display: flex;justify-content: center;align-items: center;font-size: 30px;font-weight: bold;color: #000;
}div+div {margin-top: 20px;
}.rightBox {position: fixed;right: 20px;top: 50%;color: teal;transform: translatey(-50%);
}li {cursor: pointer;box-sizing: border-box;border: 1px solid #fff;border-radius: 4px;padding: 8px 12px;
}li:hover {background: #f5d2c4;
}.active {background: #f5d2c4;
}

预览如下:

image


第一步:点击右边的导航菜单,利用 scrollIntoView 方法使内容区域对应的元素出现在可视区域中。

    let rightBox = document.querySelector('.rightBox')rightBox.addEventListener('click', function (e) {let target = e.target || e.srcElement;if (target && !target.classList.contains('rightBox')) {document.querySelector('.' + target.className.replace('Li', '')).scrollIntoView({behavior: 'smooth',block: 'center'})}}, false)

image


第二步:页面容器滚动时,当目标元素出现在检测区域内则联动改变对应导航的样式。

这里 threshold 被设置为 1,也就是当目标元素完全显示在可视区域时执行回调,改变导航菜单的样式。

let observer = new IntersectionObserver(function (entries) {entries.forEach(entry => {let target = document.querySelector('.' + entry.target.className + 'Li')if (entry.isIntersecting) { // 出现在检测区域内document.querySelectorAll('li').forEach(el => {if(el.classList.contains('active')){el.classList.remove('active')}})if (!target.classList.contains('active')) {target.classList.add('active')}}})
}, {threshold: 1
})document.querySelectorAll('div').forEach(el => {observer.observe(el)
})

效果如下:

image


基本要求达到了,不过在滚动过程中,还有些问题。比如连续两个元素来回切换时,第二个元素比第一个元素在检测区域显示的比例更高,虽然没达到 100%,这时候导航菜单显示还是第一个元素的。见下图:

image


所以这里可以控制的更细,两个元素之间谁显示的比例更高时就高亮谁的导航菜单。

let observer = new IntersectionObserver(function (entries) {entries.forEach(entry => {if (entry.isIntersecting && entry.intersectionRatio > 0.65) {}})
}, {threshold: [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
})

这里设置了 threshold: [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],当目标元素出现在检测区域的比例达到 20%,30%,40%,50%,60%,70%,80% 的时候会执行回调函数,在回调函数里,目标元素可见并且在检测区域显示的比例达到 65% 时高亮导航菜单。这样效果就好些了:

image

在这里可以查看这个完整例子 IntersectionObserver + scrollIntoView 实现电梯导航

当然,具体还是看实际元素块大小和业务需求来定。

如有帮助,帮忙点点赞,感谢~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/779013.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pthread_once导致死锁

在一个pthread_once方法内又再次调用了这个pthread_once导致死锁。分析下这个pthread_once的源码:可以看到这个pthread_once_t结构体就是一个整形数字加自旋锁。 int ___pthread_once (pthread_once_t *once_control, void (*init_routine) (void)) {/* Fast path. See __pth…

1.3 功率电感选型----硬件设计指南(持续补充更新)

本系列文章是笔者总结多年工作经验,结合理论与实践进行整理备忘的笔记。希望能在帮助自己温习整理避免遗忘的同时,也能帮助其他需要参考的朋友。笔者会不定期进行查漏补缺。如有谬误,欢迎大家进行指正。 一、设计要点 1.电流降额建议按照1-10%-电感精度进行,主要设计参数是…

基于simulink的分布式发电系统自动重合闸的建模与仿真分析

1.课题概述在配电系统中,80%-90%的故障都是瞬时故障。发生故障时,线路被保护迅速断开,随即重合闸。当分布式电源接入配电网后,线路发生故障后重合闸,此时分布式电源没有跳离线路,这将产生两种潜在威胁,即非同期重合闸和故障点电弧重燃。非同期重合闸:当线路上发生故障,…

Windows10 安装编译后的 pysqlcipher3-1.2.1 基于 Python 3.8.10

Windows10 安装编译后的 pysqlcipher3-1.2.1 基于 Python 3.8.10 本文主要是将直接安装编译后的文件,不一定的成功,但是可以尝试使用,若无法直接安装,请参考编译过程,自行编译安装,编译过程见这里 安装 pysqlcipher3 这里用 32位 举例 因为 64位 安装完全相同,只需要把对…

17 模块subprocess、re

1. subprocess模块 1.1 概念subprocess模块启动一个新进程,并连接到它们的输入/输出/错误管道,从而获取返回值 简单理解:可以远程连接电脑(socket模块) 1.2 Popen方法import subprocessres = subprocess.Popen([help, ver], # windows中执行的命令要放在列表里面,命令单…

STM32学习记录(八):DMA

什么是DMA? DMA在之前的学习中已经用过了。那么,什么是DMA?Direct memory access (DMA) is used in order to provide high-speed data transfer between peripherals and memory as well as memory to memory. Data can be quickly moved by DMA without any CPU actions…

CTF—Misc基础

一:文件操作与隐写 1、文件类型的识别 1、文件头完好情况: (1)file命令 使用file命令识别:识别出file.doc为jpg类型(2)winhex 通过winhex工具查看文件头类型,根据文件头部内容去判断文件的类型eg:JPG类型(3)notepad++ 下载HEXeditor插件,查看文件的头部信息,和010e…

位运算符

1.与(&)2.或(|)3.亦或(^)4.非(~)5.关于位运算的面试题 问:如何用电脑将2乘8最快算出?6.左移 右移的底层原理

雷达气象学(7)——反射率因子图分析(气象回波篇)

从本篇文章开始介绍反射率因子图(即雷达回波强度图)的分析与识别方法。 目录7.0 雷达回波的分类7.1 层状云降水回波7.2 积状云降水回波(对流性降水回波)7.3 层积混合降水回波7.4 零度层亮带7.5 晴空回波 7.0 雷达回波的分类 雷达回波可分为气象回波和非气象回波: \[雷达回…

(Jmeter新玩法)Python 调 Jmeter执行参数化jmx脚本

# Python 调 Jmeter执行参数化jmx脚本import os from os.path import join import time import re from string import Templatejmeter_Home = r"F:\softtotal\xxx\bin\jmeter.bat"# jmx文件路径 currpath = os.path.dirname(os.path.realpath(__file__)) # 要运行的…