前言
SortableJS 是一个功能强大的 JavaScript 拖拽库,但在使用过程中不免会遇到一些令人抓挠的“坑”。
本片文章将记录自身在一些应用场景下遇到的一些问题,以及问题的解决方案。
SortableJS 的基础使用可以参考:SortableJS,一个简单的 JS 拖拽库
拖动克隆 clone
1、业务场景:
比如有左右两个拖拽容器,左侧容器内的 Element
可以拖拽(clone)放置在右侧容器中。
<div className="row" style={{ display: 'flex' }}>
<div id="containerLeft" className="list-group col-6" style={{ flex: 1 }}>
<div className="list-group-item" onClick={() => setCount(count + 1)}>
Item 1 + {count}
</div>
...
</div>
<div id="containerRight" className="list-group col-6" style={{ flex: 1 }}>
...
</div>
</div>
new Sortable(containerLeft, {
group: {
name: 'shared',
pull: 'clone',
push: false,
}, // set both lists to same group
animation: 150
});
new Sortable(containerRight, {
group: {
name: 'shared',
pull: false,
push: true,
},
animation: 150
});
2、问题描述:
当左侧容器内的 Element 被拖拽 clone 到右侧容器后,这个目标 Element 在左侧容器内将无法读到新的状态值(比如可以是 react 的 state),并且事件都失效了(比如 onclick 点击事件)。
比如每个 Element 只允许被拖拽一次到右侧容器内。在 Element 成功拖拽到右侧容器后,需要在左侧容器内更新 state 对这个元素进行禁用。
但是会发现,state 是更新了,其他 Element 也能读到新的 state,唯独此 Element 无法读取到。
反观,在拖动成功到右侧容器中的这个 Element,可以接收 state 的更新。
3、思路分析:
根据问题描述的客观分析,猜想拖拽 clone
是将 target 元素
移动到了新的容器内,而留下了一个不会流动的 clone 元素
。下面我们论证一下这一点。
1)输出 evt 事件对象:
我们可以在拖拽结束后的 onEnd
事件内输出 evt
事件对象:
new Sortable(containerLeft, {
group: {
name: 'shared',
pull: 'clone',
push: false,
}, // set both lists to same group
animation: 150,
onEnd(evt: any) {
console.log(evt);
},
});
2)查看 evt 事件对象:
现在我们回到浏览器,打开调试终端可以看到 evt
对象结构,我们先关注两个参数属性:clone
和 item
。
当我们鼠标移动到 clone
参数值上时,对应的是左侧容器内的此 Element:
当我们鼠标移动到 item
参数值上时,对应的是拖拽至右侧容器内的此 Element:
当我们在 Element 上添加了点击事件,拖拽后会发现:
- 在左侧容器内点击此元素不会触发 onClick 事件,事件内的更新 state 的动作也不会执行;
- 反观在右侧容器内点击此元素,会触发 onClick 事件,并且 state 也在变化。
3)思考方案:
通常这种场景都是数据 List
来渲染节点,也就是说当拖拽成功后,我们需要将数据添加到右侧容器 List
内,由数据去渲染视图;因此我们就可以想到:将拖拽成功至右侧容器的 Element,还原到左侧容器内。
因此我们的方案分为两步:
- 移除左侧容器内无法流动的
clone
Element; - 将右侧容器内正常的
item
Element 还原至左侧容器内。
4、解决方案:
export const resetDragElement = (evt) => {
// const { target, clone, item, oldIndex } = evt;
const { target, clone, item } = evt;
// 考虑到在容器中,会存在部分元素可以拖拽,而非所有元素都可以拖拽,若使用 evt.oldIndex 就会导致存在 bug
const oldIndex = Array.from(target.children).findIndex(child => child === clone);
if (target.contains(clone)) {
target.removeChild(clone);
target.insertBefore(item, target.children[oldIndex]);
}
}
new Sortable(containerLeft, {
group: {
name: 'shared',
pull: 'clone',
push: false,
}, // set both lists to same group
animation: 150,
onEnd(evt: any) {
console.log(evt);
resetDragElement(evt);
},
});
值得注意的是:
oldIndex
并没有直接使用 evt.oldIndex
,因为 evt.oldIndex
的索引位置是按照容器内支持拖动
的 Element List,而非容器内所有
Element List,当容器内存在部分 Element 不能拖动
时,使用 evt.oldIndex
这个索引顺序
将达不到预期的效果。
排序原理
在容器内使用 SortableJS
进行拖拽排序时会发现,当拖动元素 dragEl
拖动到目标元素 targetEl
上后,dragEl 和 targetEl 的位置互换了,原理是进行到 DOM 操作:
parentNode.insertBefore(dragEl, after ? nextSibling : target);
而这一操作是发生在 dragover
移动时,即没有松开鼠标;若此时想按照通常习惯,按键盘 esc
键想撤销拖拽移动操作,会发现无法还原为拖拽移动之前的位置。