html表格拖动-表格列拖拽的前端实现

2023-09-04 0 4,834 百度已收录

因为是使用el-table实现的,所以一些捕获dom的类类是通过el-table的外部类来实现的。 其实如果是简单的桌子,可以自己降低等级。

底层框架/原则

可排序js

核心拖拽原理,我们使用sortablejs提供的dom拖拽方案来实现

我们将sortablejs的el参数赋值给el-table的表头

const query = ".el-table__header-wrapper thead tr"
const el docuemnt.querySelector(query) // this.$el.querySelector(query)

然后表头所在行的所有th都成为拖拽目标,然后按照索引的顺序进行改变,可以反过来到列切换

html表格拖动-表格列拖拽的前端实现

核心代码

const sortable = new Sortable(el, {
    onEnd(evt) {
        let { newIndex, oldIndex, item } = evt;
        // 通知上级交换column位置
    }
})

其他一些实现是跨表实现的

跨表实现的思路是在window上建立一个桥接映射缓存表与dom => vue实例对应

const sortable = new Sortable(el, {
    onEnd(evt) {
          const { to, from, pullMode } = evt;
          const toContext = window.bridge.get(to)
          const fromContext = window.bridge.get(from)
        let { newIndex, oldIndex, item } = evt;
        // 通知from和to对应的数据进行切换即可
    }
})

html表格拖动-表格列拖拽的前端实现

拖放优化

虽然核心代码很简单,但并不完美。 拖动时,只能拖动标题。 事实上,整个列并不是被拖在一起的。

所以我们需要优化样式,主要有两点

拖动列阴影时,所有td都跟随标题,拖拽阴影优化

阴影其实可以通过dataTransfer的setDragImage改变,参数支持传入一个dom

但是,因为列是由很多dom组成的,所以恢复它们实际上非常困难,所以我改变了主意。 通过dom.cloneNode复制一个表后,只有拖放列会被泄漏。 当然是需要插入的。 到当前页面

所以我们新建一个三层dom节点html表格拖动,分别是

所以我们通过改变setData来控制风暴,具体代码如下

        setData(dataTransfer, dragEl) {
            /**
             * 在页面上创建一个当前table的wrapper,然后隐藏它,只显示那一列的部分作为拖拽对象
             * 在下一个事件循环删除dom即可
             */
            const { offsetLeft, offsetWidth, offsetHeight } = dragEl;
            const tableEl = elTableContext.$el;
            const wrapper = document.createElement("div"); // 可视区域
            wrapper.style = `position: fixed; z-index: -1;overflow: hidden; width: ${offsetWidth}px`;
            const tableCloneWrapper = document.createElement("div"); // table容器,宽度和位移
            tableCloneWrapper.style = `position: relative; left: -${offsetLeft}px; width: ${tableEl.offsetWidth}px`;
            wrapper.appendChild(tableCloneWrapper);
            tableCloneWrapper.appendChild(tableEl.cloneNode(true));
            // 推进dom,让dataTransfer可以获取
            document.body.appendChild(wrapper);
            // 拖拽位置需要偏移到对应的列上
            dataTransfer.setDragImage(
              wrapper,
              offsetLeft + offsetWidth / 2,
              offsetHeight / 2
            );
            setTimeout(() => {
              document.body.removeChild(wrapper);
            });
        },

之后拖拽的阴影就完全正常了,就是当前拖拽的那一列

html表格拖动-表格列拖拽的前端实现

拖拽关注联通

找到问题了

因为实际上dom位置不需要移动,这样切换列变换后可以手动重新渲染el-table,所以有必要

在实际操作中,一开始我希望把整个操作简化为,onMove时,交换下对应的td

我原本想到的匹配规则是根据th当前的索引来推断对应的td列表,但是我发现因为onMove会导致索引改变,所以查询到的列表是错误的。 最后我发现每个td都会被添加到el-table中。 当前列的列名类似于table0-column2,所以只要拖拽th,就可以通过document.querySelectAll查询得到对应的td

其次,交易所需要推断对应的仓位。 一开始我尝试用需要交换的对应td的当前位置作为新位置。 但是我发现如果减少动画效果的话,通过getBoundingClientRect获取到的位置其实是在动画中。 如果连续拖拽的话,中间交换的列位置就会出现问题,所以还是需要修正

并且,计算中国联通的位置html表格拖动,需要考虑中国联通当前的位置,才能估计出真正的转变

所以我最终选择的解决方案是

具体的代码工具功能

/* eslint-disable no-unused-vars */
import throttle from 'lodash/throttle'
import Sortable from "sortablejs";
const { utils } = Sortable;
const { css } = utils;
/** @type {Set} */
const animatedSet = new Set();
export const ANIMATED_CSS = "el-table-draggable-animated";
const translateRegexp = /translate((?.*)px,s?(?.*)px)/;
const elTableColumnRegexp = /el-table_d*_column_d*/
/**
 * 重设transform
 * @param {Element} el 
 */
function resetTransform(el) {
  css(el, "transform", "");
  css(el, "transitionProperty", "");
  css(el, "transitionDuration", "");
}
/**
 * 获取原始的boundge位置
 * @param {Element} el
 * @param {boolean} ignoreTranslate
 * @returns {{x: number, y: number}}
 */
export function getDomPosition(el, ignoreTranslate = true) {
  const position = el.getBoundingClientRect().toJSON();
  const transform = el.style.transform;
  if (transform && ignoreTranslate) {
    const { groups = { x: 0, y: 0 } } = translateRegexp.exec(transform) || {};
    position.x = position.x - +groups.x;
    position.y = position.y - +groups.y;
  }
  return position;
}
/**
 * 添加动画
 * @param {Element} el
 * @param {string} transform
 * @param {number} animate
 */
export function addAnimate(el, transform, animate = 0) {
  el.classList.add(ANIMATED_CSS);
  css(el, "transitionProperty", `transform`);
  css(el, "transitionDuration", animate + "ms");
  css(el, "transform", transform);
  animatedSet.add(el);
}
/**
 * 清除除了可忽略选项内的动画
 * @param {Element[]|Element} targetList
 */
export function clearAnimate(targetList = []) {
  const list = Array.isArray(targetList) ? targetList : [targetList]
  const removedIteratory = list.length ? list : animatedSet.values()
  for (const el of removedIteratory) {
    el.classList.remove(ANIMATED_CSS);
    resetTransform(el)
    if (animatedSet.has(el)) {
      animatedSet.delete(el);
    }
  }
}
/**
 * 获取移动的animate
 * @param {Element} el
 * @param {{x?: number, y?:number}} target
 * @returns {string}
 */
 export function getTransform(el, target) {
  const currentPostion = getDomPosition(el)
  const originPosition = getDomPosition(el, true)
  const { x, y } = target
  const toPosition = {
    x: x!==undefined ? x : currentPostion.x,
    y: y!==undefined ? y : currentPostion.y
  }
  const transform = `translate(${toPosition.x -
    originPosition.x}px, ${toPosition.y - originPosition.y}px)`
  return transform
}
/**
 * 移动到具体位置
 * @param {Element} el
 * @param {{x?: number, y?:number}} target
 * @returns {string}
 */
export function translateTo(el, target) {
  resetTransform(el)
  const transform = getTransform(el, target)
  el.style.transform = transform
}
/**
 * 交换
 * @param {Element} newNode
 * @param {Element} referenceNode
 * @param {number} animate
 */
export function insertBefore(newNode, referenceNode, animate = 0) {
  /**
   * 动画效果
   * @todo 如果是不同列表,动画方案更新
   */
  if (animate) {
    // 同一列表处理
    if (newNode.parentNode === referenceNode.parentNode) {
      // source
      const offset = newNode.offsetTop - referenceNode.offsetTop;
      if (offset !== 0) {
        const subNodes = Array.from(newNode.parentNode.children);
        const indexOfNewNode = subNodes.indexOf(newNode);
        const indexOfReferenceNode = subNodes.indexOf(referenceNode);
        const nodes = subNodes
          .slice(
            Math.min(indexOfNewNode, indexOfReferenceNode),
            Math.max(indexOfNewNode, indexOfReferenceNode)
          )
          .filter((item) => item !== newNode);
        const newNodeHeight =
          offset > 0 ? -1 * newNode.offsetHeight : newNode.offsetHeight;
        nodes.forEach((node) =>
          addAnimate(node, `translateY(${newNodeHeight}px)`, animate)
        );
        addAnimate(newNode, `translateY(${offset}px)`, animate);
      }
    } else {
      console.log("非同一列表");
    }
    // 清除
    setTimeout(() => {
      clearAnimate();
    }, animate);
  }
  referenceNode.parentNode.insertBefore(newNode, referenceNode);
}
/**
 * 交换
 * @param {Element} newNode
 * @param {Element} referenceNode
 * @param {number} animate
 */
export function insertAfter(newNode, referenceNode, animate = 0) {
  const targetReferenceNode = referenceNode.nextSibling;
  insertBefore(newNode, targetReferenceNode, animate);
}
/**
 * 交换元素位置
 * @todo 优化定时器
 * @param {Element} prevNode
 * @param {Element} nextNode
 * @param {number} animate
 */
export function exchange(prevNode, nextNode, animate = 0) {
  const exchangeList = [
    {
      from: prevNode,
      to: nextNode,
    },
    {
      from: nextNode,
      to: prevNode,
    },
  ];
  exchangeList.forEach(({ from, to }) => {
    const targetPosition = getDomPosition(to, false)
    const transform = getTransform(from, targetPosition);
    addAnimate(from, transform, animate);
  });
}
/**
 * 从th获取对应的td
 * @todo 支持跨表格获取tds
 * @param {Element} th
 * @returns {NodeListOf}
 */
export function getTdListByTh(th) {
  const className = Array.from(th.classList).find(className => elTableColumnRegexp.test(className))
  return document.querySelectorAll(`.${className}`)
}
/**
 * 自动对齐列
 * @param {Element[]|Element} thList 
 */
export const alignmentTableByThList = throttle(
  function alignmentTableByThList(thList) {
    const list = Array.isArray(thList) ? thList : [thList]
    list.forEach(th => {
      const tdList = getTdListByTh(th)
      tdList.forEach(td => {
        const { x } = getDomPosition(th)
        translateTo(td, { x })
      })
    })
  },
  1000 / 60
)
export default {
  alignmentTableByThList,
  getTransform,
  clearAnimate,
  addAnimate,
  ANIMATED_CSS,
  getTdListByTh,
  translateTo,
  getDomPosition,
  insertAfter,
  insertBefore,
  exchange,
};

拖拽联通手动排序

onMove(evt, originalEvent) {
        const { related, willInsertAfter, dragged } = evt;
        
        // 工具函数,自动对齐之前的列
        dom.alignmentTableByThList(Array.from(dragged.parentNode.childNodes))
        
        // 交换dom位置,动画
            const { animation } = vm._sortable.options;
            // 需要交换两列所有的td
            const thList = [dragged, related];
            const [fromTdList, toTdList] = (willInsertAfter
              ? thList
              : thList.reverse()
            ).map((th) => dom.getTdListByTh(th));
            fromTdList.forEach((fromTd, index) => {
              const toTd = toTdList[index];
              // 交换td位置
              dom.exchange(fromTd, toTd, animation);
            });

当然还有一些可以优化的地方,不过目前来说是比较完善的栏目拖拽

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 html html表格拖动-表格列拖拽的前端实现 https://www.wkzy.net/game/192669.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务