vue+element-ui实现anchor组件

666 阅读5分钟

前言

最近在一个将react项目转化为vue项目的过程中,遇到了需要自己去实现element-ui中所没有的anchor组件。样式大致如下:

屏幕截图 2024-02-21 144333.png

该组件功能主要体现在如下两点:

  1. 滚动屏幕可以高亮相应区域的标题;
  2. 点击导航标题可以实现内容的跳转;

在清楚了开发需求后,便可以开始了:

实现屏幕滚动可以高亮相应的标题

首先给待滚动的dom结构需要绑定滚动scroll事件,在一般的开发中通常为根组件(即#app),该事件当页面进行滚动时便可触发;

在组件挂载时: this.scrollWrap.addEventListener('scroll',this.scrollHandle)

其中 scrollwrap即为待滚动的元素(window或某个具体的dom结构)

如何实现滚动到不同区域即可高亮呢,那么我们首先就要知道不同区域距离这个dom顶部的位置,根据我们滚动的偏移量与位置进行比对,满足一定的条件下,即可添加active样式。

即在执行滚动函数前,我们首先需要求得各个区域的“位置”,如何获取每个区域呢,很简单,我们给组件传入每个区域的id值,进而可以获取到各个区域元素:

export default {
    name: 'AnchorNav',
    props: {
        // 滚动的元素,不传默认取离元素最近的可以滚动的元素
        scrollDom:{
            type:String,
            required:true,
            default:''
        },
        //滚动区域名单
        anchorList:{
            type:Array,
            required:true
        }
    },

获取位置函数,这里用到了十分重要的offsetParent属性:

setOffsetTopList(){
    // console.log('待遍历anchor',this.anchorList)
    this.offsetTopList = this.anchorList.map((item)=>{
        // console.log('具体item',item)
        const element = document.getElementById(item.anchor)
        // console.log('element:',element)
        return {
            anchor:item.anchor,
            offsetTop:element.offsetTop
        }
    })
},

HTMLElement.offsetParent 是一个只读属性,返回一个指向最近的包含该元素的定位元素或者table,td,th,body元素。当元素的 style.display 设置为 "none" 时,offsetParent 返回 null

因此,如果我们想要获得该HTMLElement相对执行滚动的元素的高度,切记弄清楚其是否为其第一级定位元素!

依次获得了各个区域元素的位置量后,滚动函数处理便是依次进行判断是否进入了当前区域:

scrollHandle({target}){
    let flag = true
    const curScrollTop = target.scrollTop //当前滚动高度
    if (!this.offsetTopList.lenth) {
        this.setOffsetTopList()
    }
    //获取一个保存页面上所有锚点位置信息的数组
    const len = this.offsetTopList.length
    //获取第一个锚点的位置
    const min = this.offsetTopList[0].offsetTop
    //未匹配到任何锚点
    if (curScrollTop < min) {
        // sidebarStore.setAnchor('')
        this.active = '' //无匹配
        return
    }
    for (let i = len - 1; i >= 0; i--) {
        const curReference = this.offsetTopList[i].offsetTop // 当前参考值
        if (flag && curScrollTop >= curReference-20) {
            flag = false
            this.active = this.offsetTopList[i].anchor
        }
    }
},

this.active的作用是通过判断与当前元素id是否一致,选择是否添加样式效果,如标题高亮;

这里是从后往前进行遍历:因为我们要找到的是距离最接近的那一个

点击anchor进行跳转

根据待跳转的id通过offsetTop属性获取其位置,得到该位置后我们便可以控制父元素的滚动,一开始我是使用的scrollTo函数,offsetTop即为父元素需要滚动的量,即:

scrollTo(offsetTop){
    console.log('调用滚动函数')
    console.log('父级元素为:', this.scrollWrap)
    this.scrollWrap.scrollTo({
        top:offsetTop,
        behavior:'smooth'
    })
},

但在这个过程中我发现由于我们一直开启着scrollHandle函数(在组件一挂载便绑定在了父元素的scroll事件上),这将导致我们在跳转的过程中所经过的区域也会很快高亮然后取消的问题。

为了解决这个问题,如果我们跳转的过程中先取消scroll事件的监听,那么应该什么时候恢复监听呢, 查阅资料,我们可以使用animateScrollTo的库进行开发,该库模拟ScrollTo的实现,并且以Promise的形式提供了then函数,方便我们在滚动完后及时进行处理。

库函数地址:github.com/Stanko/anim…

安装

npm i animated-scroll-to -S

使用

//使用模拟滚动代替真实滚动,避免由于我们在跳转anchor时一直开启滚动监听事件,造成经过的anchor都高亮的问题
animateScrollTo(element.offsetTop,{speed:100, elementToScroll: this.scrollWrap}).then(()=>{
    this.scrollWrap.addEventListener('scroll',this.scrollHandle)
})

第一个参数为滚动偏移量,即待跳转区域的位置;第二个参数为对象形式,主要是控制滚动元素、滚动速度;滚动速度为100的情况下与scrollTo函数本身是一致的。

因此跳转函数完整即为:

anchorPosition(anchor) {
    //取消监听
    this.scrollWrap.removeEventListener('scroll', this.scrollHandle)
    console.log('点击锚点为:',anchor)
    const element = document.getElementById(anchor)
    console.log('滚动元素为',element)
    //offsetTop:距离父元素顶部的偏移
    console.log('偏移量为:',element.offsetTop)
    // this.scrollTo(element.offsetTop)
    this.active = ''
    this.active = anchor
    // this.$nextTick(()=>{
    //     this.active = anchor
    // })
    //恢复监听
    //使用模拟滚动代替真实滚动,避免由于我们在跳转anchor时一直开启滚动监听事件,造成经过的anchor都高亮的问题
    animateScrollTo(element.offsetTop,{speed:100, elementToScroll: this.scrollWrap}).then(()=>{
        this.scrollWrap.addEventListener('scroll',this.scrollHandle)
    })
},

样式新问题

这里主要是指,当我们点击跳转到最下层的内容块时,最先触发的是我们的anchorPosition函数,这时会给我们高亮下层的标题,但是一旦跳转结束,scroll事件恢复绑定,便会执行一次scrollHandle函数,根据位置匹配,这时会取消下层标题的高亮,进而点亮的是上一层的标题。

出现该问题的原因其实很好理解,就是在上一层的内容中其实已经到达了顶部,无法再滚动,这时是无论如何都不会进入到下面的区域内容的,因此只需要对我们的scrollHandle函数进行一下修改:

scrollHandle({target}){
    //如果已经到达底部,不再进行后面的判断:避免anchor高亮后经过判断又取消的问题
    //scrollTop + clientHeight == scrollHeight
    //是否找到了匹配的滚动位置
    let flag = true
    const curScrollTop = target.scrollTop //当前滚动高度
    const clientHeight = target.clientHeight //
    const scrollHeight = target.scrollHeight //页面总高度
    if(curScrollTop+clientHeight === scrollHeight){
        console.log('已经到达底部')
        flag = false
    }
    // console.log('当前滚动位置',curScrollTop)
    if (!this.offsetTopList.lenth) {
        this.setOffsetTopList()
    }
    //获取一个保存页面上所有锚点位置信息的数组
    const len = this.offsetTopList.length
    //获取第一个锚点的位置
    const min = this.offsetTopList[0].offsetTop
    //未匹配到任何锚点
    if (curScrollTop < min) {
        // sidebarStore.setAnchor('')
        this.active = '' //无匹配
        return
    }
    for (let i = len - 1; i >= 0; i--) {
        const curReference = this.offsetTopList[i].offsetTop // 当前参考值
        if (flag && curScrollTop >= curReference-20) {
            // console.log('进入某个区域')
            flag = false
            this.active = this.offsetTopList[i].anchor
        }
    }
},

在scrollHandle时,首先判断页面是否达到了底部,参考链接:www.jianshu.com/p/3a53bd54f…

滚动高度+窗口高度=页面总高度,如果已经达到了底部,不再进行后续的匹配,即优先anchorPosition过程中的高亮:

image.png