可媲美Element Plus Tree组件研发:节点拖拽(上)

为自研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;
    }
  }

  ...
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java小卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值