简言
介绍实现表格合并的一种方法。
表格合并
表格合并操作是一个比较复杂的操作,它主要分为以下步骤:
- 获取选中区域
- 选择合并显示的单元格
- 实现合并操作。
我们就逐一实现这三步,最后实现一个较完整的合并操作。(不考虑边界情况)
获取选中区域
选中区域这里相对来说比较难,它是第一步,也是最重要的一步,只要选的不对,白搭。
还有就是正常的选区,它可以有以下四种选中方向:
这里只考虑第3种,其他的可自行实现(利用x和y差值方向)。
另外,还有就是选区取消实现,例如我选中了2-3,2-4,然后我的鼠标又移回2-3区域了,那么2-4就应该取消选中。
思路
这里我选择的是利用鼠标按下、移动、抬起事件来实现长按选中操作,期间记录选中的节点和范围,以及最后选中节点的位置。
代码在示例。
选择合并显示的单元格
要选择合并显示的单元格,首先要判断你怎么选区的(选区方向)。
因为table元素中,一般都是靠前的td元素修改colspan和rowspan属性来执行合并操作。
示例代码 只考虑了 正向选区一种,即默认第一个为靠前td元素
代码在示例。
实现合并操作
合并操作这里主要处理选中区域的单元格,根据选中个数和合并情况来处理合并操作。
示例实现的是右键合并操作
示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表格合并</title>
<style>
.zsk-table {
border-collapse: collapse;
border: 1px solid;
font-family: inherit;
user-select: none;
}
.zsk-table tr {
height: 32px;
}
.zsk-table td {
border: 1px solid;
height: 32px;
padding: 16px;
}
.amount {
width: 100px;
}
.show-box {
position: absolute;
top: -200px;
left: -200px;
width: 200px;
background-color: #eee;
}
.show-box>div {
width: 200px;
height: 50px;
line-height: 50px;
border-bottom: 1px solid #000;
}
.show-box>div:hover {
background-color: #ccc;
cursor: pointer;
}
.select {
color: #fff;
background-color: #3987cf;
}
.hide {
display: none;
}
</style>
</head>
<body>
<h1>表格合并</h1>
<table tabindex="1" class="zsk-table">
<tr>
<td>1-1</td>
<td>1-2</td>
<td>1-3</td>
<td>1-4</td>
<td>1-5</td>
</tr>
<tr>
<td>2-1</td>
<td>2-2</td>
<td>2-3</td>
<td>2-4</td>
<td>2-5</td>
</tr>
<tr>
<td>3-1</td>
<td>3-2</td>
<td>3-3</td>
<td>3-4</td>
<td>3-5</td>
</tr>
</table>
<!-- 表格右键 -->
<div class="show-box">
<div>向下添加一行</div>
<div>向上添加一行</div>
<div>删除当前行行</div>
<div class="merge-cell">合并</div>
</div>
<script>
const table = document.querySelector('.zsk-table')
const showBox = document.querySelector('.show-box')
const mergeDiv = document.querySelector('.merge-cell')
const select = { // 选中单元格
value: [[]],
range: [[], []] // [start,end]范围
}
// 合并命令
mergeDiv.addEventListener('click', () => {
if (select.value.length === 0) return
console.log(select.range, 'range');
// 默认是正向选中,即结尾点比开始点的x和y都大
select.value.forEach((item, i) => {
item.forEach((v, k) => {
if (i === 0 && k === 0) {
console.log(v, '显示项');
v.setAttribute('colspan', item.length || '1')
v.setAttribute('rowspan', select.value.length || '1')
} else {
v.classList.add('hide')
}
})
})
clearSelect()
})
// 右键
table.addEventListener('click', (e) => {
e.target.focus()
})
table.addEventListener("contextmenu", (e) => {
e.preventDefault()
console.log(e.target, '右键', e)
showBox.style.left = e.clientX + 'px'
showBox.style.top = e.clientY + 'px'
})
table.addEventListener('blur', (e) => {
setTimeout(() => {
showBox.style.left = -1000 + 'px'
showBox.style.top = -1000 + 'px'
}, 150)
})
/**
* 选中逻辑
*
**/
selectLogic(table, select)
function selectLogic(table, select) {
let lastEnd = [0, 0] // 最后选中的单元格位置
let lastInfo = [0, 0] // 最后选中单元格的宽高
let endUp = [0, 0]
let startRange = [0.0]
let endRange = [0, 0]
let run = false
// 按下
let timer = 0
table.addEventListener('mousedown', (e) => {
if (timer !== 0) {
clearTimeout(timer)
timer = 0
}
timer = setTimeout(() => {
// 先清空
clearSelect()
run = true
startRange = [e.clientX - e.offsetX, e.clientY - e.offsetY]
lastEnd = [startRange[0], startRange[1]]
lastInfo = [e.target.offsetWidth, e.target.offsetHeight]
e.target.classList.add('select')
if (e.target.tagName === 'TD') {
select.value[0].push(e.target)
select.range[0] = startRange
select.range[1] = [startRange[0] + e.target.offsetWidth, startRange[1] + e.target.offsetHeight]
}
}, 200)
})
// 移动
table.addEventListener('mousemove', (e) => {
if (run) {
end = [e.clientX, e.clientY]
console.log(`x: ${end[0] - startRange[0]} y: ${end[1] - startRange[1]} 范围:${select.range[1][0] - select.range[0][0]}`);
// 计算范围 然后 判断是否修改选中dom数组
let x = end[0] - lastEnd[0]
let y = end[1] - lastEnd[1]
if (x > lastInfo[0]) {
console.log('横向超出,x扩展');
lastEnd = [select.range[1][0], lastEnd[1]]
lastInfo = [e.target.offsetWidth, lastInfo[1]]
// 每行横向添加一行
for (let i = 0; i < select.value.length; i++) {
// 查找最后一个节点元相邻td元素
console.log(select.value[i]);
let el = getNextElement(select.value[i][select.value[i].length - 1])
select.value[i].push(el)
}
// 更新选取范围 x
select.range[1] = [select.range[1][0] + e.target.offsetWidth, select.range[1][1]]
} else if (x < 0) {
if (select.value[0].length <= 1) return
console.log(select.value[0].length, '当前个数');
select.range[1] = [lastEnd[0], select.range[1][1]]
lastEnd = [lastEnd[0] - e.target.offsetWidth, lastEnd[1]]
lastInfo = [lastInfo[0], e.target.offsetHeight]
// 减去每行的最后一个
for (let i = 0; i < select.value.length; i++) {
if (select.value[i].length > 0) {
select.value[i][select.value[i].length - 1].classList.remove('select')
select.value[i].pop()
}
}
}
if (y > lastInfo[1]) {
console.log('纵向超出,y扩展', select.value[0].length);
lastEnd = [lastEnd[0], select.range[1][1]]
lastInfo = [lastInfo[0], e.target.offsetHeight]
const lastRow = []
for (let k = 0; k < select.value[0].length; k++) {
let el = select.value[select.value.length - 1][k]
lastRow.push(getNextRowXElement(el))
}
select.value.push(lastRow)
// 更新选区范围
select.range[1] = [select.range[1][0], select.range[1][1] + e.target.offsetHeight]
} else if (y < 0) {
if (select.value.length < 1) return
select.range[1] = [select.range[1][0], lastEnd[1]]
lastEnd = [lastEnd[0], lastEnd[1] - e.target.offsetHeight]
lastInfo = [lastInfo[0], e.target.offsetHeight]
// 去掉最后一行的class
select.value[select.value.length - 1].forEach(el => {
el.classList.remove('select')
})
select.value.pop()
}
// 选中元素添加class
for (let i = 0; i < select.value.length; i++) {
for (let k = 0; k < select.value[i].length; k++) {
select.value[i][k].classList.add('select')
}
}
// select.value.push(e.target)
// e.target.classList.add('select')
}
})
// 抬起
table.addEventListener('mouseup', (e) => {
run = false
if (timer !== 0) {
clearTimeout(timer)
timer = 0
}
})
}
/*
获取下一行当前横坐标相同位置元素
*/
function getNextRowXElement(currentElement) {
let nextElement = currentElement.parentElement.nextElementSibling.firstElementChild;
let currentLeft = currentElement.offsetLeft;
let nextElementLeft = nextElement.offsetLeft;
while (nextElement !== null && nextElementLeft !== currentLeft) {
nextElement = getNextElement(nextElement);
nextElementLeft = nextElement.offsetLeft;
}
return nextElement;
}
/**
* 获取下一个兄弟元素
**/
function getNextElement(element) {
if (element.nextElementSibling) {
return element.nextElementSibling;
} else {
return null
let parent = element.parentElement;
while (parent && parent.nextElementSibling === null) {
parent = parent.parentElement;
}
return parent ? parent.nextElementSibling.firstElementChild : null;
}
}
function clearSelect() {
select.value.forEach((item, index) => {
item.forEach(v => {
v.classList.remove('select')
})
})
Object.assign(select, {
value: [[]],
range: [[], []] // [start,end]范围
})
}
</script>
</body>
</html>
问题
- 选中区域方向问题
- 选中节点信息没有处理colspan和rowspan属性,导致无法再次合并。
- 无法再次合并。
- 事件触发较频繁
结语
结束了。