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

简言

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

表格合并

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

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

结语

结束了。

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZSK6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值