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

news2024/12/28 4:27:21

简言

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

表格合并

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

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

结语

结束了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1634901.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

点成分享 | 温度控制的艺术:TX150系列水浴中的稳定性与均匀性解析

前言 在实验室和工业生产中&#xff0c;温度控制对于确保实验结果的精确性和产品的高质量至关重要&#xff0c;尤其是针对温度敏感的样品和原材料&#xff0c;如蛋白酶等&#xff0c;微小的温度误差都会对实验结果可靠性和生产质量造成影响。而在控温性能中&#xff0c;稳定性…

Pytorch入门实战 P08-YOLOv5里面的C3模块实现

目录 1、YOLOv5骨干网络模型图&#xff1a; 2、C3模块介绍&#xff1a; 3、C3模块的主要代码&#xff1a; 4、完整的code 5、运行结果展示&#xff1a; &#xff08;1&#xff09;使用SGD优化器 &#xff08;2&#xff09;使用Adam优化器 &#x1f368; 本文为&#x1f…

2024年必应bing广告推广开户有什么条件?

必应Bing作为全球领先的搜索引擎之一&#xff0c;其广告平台正为无数企业开辟着新的市场蓝海。如果您正寻求在必应Bing上投放广告&#xff0c;提升品牌影响力和市场份额&#xff0c;那么了解开户条件并找到一位可靠的合作伙伴至关重要。云衔科技&#xff0c;作为数字营销领域的…

Jetson Orin NX L4T35.5.0平台LT6911芯片 调试记录(2)vi discarding frame问题调试

基于上篇调试记录 Jetson Orin NX L4T35.5.0平台LT6911芯片 调试记录(1)MIPI问题调试-CSDN博客 1.前言 当通过gstreamer持续捕获视频设备时,帧数会下降,并且I输入越高,丢失的帧数越多。 当达到4k30hz时,它完全无法使用,系统会在几秒钟的收集后崩溃并重新启动 4k30hz …

使用yolo识别模型对比两张图片并标记不同(2)

上篇文章有漏洞&#xff0c;在这里补充下&#xff0c;比如要识别第二张图相对于第一张图的违建是否拆除了 第一步旋转对其后&#xff0c;图片会有黑色的掩码&#xff0c;如果旋转角度大的话&#xff0c;没识别出来的框可能不是已经拆除了&#xff0c;而是因为黑色掩码遮挡&…

Laravel 框架请求生命周期

Laravel 框架请求的生命周期 目录 请求图示 说明 ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ 请求图示 说明 ① 所有的请求都是经Web 服务器&#xff08;Apache/Nginx&#xff09;配置引导到Laravel 应用的入口public/index.php文件。index.php 加载框架其它部分。 如下图&#xff…

基于FPGA的数字信号处理(3)--什么是浮点数?

科学计数法 你可能不了解「浮点数」&#xff0c;但你一定了解「科学记数法」。 10进制科学记数法把一个数表示成a与10的n次幂相乘的形式&#xff08;1≤|a|<10&#xff0c;a不为分数形式&#xff0c;n为整数&#xff09;&#xff0c;例如&#xff1a; 19970000000000 1.9…

关系(五)利用python绘制连接散点图

关系&#xff08;五&#xff09;利用python绘制连接散点图 连接散点图&#xff08;Connected Scatterplot&#xff09;简介 连接散点图&#xff08;点线图&#xff09;是折线图的一种&#xff0c;与散点图类似。但添加了按数据点出现顺序的连线&#xff0c;以此来表示两个变量…

币圈Cryptosquare论坛

Cryptosquare综合性资讯论坛汇集了币圈新闻、空投信息、社会热点以及与Web3相关的工作信息。让我们一起解锁加密世界的种种可能性&#xff0c;探索Cryptosquare论坛带来的精彩&#xff01; 币圈新闻板块&#xff1a; Cryptosquare论坛的币圈新闻板块是用户获取最新加密货币行业…

vite打包配置

目录 minify默认是esbuild&#xff0c;不能启动下面配置 使用&#xff1a; plugins: [viteMockServe({mockPath: mock})]根目录新建mock/index.ts. 有例子Mock file examples&#xff1a;https://www.npmjs.com/package/vite-plugin-mock-server 开发环境生产环境地址替换。根…

Matlab|含sop的33节点配电网优化

目录 1 主要内容 2 部分代码 3 程序结果 4 下载链接 1 主要内容 程序以IEEE33节点为例&#xff0c;分析含sop的配电网优化&#xff0c;包括sop有功约束、无功约束和容量约束&#xff0c;非线性部分通过转换为旋转锥约束进行编程&#xff0c;并且包括33节点配电网潮流及对应…

python自动化操作docx

使用Python自动化处理Word文档 在日常工作中&#xff0c;我们经常需要处理大量的Word文档&#xff0c;这时自动化脚本就显得尤为重要。本文将介绍如何使用Python中的python-docx库来创建和修改Word文档。 安装python-docx库 在开始之前&#xff0c;确保你已经安装了python-d…

基于JWT实现的Token认证方案

JSON Web Token是什么&#xff1f; JSON Web Token&#xff08;JWT&#xff09;是目前最流行的跨域身份验证解决方案。 JSON Web Token&#xff08;JWT&#xff09;是一个开放标准&#xff08;RFC 7519&#xff09;&#xff0c;它定义了一种紧凑且自包含的方式&#xff0c;用…

电脑文件误删除如何恢复?这5个策略亲测有效!

“求助&#xff01;在电脑上不小心删除了文件还有机会找回来吗&#xff1f;一不小心我就删除了一个重要的工作文件&#xff01;大家快帮帮我吧&#xff01;” 保存在电脑里的文件对电脑用户来说很多都是非常重要的&#xff0c;我们可能生活中、学习上以及工作上都需要使用这些文…

C++学习第七课:控制程序流程的学习和示例详解

C学习第七课&#xff1a;控制程序流程 在C中&#xff0c;控制程序流程是编程逻辑的核心部分&#xff0c;它决定了程序的执行顺序。本课我们将介绍C中的各种控制流程语句&#xff0c;包括条件语句、循环语句以及如何使用它们遍历多维数组和计算斐波那契数列。 控制流程语句 i…

哪个牌子的骨传导耳机好用?盘点五款高热度爆款骨传导耳机推荐!

近年来&#xff0c;骨传导耳机在潮流的推动下销量节节攀升&#xff0c;逐渐成为运动爱好者和音乐迷们的必备装备。但热度增长的同时也带来了一些品质上的忧患&#xff0c;目前市面上的部分产品&#xff0c;存在佩戴不舒适、音质不佳等问题&#xff0c;甚至可能对听力造成潜在损…

VSCode SSH连接远程主机失败,显示Server status check failed - waiting and retrying

vscode ssh连接远程主机突然连接不上了&#xff0c;终端中显示&#xff1a;Server status check failed - waiting and retrying 但是我用Xshell都可以连接成功&#xff0c;所以不是远程主机的问题&#xff0c;问题出在本地vscode&#xff1b; 现象一&#xff1a; 不停地输入…

Python俄罗斯方块

文章目录 游戏实现思路1. 游戏元素的定义2. 游戏区域和状态的定义3. 游戏逻辑的实现4. 游戏界面的绘制5. 游戏事件的处理6. 游戏循环7. 完整实现代码 游戏实现思路 这个游戏的实现思路主要分为以下几个步骤&#xff1a; 1. 游戏元素的定义 Brick类&#xff1a;表示游戏中的砖…

使用Tortoise 创建远程分支

1。首先创建本地分支branch1&#xff0c;右键tortoise git->创建分支&#xff0c;输入分支名称branch1&#xff0c;确定。 2。右键tortoise git->推送&#xff0c;按下图设置&#xff0c;确定&#xff0c;git会判断远程有没有分支branch1&#xff0c;如果没有会自动创建…

QT类之间主窗口子窗口传递*指针对象

1.新建CFile_Operation 类文件 2.主窗口头文件声明&#xff1a; CFile_Operation *cfile_operation; 按钮点击事件函数里面调用子窗口 dialog_debug new Dialog_Debug(this);connect(this,&MainWindow_oq::SendCfile_operation_Obj,dialog_debug,&Dialog_Debug::R…