我使用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>
相关推荐
四喜花露水24 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy34 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
工业甲酰苯胺1 小时前
C# 单例模式的多种实现
javascript·单例模式·c#
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生2 小时前
回调数据丢了?
运维·服务器·前端
丶21362 小时前
【鉴权】深入了解 Cookie:Web 开发中的客户端存储小数据
前端·安全·web