背景
当页面滚动的时候,如果超过表格的部分,表格的头部会固定在某个位置,方便用户看到数据栏的标。项目采用的是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();
},
};
参考
- Table header sticky feature
- antd-table-sticky.js