跳至主要內容

WindowAffix 窗口图钉

haneball大约 2 分钟Vue组件

概述

WindowAffix 能将页面元素固定在一个区域内。

原理

想要实现 Affix 效果,需要获取元素的位置。我们可以借助 Element.getBoundingClientRect(),该方法返回一个 DOMRect 对象,通过这个对象可以获取元素上、右、下、左相对于视窗左上边缘的距离、xy距离以及元素宽高。
详情参见 MDN 文档:Element.getBoundingClientRect()open in new window

若使用 top 定位。向下滚动时,元素随着页面不断向上运动,当元素顶部到达 offset,元素会停止移动并固定在该处。由此可见,触发的条件为 top < offset,且触发后元素的定位将变为 fixed,其中, top 为元素顶部相对于视窗上边缘的距离。

WindowAffix top 定位
WindowAffix top 定位

若使用 bottom 定位。与 top 的情况相反,页面向下运动,元素底部到达 offset 将会固定。触发的条件为 bottom > clientHeight - offset,其中,bottom 为元素底部相对于视窗上边缘的距离, clientHeight 为视窗的高度。

WindowAffix bottom 定位
WindowAffix bottom 定位

实现

  • 计算属性 affixStyle 动态更新组件的样式
  • 除了页面上下滚动,视窗的大小变化同样会触发 Affix 效果
<template>
  <div ref="root" class="window-affix">
    <div :class="{ 'window-affix__fixed': state.fixed }" :style="affixStyle">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    offset: {
      type: Number,
      default: 0
    },
    position: {
      type: String,
      default: 'top'
    },
    zIndex: {
      type: Number,
      default: 100
    }
  },
  data() {
    return {
      state: {    // 图钉状态
        fixed: false,
        width: 0,
        height: 0,
        left: 0,
        scrollTop: 0,
        clientHeight: 0
      }
    }
  },
  watch: {
    'state.fixed'(newVal) {    // 向父组件触发 change 事件
      this.$emit('changr', this.state.fixed)
    }
  },
  mounted() {
    window.addEventListener('scroll', this.onScroll)
    window.addEventListener('resize', this.updateState)
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this.onScroll)
    window.removeEventListener('resize', this.updateState)
  },
  computed: {
    affixStyle() {    // 图钉样式
      if (!this.state.fixed) {
        return
      }
      const offset = this.offset ? `${this.offset}px` : 0
      const left = `${this.state.left}px`

      return {
        height: `${this.state.height}px`,
        width: `${this.state.width}px`,
        top: this.position === 'top' ? offset : '',
        left: left,
        bottom: this.position === 'bottom' ? offset : '',
        zIndex: this.zIndex,
      }
    }
  },
  methods: {
    onScroll() {    // 滚动回调函数
      // 更新图钉状态
      this.updateState()
      // 向父组件触发 scroll 事件
      this.$emit('scroll', {
        scrollTop: this.state.scrollTop,
        fixed: this.state.fixed
      })
    },
    updateState() {    // 图钉状态更新函数
      const rootRect = this.$refs.root.getBoundingClientRect()
      this.state.width = rootRect.width
      this.state.height = rootRect.height
      this.state.left = rootRect.left
      this.state.scrollTop = document.documentElement.scrollTop
      this.state.clientHeight = document.documentElement.clientHeight

      if (this.position === 'top') {    // top 定位
        this.state.fixed = this.offset > rootRect.top
      } else {    // bottom 定位
        this.state.fixed = rootRect.bottom > this.state.clientHeight - this.offset
      }
    }
  }
}
</script>