我使用34行 JS 核心代码实现掘金官网的目录滚动效果

效果

当滚动鼠标的时候右侧的目录会跟着一起滚动,当点击目录的时候,也会定位到相应的标题处,目录容器除前三个和后三个之外,其余的都处于中间状态。

这是本项目的在线浏览地址:​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() 的元素对用户可见。

总结

该功能类似于楼层滚动的效果,只不过对右侧的目录滚动进行优化,滚动每次基于中间位置,总体来说难度不大,主要判断当前元素是否已经滚动到顶部即可,那么核心就是怎么处理这个问题。

如果觉得以上思路对你有任何帮助或者启发,可以给作者点下赞哦~你的鼓励就是作者最大的动力呢~~

源码

GitHub地址:chenyajun-create/juejinCatalogScroll: Imitate the directory scrolling effect on the juejin official website (github.com)

全部源码:

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>
相关推荐
darling3312 分钟前
vue+elementUI 表单项赋值后无法修改的问题
前端·javascript·vue.js·elementui·ecmascript
呆呆小雅31 分钟前
四、Vue 条件语句
前端·javascript·vue.js
LUwantAC42 分钟前
一篇文章学会HTML
前端·javascript·html
小林爱1 小时前
【Compose multiplatform教程12】【组件】Box组件
前端·kotlin·android studio·框架·compose·多平台
山沟沟里的娃1 小时前
pinia从0到1
vue.js
发呆的薇薇°1 小时前
React里使用lodash工具库
javascript·react.js
光影少年1 小时前
js原型和原型链
开发语言·javascript·原型模式
风清云淡_A1 小时前
【再学javascript算法之美】前端面试频率比较高的基础算法题
前端·javascript
xcLeigh1 小时前
HTML5实现喜庆的新年快乐网页源码
前端·html·html5
DT——1 小时前
HTTPS验证流程
前端·https