效果
当滚动鼠标的时候右侧的目录会跟着一起滚动,当点击目录的时候,也会定位到相应的标题处,目录容器除前三个和后三个之外,其余的都处于中间状态。
这是本项目的在线浏览地址:chenyajun.fun/#/catalogSc...
实现
下面这张图是所有的 JS 核心代码(52-18)34行,接下来就和大家简单分享一下实现过程~
原理图
通过滚动容器的滚动距离 - 文章内容元素的 offsetTop
也就是 getBoundingClientRect().top - offsetTop,如果满足条件就处理我们的右侧目录进行相应的滚动。 getBoundingClientRect().top:用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。简单说就是元素移动时可以得到其相对于浏览器视窗的实时位置,下面是MDN的解释:
Element.getBoundingClientRect() - Web API 接口参考 | MDN (mozilla.org)
offsetTop : 它返回当前元素相对于其 offsetParent 元素的顶部内边距的距离。
注意 : offsetParent 元素是一个指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的元素。
代码
先看几个变量
JS
const scrollContainer = ref(null) //内容区滚动容器元素
const catalogContent = ref(null) //标题加内容区目录元素
const catalogBox = ref(null) //目录容器
const currentCatalog = ref(0) //当前目录索引
内容容器滚动
JS
function scrollElement() {
const distance = Math.abs(scrollContainer.value.getBoundingClientRect().top) + 170 //增加一些距离目的为了快滚到时就激活对应目录
catalogContent.value.forEach(function (el, index) {
// 如果容器的滚动距离 大于或等于标题距离ul顶部的距离,就说明当前标题已经触顶
if (distance >= el.offsetTop) {
currentCatalog.value = index
}
})
setCatalogScroll()
}
当下面这个条件成立时,就说明滚动到了相应的目录,我们只需要激活相应的目录即可;
JS
if (distance >= el.offsetTop) {
currentCatalog.value = index
}
目录容器滚动
激活之后根据当前是第几个目录进行右侧的目录移动:
scrollTo:
scrollTo()
方法可以使界面滚动到给定元素的指定坐标位置。
JS
function setCatalogScroll() {
// 设置目录滚动距离
if (currentCatalog.value >= 3) {
catalogBox.value.scrollTo({
top: (currentCatalog.value - 3) * 65,
})
} else {
catalogBox.value.scrollTo({
top: 0,
})
}
}
如果当前的目录超过第三个,就将目录容器往下相应的进行移动,并且始终保持在中间部位;
JS
if (currentCatalog.value >= 3) {
catalogBox.value.scrollTo({
top: (currentCatalog.value - 3) * 65,
})
}
如果小于3就将其滚动到顶部即可;
JS
catalogBox.value.scrollTo({
top: 0,
})
点击滚动
点击对应的目录将左侧的内容直接滚动到顶部,而右侧的目录位置和上面一致,如果当前的目录超过第三个,就将目录容器往下相应的进行移动,并且始终保持在中间部位。
点击之后先设置 目录容器 进行滚动,然后直接将对应的内容区标题进行置顶即可,通过点击的索引控制。
JS
function jumpToCatalog(index) {
// 点击跳转当前目录
currentCatalog.value = index
setCatalogScroll()
catalogContent.value[index].scrollIntoView()
}
scrollIntoView:该方法会滚动元素的父容器,使被调用 scrollIntoView()
的元素对用户可见。
总结
该功能类似于楼层滚动的效果,只不过对右侧的目录滚动进行优化,滚动每次基于中间位置,总体来说难度不大,主要判断当前元素是否已经滚动到顶部即可,那么核心就是怎么处理这个问题。
如果觉得以上思路对你有任何帮助或者启发,可以给作者点下赞哦~你的鼓励就是作者最大的动力呢~~
源码
全部源码:
HTML
<script setup>
// 循环生成数据结构
const catalogTitle = ref([])
let i = 0
for (let index = 0; index < 12; index++) {
if (i >= 6) {
i = 0
}
catalogTitle.value.push({
title: `文章题目${i + 1}`, //标题
id: `${i + 1}`,
level: `${i + 1}`, //层级
left: (i + 1) * 15 + 'px', //左边距
})
i++
}
const canRun = ref(true) //节流 防止多次执行滚动事件
function handleScroll() {
// 内容区滚动函数
if (canRun.value) {
canRun.value = false
scrollElement() //滚动元素
setTimeout(() => {
canRun.value = true
}, 100)
}
}
const scrollContainer = ref(null) //内容区滚动容器元素
const catalogContent = ref(null) //标题加内容区目录元素
const currentCatalog = ref(0) //当前目录索引
const catalogBox = ref(null) //目录容器
function scrollElement() {
const distance = Math.abs(scrollContainer.value.getBoundingClientRect().top) + 170 //增加一些距离目的为了快滚到时就激活对应目录
catalogContent.value.forEach((el, index) => {
// 如果容器的滚动距离 大于或等于标题距离ul顶部的距离,就说明当前标题已经触顶
if (distance >= el.offsetTop) {
currentCatalog.value = index
}
})
setCatalogScroll()
}
function setCatalogScroll() {
// 设置目录滚动距离
if (currentCatalog.value >= 3) {
catalogBox.value.scrollTo({
top: (currentCatalog.value - 3) * 65,
})
} else {
catalogBox.value.scrollTo({
top: 0,
})
}
}
function jumpToCatalog(index) {
// 点击跳转当前目录
currentCatalog.value = index
setCatalogScroll()
catalogContent.value[index].scrollIntoView()
}
</script>
<template>
<div class="outer" @scroll="handleScroll">
<!-- 内容区 -->
<div ref="scrollContainer" class="catalog-content">
<div v-for="(item, index) in catalogTitle" ref="catalogContent":key="item.id">
<h1 v-if="item.level === '1'">{{ item.title }}</h1>
<h2 v-if="item.level === '2'">{{ item.title }}</h2>
<h3 v-if="item.level === '3'">{{ item.title }}</h3>
<h4 v-if="item.level === '4'">{{ item.title }}</h4>
<h5 v-if="item.level === '5'">{{ item.title }}</h5>
<h6 v-if="item.level === '6'">{{ item.title }}</h6>
<div class="catalog-message"></div>
</div>
<div style="height: 1000px"></div>
</div>
<!-- 目录区 -->
<div class="catalog-name">
<div class="catalog-name-title">目录</div>
<div ref="catalogBox" class="catalog-box">
<div
v-for="(item, index) in catalogTitle"
:key="item.id"
:style="{
marginLeft: item.left,
color: currentCatalog === index ? '#1e80ff' : '#000000',
marginTop: index === 0 ? '10px' : '30px',
}"
class="catalog-title"
@click="jumpToCatalog(index)"
>
{{ item.title }}
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
html,
body {
margin: 0;
height: 100%;
}
$i: 7;
@while $i > 0 {
h#{$i} {
margin-left: 10px;
}
$i: $i - 1;
}
.outer {
width: 100%;
height: 100%;
position: relative;
background-color: #f2f3f5;
display: flex;
justify-content: center;
overflow-y: scroll;
}
.catalog-content {
display: inline-block;
background-color: #ffffff;
width: 800px;
border-radius: 10px;
margin-top: 20px;
.catalog-message {
height: 400px;
background-color: #bfa;
}
}
.catalog-name {
position: fixed;
right: 20px;
top: 20px;
width: 20%;
border-radius: 10px;
background-color: #ffffff;
.catalog-box {
max-height: 312px;
overflow-y: auto;
.catalog-title {
margin-left: 15px;
margin-bottom: 15px;
&:hover {
color: #1e80ff;
cursor: pointer;
}
}
}
.catalog-name-title {
font-weight: 500;
margin-top: 15px;
margin-left: 15px;
margin-bottom: 35px;
position: relative;
&:after {
content: '';
width: 95%;
position: absolute;
height: 1px;
top: 32px;
left: 0;
background-color: #e4e6eb;
}
}
}
.catalog-box::-webkit-scrollbar {
display: none;
}
</style>