背景: 需求需要实现一个勾选列表,支持拖拽排序。由于组件库不支持,加上想顺便学习下
drag API
,于是用原生方法实现了一下。
HTML 拖放 API
HTML 拖放(Drag and
Drop)接口使应用程序能够在浏览器中使用拖放功能。例如,用户可使用鼠标选择可拖拽(draggable)元素,将元素拖拽到可放置(droppable)元素,并释放鼠标按钮以放置这些元素。拖拽操作期间,会有一个可拖拽元素的半透明快照跟随着鼠标指针。
详见MDN文档:HTML 拖放 API
生命周期流程如图:
实现思路
首先拆分组件为列表和每一项列表项。
对于每个列表项,保存三个属性用于渲染
- dragStatus:是否在拖拽中,用于渲染拖拽样式
- dragOver:是否被覆盖,用于渲染被覆盖时的样式
- dragPosition:拖拽方向。释放元素时,位于覆盖元素的上侧还是下侧,用于处理排序逻辑
在对应的生命周期更新属性:
<li
draggable={true}
className={getClass({
[`${baseClassName}-draggable`]: !!draggable,
[`${baseClassName}-${dragStatus}`]: dragStatus !== 'none',
[`${baseClassName}-gap-top`]: dragStatus !== 'dragging' && dragOver && dragPosition < 0,
[`${baseClassName}-gap-bottom`]: dragStatus !== 'dragging' && dragOver && dragPosition > 0,
})}
onDragStart={(e) => {
e.stopPropagation()
setDragStatus('dragging')
}}
onDragEnd={(e) => {
e.stopPropagation()
setDragStatus('none')
}}
onDragOver={(e) => {
e.stopPropagation()
e.preventDefault()
const rect = itemRef.current?.getBoundingClientRect()
const threshold = window.pageYOffset + rect.top + rect.height / 2
const position = e.pageY > threshold ? 1 : -1
setDragOver(true)
setDragPosition(position)
}}
onDrop={(e) => {
e.stopPropagation()
e.preventDefault()
setDragOver(false)
setDragPosition(0)
setDragStatus('none')
if (onDrop) onDrop(e, item, dragPosition)
}}
onDragLeave={(e) => {
e.stopPropagation()
setDragOver(false)
}}
>
{itemContent}
</li>
关于计算拖动释放时排列位置
对于列表,在拖拽开始&结束时记录拖拽元素,在释放时记录释放元素&拖拽方向,并计算得出排序后的数组。
<div>
{dataSource.map((item) => (
<Item
item={item}
onDragStart={(e, item: ITransferItem) => {
setDragItem(item)
}}
onDragEnd={(e, item: ITransferItem) => {
setDragItem(null)
}}
onDrop={(e: DragEvent<HTMLSpanElement>, dropItem: ITransferItem, dropPosition: number) => {
if (dragItem && dragItem.key !== dropItem.key) {
if (onDrop)
onDrop({
e,
dropItem,
dropPosition,
dragItem,
dataSource,
})
}
}}
/>
))}
</div>
关于数组按顺序移动
const moveArrayItem = (arr: any[], fromIndex: number, toIndex: number) => {
const ans = [...arr]
if (fromIndex > toIndex) {
ans.splice(toIndex, 0, ans[fromIndex])
ans.splice(fromIndex + 1, 1)
} else {
ans.splice(toIndex + 1, 0, ans[fromIndex])
ans.splice(fromIndex, 1)
}
return ans
}
第三方库
- react-sortable-hoc
- react-dnd
- react-beautiful-dnd