表格的表头及多列冻结

  • Sorzen
  • 24 Minutes
  • April 12, 2020

一、什么是表格冻结

当数据较多时将某些行列进行冻结,即固定在某一位置展示,比如当数据较多时,表头可以固定在首行展示,不受滚动影响。由于项目中表格及交叉透视表的功能已经实现,因此只能在原来基础上进行功能的改造,因此需要在原来表格基础上添加表头及多列冻结。

二、实现方案

avatar

优点:固定行数没有限制

缺点:适用场景,仅横向 纵向均无滚动条时可用

若有横向滚动条,两个表格的滚动条是分开的,会出现滚动 A B 不动,或滚动 B A 不动的情况,Windows 下会展示两个横向滚动条,不美观

若有纵向滚动条,在 Windows 下滚动条是默认有宽度的,滚动条的宽占用了表格 B 的部分宽度,导致表头和表主体列错位,示意图如下
avatar

要做的就是把样式调整成我们想要的,使用定位把 footer 放到 header 下,body 向下移一行的距离即可。

三、初步实现表格冻结

1、表头和首列冻结功能设置入口
在表格编辑的右边控制面板 → 高级中增加表格表头和首列冻结设置项

avatar

2、表头和首列冻结实现
sugar 中表格分为简单表格和交叉透视表,因此实现冻结需要满足两种表格要求,而且需要不影响之前表格布局和样式情况下,进行冻结功能添加。

(一)监听表格滚动
通过监听表格滚动,当滚动条滚动时并且表头固定设置打开时,此时应该固定 thead 位置,但是由于 thead 会随滚动条滚动,此时可以用 div 模拟 thead 进行定位在顶部。

avatar

图(一)
图(二)

图一通过 div 模拟 thead 固定在表格顶部,但是 thead 每个单元格宽度如何完美呈现在模拟 thead 的 div 中呢?

(二)表格信息记录

通过查找 thead 元素,并将表格每行及每列的宽高、边框大小、背景颜色等信息进行记录,然后在用 div 进行模拟时,根据记录的数据设置每行每列样式,做到与原表格样式一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
   if (this.refs.theVeryTable && this.dom) {
let fixedThs = (this.refs.theVeryTable as HTMLElement).getElementsByTagName('th');
let superHeaderThSizeArr = [];
let tableContainerWidth = (this.refs.tableContainer as HTMLElement).clientWidth;
for (let i = 0; i < fixedThs.length; i++) {
superHeaderThSizeArr.push([
fixedThs[i].clientWidth + fixedThs[i].clientLeft,
fixedThs[i].clientHeight + fixedThs[i].clientTop,
fixedThs[i].clientLeft,
fixedThs[i].clientTop
]);
}


if (
JSON.stringify(superHeaderThSizeArr) !== JSON.stringify(this.state.superHeaderThSizeArr) ||
tableContainerWidth !== this.state.tableContainerWidth
) {
this.setState({
superHeaderThSizeArr
});
}
}


{superHeaders.map((headerItem: PlainObject[], headerIndex: number) => {
return (
<div key={headerIndex}>
{headerItem && headerItem.map
? headerItem.map((headerTr: PlainObject, headerTrIndex: number) => {
const thStyle: PlainObject = {
width: this.state.superHeaderThSizeArr[headerTrIndex][0],
height: this.state.superHeaderThSizeArr[headerTrIndex][1],
borderLeft: this.state.superHeaderThSizeArr[headerTrIndex][2]
? this.state.superHeaderThSizeArr[headerTrIndex][2] + `px solid ${_lineColor}`
: 0,
// padding: '8px 12px',
borderBottom: '1px solid #e8e8e8',
display: 'inline-block',
fontSize: 1 + parseInt(config.fontSize || 13, 10),
padding: `${config.paddingTB || 8}px ${config.paddingLR || 12}px`,
float: 'left',
fontWeight: 'bold'
};
// 当某一项超级表头的name是undefined时,将底边框设置为和表头一样的背景色,从而来模拟合并单元格的情况
if (headerTr['name'] === undefined) {
thStyle.borderBottom = `solid 1px ${_titleBg}`;
}
return headerTr['colspan'] > 0 ? (
<div
key={headerTrIndex}
style={thStyle}
>
{headerTr['name']}
</div>
) : null;
})
: null}
</div>
);
})}

虽然样式一致,但是当表格内容过多时,页面需要滚动,如何做到模拟的 thead 与 thead 一致随着页面滚动呢?

(三)监听表格页面滚动
当固定首列时,页面内容滚动时,模拟的首列信息需要跟着滚动,因此需要监听页面的滚动,然后根据表格滚动的 scrollTop 信息,将模拟的表格列进行定位,已到达模拟滚动的效果,为了防止页面卡顿可以加入防抖操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
tableScroll() {
const {
chart: {config}
} = this.props;
if (this.refs.normalHeader) {
// 获取下页面滚动高度
let scorllTop;
if (this.props.mode === 'editor') {
let editor = document.getElementById('editor-body');
scorllTop = editor ? editor.scrollTop + this.headerHeight : 0;
} else {
scorllTop =
(document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset) + this.headerHeight;
}


// 表格顶部相对页面顶部的差距
let tableHeaderOffset = scorllTop - this.offset(this.refs.normalHeader);
let height = Math.min(this.dom.clientHeight, (this.refs.theVeryTable as HTMLElement).offsetHeight);
// console.log('scorllTop:'+scorllTop)
// console.log('tableHeaderOffset:'+tableHeaderOffset)
// console.log('height:'+height)
// console.log('tableContainer:'+(this.refs.tableContainer as HTMLElement).scrollTop)
const tableContainerScrollTop = (this.refs.tableContainer as HTMLElement).scrollTop;
const normalHeaderHeight = document.getElementsByClassName('normalHeader') && document.getElementsByClassName('normalHeader')[0].clientHeight;
// 是否需要将表头固定在文档顶部
if (tableHeaderOffset < 0 || (height - 100 - tableHeaderOffset < 0 && this.state.tableShow)) {
this.setState({
tableShow: false
});

} else if (tableHeaderOffset >= 0 && height - 100 - tableHeaderOffset >= 0 && !this.state.tableShow) {
if (config.fix && config.fix.includes('fixedHeader') && tableContainerScrollTop > normalHeaderHeight) {
this.setState({
tableShow: true
});
}
}

// 是否需要将表头固定在表格顶部
if (this.dom.scrollTop > (this.refs.normalHeader as HTMLElement).offsetTop && !this.state.showFixedDiv) {
if (config.fix && config.fix.includes('fixedHeader')) {
this.setState({
showFixedDiv: true
});
} else {
this.setState({
showFixedDiv: false
});
}

} else if (this.dom.scrollTop <= (this.refs.normalHeader as HTMLElement).offsetTop && this.state.showFixedDiv) {
this.setState({
showFixedDiv: false
});
}
if (this.dom.scrollLeft !== this.state.scrollLeft) {
this.setState({
scrollLeft: this.dom.scrollLeft
});
}
// console.log('this.dom.scrollTop:'+this.dom.scrollTop)
if (this.dom.scrollTop !== this.state.scrollTop) {
this.setState({
scrollTop: this.dom.scrollTop
});
}
}
}

四、最后实现方案

(1)固定行列实现

由于 sugar 中的 table 可能需要固定表头同时并固定列,这样需要三个相同表格来覆盖展示,不是太有好,因此使用 div 模拟的形式去处理,减少了不必要的数据渲染。

div 模拟普通表格时比较简单,当模拟交叉透视表,出现行列合并时,很容易出现排列错误问题。

因此通过模拟 table 中行列布局形式去实现:

1、让每一行有固定高度,防止没内容时导致错位

2、通过 flex 形式对行内元素进行布局,因为固定的列最后一列肯定有元素,因此采用

justify-content:’flex-end’形式去布局页面,每行元素从后往前一次排列,通过固定宽高自动将元素挤到固定位置
3、通过遍历表头元素已经记录表格的宽高信息,通过遍历 tbody 的 tr 记录行的高度(交叉透视表时主要记录普通表头宽高,超级透视表中固定列宽度主要是: 1、colspan < 固定列宽度时,当前所在列宽度 + 向后固定列数个普通表头宽度)

固定列的元素宽高如何计算

<1>通过列的 rowspan 计算固定列元素的宽高计算

当前列元素高度 = 当前行高度 + 从当前行高度向后rowspan个行的高度

当前列元素宽度 = 当前元素所在列宽度 + 从当前列宽度向后colspan个列宽度,但是当固定的列小于colspan时,当前所在列宽度 + 向后固定列数个普通表头宽度

风险:由于不同浏览器下可能会有 1px 偏差,导致 div 模拟的行列可能会发生错乱。

<2> 为了防止出现不同浏览器下可能出现错位风险,因此改为用 table 去模拟固定行列,这样可以通过 colspan 和 rowspan 去计算宽高,减少了页面渲染时的计算问题,也优化了一些性能。

4、页面滚动

<1>通过设置 scrollTop 进行上下移动,scrollLeft 进行左右移动

问题:由于内容滚动时左右固定列也需要滚动,左右列滚动时内容也需要滚动,因为会造成 scroll 事件的互相触发,造成滚动时页面跳动,并造成页面卡顿。

解决办法:使用模拟滚动条来解决 scroll 事件互相触发问题(如:better-scroll、perfect-scrollbar)

5、优化滚动

<1>由于 react 自身原因,在页面滚动时最好不要进行 setState 操作,防止页面的 diff 过程,并且防止当存在 componentDidUpdate 时,频繁触发 componentDidUpdate 内操作

<2>不要进行大量的计算,防止在滚动时,出现大量计算,阻塞页面渲染造成页面卡顿

<3>滚动时加入防抖

avatar