【源码分析系列】antdv table滚动时固定表头

背景

当页面滚动的时候,如果超过表格的部分,表格的头部会固定在某个位置,方便用户看到数据栏的标。项目采用的是vue2+antdv,但是这个版本的table没有sticky属性,所以需要自行解决。
滚动前:
在这里插入图片描述
滚动后:
在这里插入图片描述

原理

知道大概是用了sticky属性,但是奈何我在怎么修改元素的style还是没有起到想要的作用,经过多番寻找,找到了这样一个问题:Table header sticky feature,在这最后有一个老哥提到了自己封装的一个指令:antd-table-sticky.js

所以研究一下它的基本实现原理,在antdv table中,它的页面结构是这样的:

div.ant-table
  div.ant-table-content
    div.ant-table-scroll
      div.ant-table-body
        table.ant-table-fixed
          colgroup
          thead.ant-table-thead
          tbody.ant-table-tbody
          

之前我以为是在ant-table-content上或者是内部的html中加入关于sticky的属性,然而通过测试,大佬的方法主要是复制了一个.ant-table-content元素,放在同级,将其设置fixed

div.ant-table-content
    div.ant-table-scroll
      div.ant-table-body
        table.ant-table-fixed
          colgroup
          thead.ant-table-thead
          tbody.ant-table-tbody

然后修改这个ant-table-content的样式,并当作兄弟组件插入,最后的结果是这样的:

div.ant-table
  div.ant-table-content
  div.ant-table-content(position: fixed; top: 64px; z-index: 1000; background-color: rgb(255, 255, 255); width: 1114px;)

如图所示:
在这里插入图片描述
实现的过程大概是这样的:

  • 获取滚动容器,支持id传入获取指定的容器,不然就是默认window
  • 深度拷贝ant-table-content节点,并将这个节点设置样式:position: fixed;top: ${fixedTop}px; z-index: ${zIndex}; ${stickyStyle.cssText};background-color: ${bgColor}
  • 监听:
    • 监听容器滚动,如果滚动到指定位置,就插入拷贝的节点,否则就移除;
    • 监听容器的大小,要重置拷贝节点的宽度;
    • 监听.ant-table-body的滚动,设置拷贝节点的横向位置
  • 当表格发生更新的时候,移除.ant-table-tbody,.ant-table-placeholder,这样就只有表头了

源码

/**
 * 使Ant Design Vue的Table组件支持表头sticky
 * 单元格宽度必须固定
 */
import { throttle } from 'lodash'

let listenAction;
let container;
let stickyHeader = null;
let originEl = null;
let bindingConfig = {};

const originSelector = '.ant-table-content';
const scrollSelector = '.ant-table-scroll .ant-table-body';
const toRemoveSelector = '.ant-table-tbody,.ant-table-placeholder';

// 获取指令参数
const getBindingConfig = (binding) => {
  const params = binding.value || {};
  const {
    fixedTop = 64,
    zIndex = 1000,
    bgColor = '#fff',
    disabled,
    scrollContainerId
  } = params;
  return { fixedTop, zIndex, disabled, scrollContainerId, bgColor };
};

const unwatch = () => {
  container && container.removeEventListener('scroll', listenAction);
  container && container.removeEventListener('resize', resizeStickyHeader);
  originEl && originEl.querySelector(scrollSelector).removeEventListener('scroll', setScrollX);
};

const watch = () => {
  container && container.addEventListener('scroll', listenAction);
  container && container.addEventListener('resize', resizeStickyHeader);
  originEl && originEl.querySelector(scrollSelector).addEventListener('scroll', setScrollX);
};

// 根据表格实际内容修改表头内容
const adaptStickyHeader = () => {
  stickyHeader.innerHTML = originEl.innerHTML;
  stickyHeader.querySelector(scrollSelector).style.overflowX = 'hidden';
  const tbodyList = Array.from(stickyHeader.querySelectorAll(toRemoveSelector));
  tbodyList.forEach((tbody) => {
    tbody.parentNode.removeChild(tbody);
  });
  resizeStickyHeader();
  setScrollX();
};

// 根据实际内容设置宽度
const resizeStickyHeader = throttle(() => {
  stickyHeader.style.width = `${originEl.getBoundingClientRect().width}px`;
});

// 根据表格横向滚动,设置表头的横向位置
const setScrollX = throttle(() => {
  const stickyHeaderScroller = stickyHeader.querySelector(scrollSelector);
  const originScroller = originEl.querySelector(scrollSelector);
  stickyHeaderScroller.scrollLeft = originScroller.scrollLeft;
});

export default {
  bind(el, binding) {
    // 获取 .ant-table-content
    originEl = el.querySelector(originSelector);
    // 读取指令参数
    bindingConfig = getBindingConfig(binding);

    const { disabled, fixedTop, zIndex, scrollContainerId, bgColor } = bindingConfig;

    if (disabled) return;

    // 如果没有指定父级容器的id,则默认为浏览器
    container = document.getElementById(scrollContainerId) || window;
    let active = false;
    stickyHeader = originEl.cloneNode(true);
    const stickyStyle = stickyHeader.style;
    stickyStyle.cssText = `position: fixed;top: ${fixedTop}px; z-index: ${zIndex}; ${stickyStyle.cssText};background-color: ${bgColor}`;

    const sticky = () => {
      if (active) return;
      setScrollX();
      originEl.insertAdjacentElement('afterend', stickyHeader);
      active = true;
    };

    const reset = () => {
      if (!active) return;
      stickyHeader.parentNode?.removeChild(stickyHeader);
      active = false;
    };

    listenAction = throttle(() => {
      const rectEl = originEl?.parentNode;
      const rect = rectEl.getBoundingClientRect();
      const offsetTop = rect.top;
      if (offsetTop <= fixedTop) {
        return sticky();
      }
      reset();
    });

    watch();
  },

  unbind: unwatch,

  update(el, binding) {
    bindingConfig = getBindingConfig(binding);
    originEl = el.querySelector(originSelector);

    if (bindingConfig.disabled) {
      stickyHeader.parentNode?.removeChild(stickyHeader);
      unwatch();
      return;
    }

    adaptStickyHeader();
  },
};

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值