前言
最近由于经手的任务遇到过几回tab吸顶的问题,其实tab吸顶实现并不难,但是tab吸顶常常是与选项卡切换一起出现的。本人较菜并且记忆力不好,所以记录下,哈哈哈!
一、tab吸顶的实现方式
tab吸顶可以通过几种方式实现,实现大体分为两种:1.css实现 2.js监听scroll实现;但是实际开发中使用的就是一种那就是js的实现方式。但不管怎么说,该说得说,不必要(其实也挺有必要)说的也得说说
1.position:sticky实现吸顶(不推荐)
sticky其实就是fixed与relative这两种方式的结合,设置position:sticky的元素达到设置位置要求之前相当于relative定位,当达到位置要求是就是fixed定位。
sticky的使用要求:
- 父元素高度不能小于设置sticky元素的高度
- 父元素不能设置overflow:hidden或auto
- 必须指定top、bottom、left、right其中一个属性
- sticky元素值在父元素内生效
<style>
.sticky {
position: -webkit-sticky;
position: sticky;
top: 0;//当top达到0时,变为fixed;小于0时,为relative
}
</style>
...
<div id="app">
<div class="header"></div>
<div class="body">
<div class="tab sticky"></div>
<div class="content"></div>
</div>
</div>
sticky存在的最大问题在于sticky的兼容性问题
2.scrollTop - offsetTop > 0 实现吸顶
这种方式的思路就是屏幕滚动距离与tab元素距离顶部距离的位置之间的判断,其实这种方式就是js实现的大体思路。该种实现思路有一个问题就是offsetTop不好获取。offsetTop是什么?
offsetTop:只读属性,当前元素相对于其 offsetParent 元素的顶部内边距的距离。offsetParent 又是什么?
offsetParent:只读属性,返回一个指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的 table, td, th, body 元素。(只要父元素没有设置position,则offsetParent就一直向上找,直到body)
获取offsetTop的方法封装
getOffset(el) {
let offsetTop = 0;
while (el !== window.document.body && el !== null) {
offsetTop += el.offsetTop;
el = el.offsetParent;
}
return offsetTop;
}
吸顶实现
<div id="app">
<div class="header"></div>
<div class="body">
<div ref='tabRef' class="tab" :class="{'tab-fixed': isTabShow}"></div>
<div class="empty-box" v-show='isTabShow'></div>
<div ref='contentRef' class="content"></div>
</div>
</div>
var app = new Vue({
el: '#app',
data: {
isTabShow: false,
offsetTop: 0,
},
mounted() {
window.addEventListener('scroll', this.handleScroll)
this.$nextTick(() => {
this.offsetTop = this.getOffset(this.$refs.tabRef)//offsetTop可以通过jquery去获取
})
},
methods: {
handleScroll() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
this.isTabShow = scrollTop >= this.offsetTop
},
getOffset (el) {
let offsetTop = 0;
while (el !== window.document.body && el !== null) {
offsetTop += el.offsetTop;
el = el.offsetParent;
}
return offsetTop;
}
}
})
该方式存在的问题就是offsetTop以及scrollTop的获取问题,offsetTop获取需要注意offsetParent,scrollTop要注意兼容性问题。
3.getBoundingClientRect
getBoundingClientRect是什么?方法返回元素的大小及其相对于视口的位置。(元素的宽高,距离视口的上下左右距离,以及xy坐标)。通过这种方式可以直接判断距离视口的top值是否为0,从而进行吸顶,但是考虑到下拉时解除吸顶问题,所以不能直接判断top<=0,而应该通过tab下方的内容的top值与tab的height进行判断。
<div id="app">
<div class="header"></div>
<div class="body">
<div ref='tabRef' class="tab" :class="{'tab-fixed': isTabShow}">
<span @click="handleTabChange(1)" class="tab-title">1</span>
<span @click="handleTabChange(2)" class="tab-title">2</span>
<span @click="handleTabChange(3)" class="tab-title">3</span>
</div>
<div class="empty-box" v-show='isTabShow'></div>
<div ref='contentRef' class="content">
<div v-show="tabActive === 1" class="tab-content">1</div>
<div v-show="tabActive === 2" class="tab-content">2</div>
<div v-show="tabActive === 3" class="tab-content">3</div>
</div>
</div>
</div>
var app = new Vue({
el: '#app',
data: {
isTabShow: false,
tabActive: 1,
},
mounted() {
window.addEventListener('scroll', this.handleScroll)
},
computed: {
tabHeight() {
return this.$refs.tabRef.getBoundingClientRect().height;
}
},
methods: {
handleScroll() {
const contentTop = this.$refs.contentRef.getBoundingClientRect().top;
this.isTabShow = contentTop <= this.tabHeight;
},
handleTabChange(index) {
this.tabActive = index;
}
}
})
存在的问题:tab吸顶的时候,position变为fixed,脱离文档流,造成后面的元素往上挤,出现文档内容突然向上跳动的情况,本人之前的解决方法式在tab元素后面加上一个空盒子即class="empty-box"的div,另一种方式是在吸顶的tab外层包一个div。
getBoundingClientRect有很好的兼容性。
二、tab吸顶存在的问题
1.tab无记忆
由于tab吸顶一般是与选项卡一起出现的所以,就会出现一个问题,就是选项卡切换的时候,每个选项卡的内容所处的位置是相互影响的。如下图
点击1并将1下的内容移到某一位置,然后点击2,你会发现2的初始位置就是1的结束位置,这显然是不符合预期的
解决思路:通过一个对象,该对象的属性就是每个tab对应的name,每个值就是tab切换时所处的位置。
问题:上面的思路仅仅只是实现了吸顶的选项tab吸顶了,没有切换过的选项tab是不会吸顶的,因为没有切换的tab的滚动距离为0。
优化:在上面解决思路上优化,切换tab时,如果已经吸顶了,那么切换的tab移动默认值(该值不为0,且该值恰好使得该tab处于吸顶状态)
<div id="app">
<div class="header"></div>
<div class="body">
<div ref='tabRef' class="tab" :class="{'tab-fixed': isTabShow}">
<span @click="handleTabChange('one')" class="tab-title">1</span>
<span @click="handleTabChange('two')" class="tab-title">2</span>
<span @click="handleTabChange('three')" class="tab-title">3</span>
</div>
<div class="empty-box" v-show='isTabShow'></div>
<div ref='contentRef' class="content">
<div v-show="tabActive === 'one'" class="tab-content">1</div>
<div v-show="tabActive === 'two'" class="tab-content">2</div>
<div v-show="tabActive === 'three'" class="tab-content">3</div>
</div>
</div>
</div>
var app = new Vue({
el: '#app',
data: {
isTabShow: false,
tabActive: 'one',
initContentTop: 0,
scrollDistance: {
one: 0,
two: 0,
three: 0,
}
},
mounted() {
window.addEventListener('scroll', this.handleScroll)
this.$nextTick(() => {
// 初始化tab对应的content对于视口的top
this.initContentTop = this.$refs.contentRef.getBoundingClientRect().top;
})
},
computed: {
tabHeight() {
return this.$refs.tabRef.getBoundingClientRect().height;
},
},
methods: {
handleScroll() {
const contentTop = this.$refs.contentRef.getBoundingClientRect().top;
// 记录每个tab滚动的距离
this.scrollDistance[this.tabActive] = this.initContentTop - contentTop;
this.isTabShow = contentTop <= this.tabHeight;
},
handleTabChange(index) {
this.tabActive = index;
if (this.isTabShow) {
//只要一个tab吸顶了,那么切换时,会移动默认值。否则如果切换的tab未移动过,scrollTo的y值会是0,又会造成tab不吸顶
window.scrollTo(0, this.getDistance());
}
},
// 获取tab切换时,对应的移动距离
getDistance() {
const d = this.scrollDistance[this.tabActive];
const initd = this.initContentTop - this.tabHeight;
return d > initd ? d : initd;
}
}
})
效果图: