HTML Table 元素允许合并单元格。通常手写代码比较“反人类”思维,于是还是通过直观的可视化的工具来完成,例如奉为经典的 Dreamweaver。
研究代码,td 行元素有一 rowspan 跨行的属性,表示跨行行数。如果当前这样有 x 个跨行,那么下面 tr > td (一共 x 行)中的每一行都可以少出现一个 td。
如上图,上面有 rowspan 的 tr 的,包含一共八个 td;而下面的 tr 因为设置了跨行 2 个的缘故,少了一个 td。
我们得知标签的思路怎么实现之后,该换到代码层面来讨论如何实现的了,当然这里我们要 DOM 元素的操控,题外话——什么 RN、Vue 不用写 DOM 代码的先放一边:)。
首先是确定哪些单元格要合并的。先假设相同性质的 td 有相同的 class,都在同一列(Column)。
这一列都是 order,显然,订单 id 为 124 的有两个,都是显示一样的内容,需要合并之。另外,必须保证要合并的单元格是连续出现的,如下图未合并之前显示的,否则不能合并。
有 class 虽然指定哪些单元格要处理,但尚不知跨行多少个,有哪些需要合并的,——我们用代码来算算。
var arr = document.querySelectorAll('.order');
var map = {};
for(var i = 0, j = arr.length; i < j; i++) {
var td = arr[i];
var orderId = td.dataset.orderId;
if(undefined === map[orderId]) map[orderId] = 1; // 统计跨行的有多少,用一个 map 装着
else map[orderId] = ++map[orderId];
}
得到这个 map,key 为订单id,value 是跨多少行。
做某件事之前,必须知道目标对象究竟是什么,也就是在一个大集合中选择好实施的范围,“精确打击”——这道理亦适用于本文。我们接着就进行元素的标记,目的就是,要跨行的保留下来设置 rowspan 属性,不要的 td 标识出来通通删除之——如此就能达到跨行的目的了。
这里要引入基础知识点:队列(queue),前端小白不懂数据结构的,应要恶补一下,我博客前面几篇文章都有介绍。
var stack = [];
for(var i = 0, j = arr.length; i < j; i++) {
var td = arr[i];
var orderId = td.dataset.orderId;
var tds = map[orderId];
if( tds > 1 && stack.length === 0) {// 标记
for(var q = i; q < i + tds ; q++) {// 连续 tds 个都是要合并单元格的
if(q == i) {
arr[q].classList.add('firtstOne');
arr[q].dataset.rowSpan = tds;
}
stack.push(arr[q]); // 加入队列
}
}
if(stack.length && td === stack[0]) {
stack.shift();// 弹出第一个元素
if(td.className.indexOf('firtstOne') != -1) {
} else {
td.classList.add('die'); // 要删除的元素
}
}
}
我们利用数组简单模拟队列,先声明 stack =[] 以备后用。现在仍是遍历该列的所有 td 元素,发现有跨行的 td 而且队列为空的(tds > 1 && stack.length === 0)进入一个 for 循环,“向前搜索” tds 个单元格,将其逐个添加到队列中。注意循环上限中不能是 tds,而是 i + tds,因为要从当前索引开始算。并且还有一件事,就是发现第一个的元素的话要进行标记并设置它的 rowspan 属性,怎么得知是第一个? i 不变, q 会变,当 q === i 时显然是第一个。
到目前为止,我们的代码仍在外面的那个大 for 循环中。接着我们执行退栈的任务。为什么要这样操作呢?有同学可能会问,既然上面那个“子的 for 循环”已经知道哪个是第一个元素,哪些是不是第一个元素,不是第一个元素不就是要删除的么,怎么不干脆直接在那儿标记啊?这里稍作解释下,因为还是在当前 for 循环中,虽然先行向前“搜索”了要处理的元素,但回到大的元素中,下一个还是遇到要删除的 td,怎么确定是否第一个呢?所以这样的逻辑是相悖的。于是我们引入一个对象:栈,让它记住我们要处理的元素,开辟一段新的变量,作为我们判断逻辑的“参考条件”。栈的特性就是“有出有入”。
if(stack.length && td === stack[0]) {
stack.shift();// 退栈
if(td.className.indexOf('firtstOne') != -1) {
} else {
td.classList.add('die'); // 要删除的元素
}
}
这里的 td === stack[0] 栈头个元素就是当前循环变量 td,即 arr[i] (同理 stack.shift();// 退栈返回的也是 td);而且还是要栈不为空的情况下。
至此,我们的标记工作就完成了。栈的理解是必须的,尽管有点绕,但是是实现该功能的重点。
最后的工作就简单清爽多了。die() 是我们直接封装的删除 dom 元素的方法。
[].forEach.call(document.querySelectorAll('.firtstOne'), i => {
i.setAttribute('rowspan', i.dataset.rowSpan);
});
[].forEach.call(document.querySelectorAll('.die'), i => {
i.die();
});
我们贴出完整的代码:
// 合并单元格
function megeCell(columnClass) {
// 收集所有的列
var arr = document.querySelectorAll(columnClass);
var map = {};
for(var i = 0, j = arr.length; i < j; i++) {
var td = arr[i];
var orderId = td.dataset.orderId;
if(undefined === map[orderId]) map[orderId] = 1; // 统计跨行的有多少,用一个 map 装着
else map[orderId] = ++map[orderId];
}
var stack = [];
for(var i = 0, j = arr.length; i < j; i++) {
var td = arr[i];
var orderId = td.dataset.orderId;
var tds = map[orderId];
if( tds > 1 && stack.length === 0) {// 标记
for(var q = i; q < i + tds ; q++) {// 连续 tds 个都是要合并单元格的
if(q == i) {
arr[q].classList.add('firtstOne');
arr[q].dataset.rowSpan = tds;
} else {
arr[q].classList.add('die');
}
stack.push(arr[q]); // 入栈
}
}
if(stack.length && td === stack[0]) {
stack.shift();// 退栈
if(td.className.indexOf('firtstOne') != -1) {
} else {
td.classList.add('die'); // 要删除的元素
}
}
}
[].forEach.call(document.querySelectorAll('.firtstOne'), i => {
i.setAttribute('rowspan', i.dataset.rowSpan);
});
[].forEach.call(document.querySelectorAll('.die'), i => {
i.die();
});
}
megeCell('.order');
效果如下图所示。
如果要多列,也是没问题的,无非就是调用方法多次。
好了,跨行的完成了。最后留个作业给大家吧,如果要打造跨列的话,该怎么做呢?