【前端】表格合并如何实现?

devtools/2024/9/24 5:26:31/

简言

介绍实现表格合并的一种方法。

表格合并

表格合并操作是一个比较复杂的操作,它主要分为以下步骤:

  1. 获取选中区域
  2. 选择合并显示的单元格
  3. 实现合并操作。

我们就逐一实现这三步,最后实现一个较完整的合并操作。(不考虑边界情况)

获取选中区域

选中区域这里相对来说比较难,它是第一步,也是最重要的一步,只要选的不对,白搭。
还有就是正常的选区,它可以有以下四种选中方向:
在这里插入图片描述

这里只考虑第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属性,导致无法再次合并。
  • 无法再次合并。
  • 事件触发较频繁

结语

结束了。


http://www.ppmy.cn/devtools/25020.html

相关文章

STM32 ULINK

STM32 ULINK ULINK 是 ARM 公司旗下 Keil 公司推出的一系列仿真器产品&#xff0c;主要用于嵌入式系统开发中的调试、测试和编程。它提供了与 Keil MDK 集成开发环境&#xff08;Microcontroller Development Kit&#xff09;紧密配合的调试和仿真功能&#xff0c;能够与 Keil…

高速应急广播校园网络广播防水音柱40-80W应急广播ip音柱

高速应急广播校园网络广播防水音柱40-80W应急广播ip音柱 SV-7042是一款网络音柱&#xff0c;具有10/100M以太网接口&#xff0c;从网络接口接收网络的音频数据解码后播放。 本网络音柱一般内置4个喇叭&#xff0c;提供立体声的音频播放。本网络音柱可直接播放来自网络的音频&…

SpringBoot War打包部署

修改打包方式 <packaging>war</packaging>修改 Servlet 容器的 scope <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId><scope>provided</scope></d…

目标检测与追踪AI算法模型及边缘计算智能分析网关V4的算法应用

目标检测与追踪是计算机视觉领域中的一个重要任务&#xff0c;主要用于识别图像或视频中的目标&#xff0c;并跟踪它们的运动轨迹。针对这一任务&#xff0c;有许多先进的AI算法模型&#xff0c;例如&#xff1a; YOLO&#xff08;You Only Look Once&#xff09;&#xff1a;…

如何解决升级IntelliJ IDEA 2024后 打开项目就自动闪退关闭问题的终极指南

title: “&#x1f42f; 解决升级IntelliJ IDEA 2024后项目自动关闭的终极指南” date: 2024-04-23 author: 猫头虎 profile: CSDN 文章目录 title: "&#x1f42f; 解决升级IntelliJ IDEA 2024后项目自动关闭的终极指南" date: 2024-04-23 author: 猫头虎 profile: …

【Unity基础】TextMeshPro组件学习过程记录

目录 1.TextMeshPro组件渲染创建文本RTL Editor字体Font Asset字体加粗&#xff0c;下划线等字体大小控制字体颜色控制字体渐变控制字符间隔、单词间隔、行间距、段落间距控制WrappingUV映射控制代码 2.TextMeshPro组件AssetFace InfoGeneration Setting 3.使用Dynamic SDF Sys…

Java 集合中获取数据的前驱和后继元素

目录 1. NavigableSet 和 NavigableMap 2. ListIterator 3. ConcurrentSkipListSet 和 ConcurrentSkipListMap 4.示列 使用场景 使用一致性hash时&#xff0c;如何找到一个hash值对应的临近节点&#xff0c;可以使用集合中获取数据的前驱和后继元素实现。 1. NavigableSet…

C/C++程序设计实验报告综合作业 | 小小计算器

本文整理自博主本科大一《C/C程序设计》专业课的课内实验报告&#xff0c;适合C语言初学者们学习、练习。 编译器&#xff1a;gcc 10.3.0 ---- 注&#xff1a; 1.虽然课程名为C程序设计&#xff0c;但实际上当时校内该课的内容大部分其实都是C语言&#xff0c;C的元素最多可能只…