为自研Tree组件实现节点拖拽功能,实现节点拖拽后的可插入位置识别,下一小节实现节点插入和更新。
目录
用法示例
操作演示:
自研Tree组件功能演示:节点拖拽(上)
ts类型和属性定义
src/components/tree/types.ts
启用拖拽属性定义
export interface OptionProps {
...
draggable?: boolean // 启用拖拽
}
用户可以实现一个Function
的组件属性来自定义哪些节点允许drop
:
// 导出tree组件的属性定义
export const props = {
...
// 允许drop的逻辑判断方法
allowDrop: Function as PropType<AllowDropFunction>,
...
} as const // 注意属性设置为只读的,外面不能修改,同时也避免传空的情况
// 允许drop的函数定义
export declare type AllowDropFunction = (draggingNode: IFlatTreeNode, dropNode: IFlatTreeNode, type: DropType) => boolean
定义drop的操作类型,有效释放是,在一个节点前、后或者位于一个节点内:
// 拖拽释放的操作类型
export declare type DropType = 'before' | 'after' | 'inner'
拖拽事件处理函数定义
// 拖拽事件处理函数定义
export interface DragEvents {
dragStart: (event: DragEvent, node: IFlatTreeNode) => void
dragOver: (event: DragEvent, node: IFlatTreeNode) => void
dragEnd: (event: DragEvent) => void
}
记录拖拽的状态信息
// 记录拖拽过程中的状态信息
export interface DragState {
draggingNode?: IFlatTreeNode // 被拖拽的节点
dropNode?: IFlatTreeNode // 释放到目标节点
allowDrop?: boolean // 是否允许拖动
dropType?: DropType // 释放的类型枚举
}
拖拽模块的核心实现
src/components/tree/use-drag.ts
import { DragEvents, DragState, IdType, IFlatTreeNode, Props } from './types'
import { computed, ComputedRef, Ref, ref } from 'vue'
// 拖拽上下文
export interface UseDragContext {
dragEvents: DragEvents // 封装的拖拽事件处理逻辑
dragState: DragState // 当前拖拽状态信息
dragging: ComputedRef<boolean> // 是否正在拖拽中的布尔计算属性
dragNodeIds: Ref<Array<IdType>> // 拖拽的扁平化节点id列表
}
const calcDragNodeIds = (node: IFlatTreeNode, data: IFlatTreeNode[]) => {
const index = node.index as any
const length = node.originalNode.length || 0
return data.slice(index, index + length + 1).map((n) => n.id)
}
// 导出drag使用模块
export function useDrag(props: Props, data: IFlatTreeNode[]) {
// 初始状态信息
const dragState = ref({
draggingNode: null as any,
dropNode: null as any,
allowDrop: false,
dropType: null as any
} as DragState)
const dragNodeIds = ref([] as IdType[])
return {
dragEvents: {
dragStart: (event, node) => {
// 光标改变
event.dataTransfer!.effectAllowed = 'move'
// 计算拖拽的扁平化节点的id列表
dragNodeIds.value = calcDragNodeIds(node, data)
// 记录被拖拽的节点
dragState.value.draggingNode = node as any
},
dragOver: (event, node) => {
// 经过的节点正好包含在拖拽的节点列表中,则不允许drop
if (dragNodeIds.value.indexOf(node.id) >= 0) {
dragState.value.allowDrop = false
dragState.value.dropNode = null as any
delete dragState.value.dropType
} else {
// 计算在目标节点上移动的垂直距离
const oy = event.offsetY // 0-23
const dropType = oy >= 0 && oy <= 7 ? 'before' : oy > 7 && oy <= 15 ? 'inner' : 'after'
const dragNode = dragState.value.draggingNode
const dragIndex = dragNode!.index
const dropIndex = node.index as any
// 拖拽节点位于原来的位置
const inPlace =
(dropIndex === dragIndex + 1 && dropType === 'before') ||
(dropIndex === dragIndex - 1 && dropType === 'after') ||
dragNode!.parent?.id === node.id // 本来就位于drop节点中
if (!inPlace && (!props.allowDrop || props.allowDrop(dragState.value.draggingNode! as any, node, dropType))) {
event.preventDefault()
dragState.value.dropType = dropType
dragState.value.allowDrop = true
dragState.value.dropNode = node as any
} else {
dragState.value.allowDrop = false
dragState.value.dropNode = null as any
delete dragState.value.dropType
}
}
},
dragEnd: (event) => {
console.log('dragEnd', event)
// todo 触发完成拖拽的节点更新操作,待完善!!!
// 结束拖拽的后置处理
dragNodeIds.value = []
dragState.value.draggingNode = null as any
dragState.value.dropNode = null as any
delete dragState.value.dropType
delete dragState.value.allowDrop
}
} as DragEvents,
dragState,
dragNodeIds,
// 拖拽进行中的布尔计算属性绑定
dragging: computed(() => dragState.value.draggingNode != null)
} as UseDragContext
}
tsx组件完善
src/components/tree/index.tsx
...
import { useDrag, UseDragContext } from '@/components/tree/use-drag'
export default defineComponent({
name: 'JuanTree',
props, // 属性定义
setup(props: Props, { expose, slots }) {
// 实际属性对象
// 属性解构
const { data, optionProps } = props
const { ..., draggable } = optionProps
...
// 使用drag模块
const { dragEvents, dragState, dragging, dragNodeIds } = useDrag(props, originalFlatData) as UseDragContext
const dragStateRef = ref(dragState)
const handleDragStart = (event: DragEvent, node: IFlatTreeNode) => {
dragEvents.dragStart(event, node)
}
const handleDrag = (event: DragEvent, node: IFlatTreeNode) => {
dragEvents.dragOver(event, node)
}
const handleDragEnd = (event: DragEvent) => {
dragEvents.dragEnd(event)
}
...
return () => {
return (
<>
<div class={['juan-tree overflow-hidden', { dragging: dragging.value }]}>
<TransitionGroup ...>
{/* v-for的tsx版本 */}
{expandedTree.value.map((node) => (
<div
key={node.id}
class='juan-tree-node relative flex'
style={{ paddingLeft: `${24 * (node.level - 1)}px` }}
draggable={draggable}
onDragstart={($event) => handleDragStart($event, node as any)}
onDragover={($event) => handleDrag($event, node as any)}
onDragend={($event) => handleDragEnd($event)}
onDragenter={($event) => handleDrag($event, node as any)}
>
...
<div
class={[
'juan-tree-node-content relative inline-flex flex-grow-[1] select-none items-center pl-[2px] hover:bg-slate-100',
{
drag: dragNodeIds.value.indexOf(node.id) >= 0,
[`drop-${dragStateRef.value.dropType}`]: dragStateRef.value.dropNode?.id === node.id
}
]}
...
>
...
</div>
</div>
))}
</TransitionGroup>
</div>
{pageContent()}
</>
)
}
}
})
样式
src/components/tree/style.scss
.juan-tree {
/* 操作部分移上去展示 */
.juan-tree-node-content {
&.drag {
background-color: #f5f7fa !important;
}
&.drop-before::before {
position: absolute;
top: 0;
display: block;
width: 100%;
height: 2px;
content: '';
background-color: chocolate;
}
&.drop-after::after {
position: absolute;
bottom: 0;
display: block;
width: 100%;
height: 2px;
content: '';
background-color: chocolate;
}
&.drop-inner {
background-color: burlywood;
}
...
}
&.dragging {
.juan-tree-node-content:hover {
background-color: transparent;
}
}
...
}