简言
介绍实现表格合并的一种方法。
表格合并
表格合并操作是一个比较复杂的操作,它主要分为以下步骤:
- 获取选中区域
- 选择合并显示的单元格
- 实现合并操作。
我们就逐一实现这三步,最后实现一个较完整的合并操作。(不考虑边界情况)
获取选中区域
选中区域这里相对来说比较难,它是第一步,也是最重要的一步,只要选的不对,白搭。
还有就是正常的选区,它可以有以下四种选中方向:
这里只考虑第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) returnconsole.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 = 0table.addEventListener('mousedown', (e) => {if (timer !== 0) {clearTimeout(timer)timer = 0}timer = setTimeout(() => {// 先清空clearSelect()run = truestartRange = [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] = startRangeselect.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)}// 更新选取范围 xselect.range[1] = [select.range[1][0] + e.target.offsetWidth, select.range[1][1]]} else if (x < 0) {if (select.value[0].length <= 1) returnconsole.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) returnselect.range[1] = [select.range[1][0], lastEnd[1]]lastEnd = [lastEnd[0], lastEnd[1] - e.target.offsetHeight]lastInfo = [lastInfo[0], e.target.offsetHeight]// 去掉最后一行的classselect.value[select.value.length - 1].forEach(el => {el.classList.remove('select')})select.value.pop()}// 选中元素添加classfor (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 = falseif (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 nulllet 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属性,导致无法再次合并。
- 无法再次合并。
- 事件触发较频繁
结语
结束了。