因为是使用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都成为拖拽目标,然后按照索引的顺序进行改变,可以反过来到列切换
核心代码
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对应的数据进行切换即可
}
})
拖放优化
虽然核心代码很简单,但并不完美。 拖动时,只能拖动标题。 事实上,整个列并不是被拖在一起的。
所以我们需要优化样式,主要有两点
拖动列阴影时,所有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);
});
},
之后拖拽的阴影就完全正常了,就是当前拖拽的那一列
拖拽关注联通
找到问题了
因为实际上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);
});
当然还有一些可以优化的地方,不过目前来说是比较完善的栏目拖拽