实现一个 Vue 吸顶锚点组件方法(3)

<template> <div> <el-radio-group v-model="selector" size="mini" @change="handleMenuChange"> <el-radio-button v-for="menu in menus" :key="menu.value" :label="menu.value"> {{ menu.label }} </el-radio-button> </el-radio-group> </div> </template> <script> // 添加缓动函数 import { tween } from 'shifty' // 类似 lodash.get 但处理了 null 类型 import { get as _get } from 'noshjs' import { getFirstScrollElement } from 'util.js' export default { props: { // 滚动区距离可视区顶部的高度 top: { type: Number, default: 0 }, menus: { type: Array, default: [] } }, data() { return { selector: '' } }, watch: { menus: { immediate: true, handler(list) { this.selector = _get(list, [0, 'value'], '') } } }, methods: { handleMenuChange(selector) { const scrollElement = document.querySelector(select) const rootScrollElement = getFirstScrollElement(scrollElement) if (scrollElement && rootScrollElement) { const offsetTop = scrollElement.offsetTop + scrollElement.clientTop const offsetHeight = _get(this.$el, ['parentElement', 'offsetHeight'], 0) const top = offsetTop - this.top - offsetHeight // 做一个缓动处理 tween({ from: { x: rootScrollElement.scrollTop }, to: { x: top }, duration: 500, easing: 'easeOutQuint', step: ({ x }) => { rootScrollElement.scrollTop = x } }).then(({ x }) => { rootScrollElement.scrollTop = x }) } } } } </script>

锚点与视图联动

接下来我们来看看最后一个功能,当用户滚动内容区时,高亮距离按钮组件最近的那个元素所对应的按钮。这个功能我可以看成是目录导航,当我们查看不同内容时高亮对应的目录。

这个功能如何实现呢,我们来分析一下,当查看不同内容时会滚动屏幕,所以我们要给按钮的父级滚动元素绑定 `scroll` 事件。判断当前滚动区距离按钮最近的元素,我们需要在这个元素上添加与按钮中的值对应的 css 选择器。当内容区发生滚动时根据按钮获取内容区中所有的元素,然后将滚动区元素的 `scrollTop` 减去按钮元素的高度,即得出按钮下方的滚动高度,然后再遍历这些元素头部和尾部是否包含了这个滚动高度,然后找到这个元素对应的按钮。

上面的结论已经可以完成,但存在一些问题,先说第一个问题导致按钮导航失效,只导航到下一个按钮边结束。这个问题不一定会所有人都遇到,之所以我会遇到这个问题,是因为我用了 `Element` 的 `Radio` 组件,要高亮的时候变更了 v-model 的值导致。而点击按钮时会触发滚动,就会和联动高亮的事件冲突了,所以用一个 `isScroll` 变量标记当前是否是锚点定位状态,定位状态不触发滚动操作。

<template> <div> <el-radio-group v-model="selector" size="mini" @change="handleMenuChange"> <el-radio-button v-for="menu in menus" :key="menu.value" :label="menu.value"> {{ menu.label }} </el-radio-button> </el-radio-group> </div> </template> <script> import { tween } from 'shifty' import { get as _get } from 'noshjs' import { getFirstScrollElement } from 'util.js' import TabMenus from 'components/tab-menus.vue' export default { props: { top: { type: Number, default: 0 }, menus: { type: Array, default: [] }, parent: { type: String, default: '' } }, data() { return { menu: '', isScroll: true, isMounted: false, scrollTop: 0, anchorChange: false, rootScrollElement: '' } }, mounted() { this.isMounted = true this.getScrollElement() }, watch: { parent: { immediate: true, handler: 'getScrollElement' }, menus: { immediate: true, handler(list) { this.menu = _get(list, [0, 'prop'], '') } }, scrollTop(v) { if (this.anchorChange) { // 切换按钮会滚动视图,$nextTick 之后按钮值改变了,但滚动可能还没有结束,所以需要打个标记。 this.isScroll = true } } }, methods: { handleMenuChange(select) { this.isScroll = false this.anchorChange = false // 滚动高度等于元素距离可视区头部高度减去元素自身高度与元素上边框高度以及滚动区距离可视区头部的高度。 const scrollElement = document.querySelector(select) if (scrollElement && this.rootScrollElement) { const offsetTop = scrollElement.offsetTop + scrollElement.clientTop const offsetHeight = _get( this.$el, ['parentElement', 'offsetHeight'], 0 ) const top = offsetTop - this.top - offsetHeight // 做一个缓动处理 tween({ from: { x: this.rootScrollElement.scrollTop }, to: { x: top }, duration: 500, easing: 'easeOutQuint', step: ({ x }) => { this.rootScrollElement.scrollTop = x } }).then(({ x }) => { this.rootScrollElement.scrollTop = x }) this.$nextTick(() => { this.anchorChange = true }) } }, getScrollElement() { if (!this.isMounted) return // 如果没有传入 parent 默认取第一个父级滚动元素 const parent = this.parent let element = null if (parent) { element = document.querySelector(parent) // mount 之后 rootScrollElement 可能已经存在了,如果和上次一样就不做任何操作。 if (element === this.rootScrollElement) return } else if (this.$el) { element = getFirstScrollElement(this.$el.parentElement) } if (element) { this.removeScrollEvent() this.rootScrollElement = element this.rootScrollElement.addEventListener('scroll', this.handleScroll) } }, removeScrollEvent() { if (this.rootScrollElement) { this.rootScrollElement.removeEventListener('scroll', this.handleScroll) } }, handleScroll(event) { const scrollTop = this.rootScrollElement.scrollTop this.scrollTop = scrollTop if (!this.isScroll) return const { data, top } = this const offsetHeight = _get(this.$el, ['parentElement', 'offsetHeight'], 0) const scrollList = [] data.forEach(item => { const element = document.querySelector(item.prop) if (element) { const top = element.offsetTop const rect = { top: top + element.clientTop - top - offsetHeight, bottom: top + element.offsetHeight - top - offsetHeight } scrollList.push(rect) } }) // 遍历按钮元素的 top 和 bottom,查看当前滚动在那个元素的区间内。 scrollList.some((it, index) => { if (index && scrollTop >= it.top && top < it.bottom) { const menu = _get(data, [index, 'prop'], '') if (menu) this.menu = menu return true } else { // 当小于最小高度时,就等于最小高度 if (scrollTop >= 0 && scrollTop < it.bottom) { const menu = _get(data, [index, 'prop'], '') if (menu) this.menu = menu return true } } }) } } } </script> <style lang="scss"> .cpt-anchor { padding-top: 4px; .cpt-tab-menus { margin: 0; .el-radio-button { margin-left: 10px; .el-radio-button__inner { border: none; border-radius: 5px 5px 0 0; border-bottom: 2px solid #e4e7ed; background-color: #f6f6f8; font-size: 16px; &:hover { border-bottom: 2px solid #409eff; } } &.is-active { .el-radio-button__inner { color: #fff; border: none; border-radius: 5px 5px 0 0; background-color: #409eff; border-bottom: 2px solid #409eff; box-shadow: none; } } } } } </style>

吸顶锚点组件

最后将上面两个组件合并到一起就是我们所需要的吸顶锚点组件了。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/a494f6f30d5249d12d32871655426aac.html