美文网首页前端开发那些事儿每天学一点Vue3
封装第三方组件(20)完善td的拖拽功能,以及实现排序等功能

封装第三方组件(20)完善td的拖拽功能,以及实现排序等功能

作者: 自然框架 | 来源:发表于2021-08-16 17:40 被阅读0次

    昨天做了一个可以拖拽td的自定义指令,实现基本功能后开始写td的排序的功能,其间发现了几个小问题。

    几个小问题

    • 没有返回序号
      只返回 th 的内容,但是却没有返回是第几列的,这样外部程序还得去找,比较麻烦,应该内部解决

    • 没有直接返回左右
      想依据结束拖拽的时候鼠标指针在左还是右,以便于实现不同的功能,但是却只返回一个x坐标,这个不好判断,应该内部直接返回左还是右。

    • table重绘后th的事件也没了。
      由于内部实现的原因,th是用 v-for 循环出来的,调整顺序后,需要重新绘制一遍,于是某些th的事件丢失了。
      这个需要重新设置一遍。

    改进

    • 修改拖拽信息,dragInfo
      增加需要的属性。
    const dragInfo = reactive({
      offsetX: 0,
      isLeft: true, // 是否在 th 的左侧结束拖拽
      ctrl: false, // 是否按下了ctrl
      source: '',
      target: '',
      sourceIndex: 0, // 开始拖拽的位置
      targetIndex: 0 // 结束拖拽的位置
    })
    
    • 修改自定义指令
    
    /**
     * 拖拽 table 的 th,返回拖拽信息
     */
    const tableDrag = (app, options) => {
      app.directive('tabledrag', {
        // 指令的定义
        mounted (el, binding) {
          /**
           * 实现 th 的拖拽
           * @param {string} className 用于找到目标的 class 名称。
           * @param {reactive} dragInfo reactive 返回拖拽信息。
           * @returns 没有返回
           * * const dragInfo = {
           * *  offsetX: 0,
           * *  isLeft: true, // th 左侧结束拖拽
           * *  ctrl: false, // 是否按下ctrl
           * *  source: '', // 开始拖拽的th
           * *  target: '', // 结束拖拽的th
           * *  sourceIndex: 0, // 开始拖拽的序号
           * *  targetIndex: 0 // 结束拖拽的序号
           * * })
           */
          const setThforDrag = (className, dragInfo) => {
            const table = el.getElementsByClassName(className)[0]
            const tr = table.rows[0]
            const tdCount = tr.cells.length
            // 记录 th 的序号和宽度
            const thIndex = {}
            // 记录临时的源
            let src1 = ''
            let src2 = 1
            // 设置th的拖拽
            for (let i = 0; i < tdCount; i++) {
              const th = tr.cells[i]
              thIndex[th.innerText] = {
                index: i, // 记录th的序号
                width: th.offsetWidth // 记录 th 的宽度
              }
              // 设置可以拖拽
              th.setAttribute('draggable', true)
              // 拖拽时经过
              th.ondragover = (event) => {
                event.preventDefault()
              }
              // 开始拖拽
              th.ondragstart = (event) => {
                src1 = event.target.innerText
                src2 = thIndex[event.target.innerText].index
              }
              // 结束拖拽
              th.ondrop = (event) => {
                dragInfo.offsetX = event.offsetX
                dragInfo.ctrl = event.ctrlKey
                dragInfo.source = src1
                dragInfo.sourceIndex = src2
                dragInfo.target = event.target.innerText
                // 设置 th 的序号
                dragInfo.targetIndex = thIndex[event.target.innerText].index
                dragInfo.isLeft = dragInfo.offsetX < thIndex[event.target.innerText].width / 2
              }
            }
          }
          binding.value.setThforDrag = setThforDrag
        }
      })
    }
    export default tableDrag
    
    • thIndex
      在遍历的时候,记录th的位置和宽度,宽度 + x坐标 = 左右。

    • 结束拖拽时统一设置拖拽信息
      因为每次设置都是触发watch,而只有结束时的触发才是我们希望的,所以在开始拖拽的时候不能设置拖拽信息,只能临时保存,等到结束的时候一起设置。

    实现调整 th 的排序

    具体实现方式就不能在内部实现了,因为不同的项目有不同的实现方式,外面自己写就好。

    这里基于自己的项目实现一下。

    th 的实现方式

    我比较懒,不喜欢一个一个的设置 el-table-column,所以用了v-for

    <el-table-column
          v-for="(id) in colOrder"
          :colId="id"
          :column-key="id"
          :key="'grid_list_' + id"
          :fixed="id < fixedIndex"
          :prop="itemMeta[id].colName"
          :label="itemMeta[id].label"
          :width="itemMeta[id].width"
          :min-width="50"
          :align="itemMeta[id].align"
          :header-align="itemMeta[id]['header-align']"
          :filter-multiple="false"
          :show-overflow-tooltip="true"
          :formatter="myformatter"
        >
        </el-table-column>
    

    colOrder 是一个纯数组,元素只有number,那么调整排序就是调整这个数组里面的元素。

    插入 th

    把 th1 插入到 th2 的前面或者后面。这个应该是比较常见的一种调整方式吧。

      /**
       * 插入 th 后调整顺序
       */
      const _order = (cols) => {
        // 判断前插、后插。后插:偏移 0;前插:偏移 1
        const offsetTarget = dragInfo.isLeft ? 1 : 0
        // 判断前后顺序。》 1;《 0
        const offsetSource = dragInfo.sourceIndex < dragInfo.targetIndex ? 1 : 0
        // 插入源
        girdMeta.colOrder.splice(dragInfo.targetIndex - offsetTarget, 0, cols.id1)
        nextTick(() => {
          // 删除源
          girdMeta.colOrder.splice(dragInfo.sourceIndex - offsetSource, 1)
          // table被重置了,所以需要重新加拖拽事件,重新排序。
          nextTick(() => {
            tableInfo.setThforDrag(tableClass, dragInfo)
          })
        })
      }
    

    几次调试之后,代码写成了这样。这里使用 ES6 的 array.splice 来实现数组的调整。
    他可以删除指定位置的数组元素,也可以在指定位置添加数组元素,这个就很方便了。

    • offsetSource
      第一个 th 的偏移位置,删除的时候使用,因为从后面往前拖拽,和从前面往后面拖拽,位置会不一样。

    • offsetTarget
      第二个 th 的偏移位置,添加的时候使用,因为在前面插入和在后面插入,位置明显不同。

    位置确定好了之后就方便了,先把第一个 th 插入 第二个 th 的附近,然后删掉第一个th即可。

    • nextTick
      可能你会觉得奇怪,为啥要用 nextTick?
      我也不想呀,但是数组的直接修改,并不会引发模板的响应,就是说虽然数组内容变了,但是v-for并没有重新绘制。(不用问我为啥,我也不知道)
      所以只好先加入一个元素,这样会响应,然后在 nextTick 里面删除一个,实现拖拽后排序的功能。

    交换两个th的位置

    有的时候需要直接交换 th1 和 th2 的位置,其他 th 的位置不用变,这时我们直接交换就好。

      /**
       * 交换两个th的位置
       */
      const _changeSet = (cols) => {
        // 交换
        girdMeta.colOrder[dragInfo.sourceIndex - 1] = cols.id2
        girdMeta.colOrder[dragInfo.targetIndex - 1] = cols.id1
        // 强制响应
        girdMeta.colOrder.push(cols.id1)
        nextTick(() => {
          // 删除多余的col
          girdMeta.colOrder.splice(girdMeta.colOrder.length - 1, 1)
          // table被重置了,所以需要重新加拖拽事件,重新排序。
          nextTick(() => {
            tableInfo.setThforDrag(tableClass, dragInfo)
          })
        })
      }
    

    还是老问题,交换之后 v-for 不会响应重置,所以只好先加一个,然后 nextTick 里面再删掉一个。好无语。有没有更好的方法呢?

    设置 th、td 的对齐方式

    一般td有三种对齐方式:左、中、右。标题一般都是居中对齐,内容就不一定了,文本内容一般左对齐,数字一般右对齐,时间、状态等一般居中对齐,这时候需要设置一下。

    我们可以做一个规定:

    1. 拖拽时没有离开 th,则表示要修改对齐方式,而不是调整排序。
    2. 往左拖拽表示对齐方式“向左”,向右同理。
    3. 按住ctrl表示,要修改 th 的对齐方式,否则表示修改td。一般表头都是居中的。

    实现代码

      /**
       * 设置th的对齐方式
       */
      const _setThAlgin = (cols) => {
        // 判断 th 还是 td
        const alignKind = (dragInfo.ctrl) ? 'header-align' : 'align'
        const col = girdMeta.itemMeta[cols.id1]
        // 判断:左中右
        switch (col[alignKind]) {
          case 'left':
            if (dragInfo.isLeft) {
              col[alignKind] = 'left'
            } else {
              col[alignKind] = 'center'
            }
            break
          case 'center':
            if (dragInfo.isLeft) {
              col[alignKind] = 'left'
            } else {
              col[alignKind] = 'right'
            }
            break
          case 'right':
            if (dragInfo.isLeft) {
              col[alignKind] = 'center'
            } else {
              col[alignKind] = 'right'
            }
            break
        }
      }
    

    不多解释了,反正就是各种判断各种修改。

    入口

    上面是分别介绍功能,但是没有写入口代码。

      /**
       * 设置th的顺序
       */
      const setThOrder = () => {
        // 获取colId
        const cols = {
          id1: girdMeta.colOrder[dragInfo.sourceIndex - 1],
          id2: girdMeta.colOrder[dragInfo.targetIndex - 1]
        }
    
        if (dragInfo.sourceIndex !== dragInfo.targetIndex) {
          // 源和目标不同,排序
          if (dragInfo.ctrl) {
            // 交换
            _changeSet(cols)
          } else {
            // 插入
            _order(cols)
          }
        } else {
          // 源和目标相同,设置对齐方式
          _setThAlgin(cols)
        }
      }
    

    这样分解一下,代码应该可以更好维护。

    好吧,还有最后一个函数

    
    const dragInfo = reactive({
      offsetX: 0,
      isLeft: true, // 是否在 th 的左侧结束拖拽
      ctrl: false, // 是否按下了ctrl
      source: '',
      target: '',
      sourceIndex: 0, // 开始拖拽的位置
      targetIndex: 0 // 结束拖拽的位置
    })
    
    const { setThOrder } = manageTable(girdMeta, dragInfo, tableInfo)
    onMounted(() => {
      nextTick(() => {
        tableInfo.setThforDrag(tableClass, dragInfo)
        watch(() => dragInfo, () => {
          // console.log('外部:', dragInfo)
          setThOrder()
        },
        { deep: true })
      })
    })
    

    这样就ok了。

    做了一个简单的演示:

    拖拽效果

    相关文章

      网友评论

        本文标题:封装第三方组件(20)完善td的拖拽功能,以及实现排序等功能

        本文链接:https://www.haomeiwen.com/subject/cztvbltx.html