实现vue图钉Affix组件

373 阅读2分钟

概述

实际项目中,我们或多或少需要在滚动条滚动到一定位置之后,将一个元素固定定位,这种还是很常见的,比如后台菜单,方便用户操作,当然这个功能,在组件库中都是提供了的,最近在使用iview组件中的Affix图钉组件的时候,遇到了一些bug,最后还是决定自己实现一个。

效果图

动画.gif

实现原理

1.需要记录需要固定的元素距离顶部的距离是否小于或者等于给定的偏移量 2.当我们dom元素满足1条件的时候,我们需要将当前固定元素绝对定位,然后由于定位,会导致原位置空缺,因此,此时需要浅克隆一个和包裹的需要固定元素的克隆节点去占位原来的位置,避免样式出现错乱,这里克隆节点的原因在于,我们需要原来元素的位置,样式信息,这样才能保证克隆节点放到原位置保持布局一致。 3.在2的基础上,还要记录绝对定位那一刻,HTML文档滚动卷走的高度,用户后续滚动回到原位置做比较。 4.当dom元素不满足需要定位的条件时,接触3缓存的卷走的高度和当前滚动时HTML文档卷走的高度作比较,当前卷走的高度小于或者等于3记录的卷走高度时,说明条件不满足了,我们需要将定位样式去掉,同时隐藏克隆的节点。

实现

  • App.vue
<template>
  <div class="app">
    <div class="box">其他页面元素</div>
    <Affix :offset-top="100" @on-change="onChange">
      <div class="menu">
        <h2>系统菜单</h2>
        <ul>
          <li>图书管理</li>
          <li>用户管理</li>
          <li>机构管理</li>
          <li>订单管理</li>
          <li>日志管理</li>
          <li>统计管理</li>
          <li>销售管理</li>
        </ul>
      </div>
    </Affix>
  </div>
</template>

<script>
import Affix from "./components/Affix/index";
export default {
  components: {
    Affix,
  },

  methods: {
    onChange(status) {
      console.log(status);
    },
  },
};
</script>

<style lang="less">
.app {
  height: 2000px;
}
.box {
  width: 500px;
  height: 500px;
  background-color: pink;
}
.menu {
  background-color: #000;
  color: #fff;
  width: 130px;
  text-align: center;
  line-height: 40px;
  ul {
    list-style: none;
    padding: 0 20px;
   
  }
}
</style>

  • ./components/Affix/Affix.vue
<template>
  <div :style="FfixStyle" ref="ffix">
    <slot></slot>
  </div>
</template>

<script>
export default {
  props: {
    // 距离顶部的距离
    offsetTop: {
      type: Number,
      default: 0,
    },
  },
  data() {
    return {
      // 标识dom元素是否距离可视区顶部给定距离
      tag: false,
      // 当到达需要固定的位置,需要设置的样式
      FfixStyle: {},
      // 绑定滚动处理函数this的函数
      bindThisHandleScroll: null,
      // 当固定的那一刻,文档卷走的距离(用于dom元素距离可视区不满足固定的距离触发
      scrollTop: 0,
      // 标识dom元素固定的那一刻,文档卷走的距离标识
      isAdd: false,
    };
  },

  mounted() {
    // 初始化
    this.init();
  },
  beforeDestroy() {
    // 销毁函数
    this.destory();
  },
  methods: {
    destory() {
      // 移除window上面的事件
      window.removeEventListener("scroll", this.bindThisHandleScroll);
    },
    init() {
      // 绑定this执行,用户组件销毁移除监听函数
      this.bindThisHandleScroll = this.handleScroll.bind(this);
      window.addEventListener("scroll", this.bindThisHandleScroll);
    },
    handleScroll() {
    //获取插槽需要定位的dom元素
      let element = this.$slots.default[0].elm;
      // 获取元素距离可视区顶部和左边的距离
      let { top } = element.getClientRects()[0];
      // 说明到达需要固定的位置
      if (top <= this.offsetTop && !this.tag) {
        // 只需要记录一次dom元素固定的时候,文档卷走的高度
        !this.isAdd
          ? (this.scrollTop = document.documentElement.scrollTop)
          : null;
        // 设置当前已经满足固定条件
        this.tag = true;
        // 记录已经记录过文档卷走的高度
        this.isAdd = true;
        // 固定之后,将dom元素设置为绝对定位
        this.FfixStyle = {
          position: "fixed",
          top: this.offsetTop + "px",
          zIndex: 999,
        };
        // 发布固定的时候的事件
        this.$emit("on-change", true);
        // 克隆节点,顶替原位置占位
        this.handleCloneNode(true);
      }
      // 当dom元素不满足固定位置的距离时候,取消固定定位,回调初始位置
      if (
        this.tag &&
        document.documentElement.scrollTop - this.scrollTop <= 0
      ) {
        this.tag = false;
        this.FfixStyle = {};
        // 移除克隆几点
        this.handleCloneNode(false);
      }
    },
    handleCloneNode(type) {
      if (type) {
        let element = this.$slots.default[0].elm;
        let parent = element.parentNode.parentNode;
        // 克隆我们对应包裹的需要定位的元素取代绝对定位后的元素进行原位置的占位,设置透明不可见(复制节点的原因在于,我们占位元素需要原来元素的样式位置等信息)
        this.cloneNodde = this.cloneNodde
          ? this.cloneNodde
          : element.cloneNode(false);
        this.cloneNodde.style.opacity = "0";
        this.cloneNodde.style.display = "block";
        parent.insertBefore(this.cloneNodde, this.$refs.ffix);
      } else {
        // 隐藏克隆元素
        this.cloneNodde && (this.cloneNodde.style.display = "none");
      }
    },
  },
};
</script>

  • ./components/Affix/index.js
import Affix from "./Affix.vue";
export default Affix;

上述组件只记录了,距离顶部的固定,距离底部的思路是一样的,但是距离底部固定的场景我还没见过,不过实现思路和上面一样。