UNPKG

element-plus

Version:

A Component Library for Vue3.0

183 lines (159 loc) 6.05 kB
import mitt, { Emitter } from 'mitt' import { inject, provide, ref } from 'vue' import { addClass, removeClass } from '@element-plus/utils/dom' import Node from './node' interface TreeNode { node: Node $el?: HTMLElement } interface DragOptions { event: DragEvent treeNode: TreeNode } export function useDragNodeHandler({ props, ctx, el$, dropIndicator$, store }) { const emitter = mitt() provide('DragNodeEmitter', emitter) const dragState = ref({ showDropIndicator: false, draggingNode: null, dropNode: null, allowDrop: true, dropType: null, }) emitter.on('tree-node-drag-start', ({ event, treeNode }: DragOptions) => { if (typeof props.allowDrag === 'function' && !props.allowDrag(treeNode.node)) { event.preventDefault() return false } event.dataTransfer.effectAllowed = 'move' // wrap in try catch to address IE's error when first param is 'text/plain' try { // setData is required for draggable to work in FireFox // the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox event.dataTransfer.setData('text/plain', '') } catch (e) {} dragState.value.draggingNode = treeNode ctx.emit('node-drag-start', treeNode.node, event) }) emitter.on('tree-node-drag-over', ({ event, treeNode }: DragOptions) => { const dropNode = treeNode const oldDropNode = dragState.value.dropNode if (oldDropNode && oldDropNode !== dropNode) { removeClass(oldDropNode.$el, 'is-drop-inner') } const draggingNode = dragState.value.draggingNode if (!draggingNode || !dropNode) return let dropPrev = true let dropInner = true let dropNext = true let userAllowDropInner = true if (typeof props.allowDrop === 'function') { dropPrev = props.allowDrop(draggingNode.node, dropNode.node, 'prev') userAllowDropInner = dropInner = props.allowDrop(draggingNode.node, dropNode.node, 'inner') dropNext = props.allowDrop(draggingNode.node, dropNode.node, 'next') } event.dataTransfer.dropEffect = dropInner ? 'move' : 'none' if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) { if (oldDropNode) { ctx.emit('node-drag-leave', draggingNode.node, oldDropNode.node, event) } ctx.emit('node-drag-enter', draggingNode.node, dropNode.node, event) } if (dropPrev || dropInner || dropNext) { dragState.value.dropNode = dropNode } if (dropNode.node.nextSibling === draggingNode.node) { dropNext = false } if (dropNode.node.previousSibling === draggingNode.node) { dropPrev = false } if (dropNode.node.contains(draggingNode.node, false)) { dropInner = false } if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) { dropPrev = false dropInner = false dropNext = false } const targetPosition = dropNode.$el.getBoundingClientRect() const treePosition = el$.value.getBoundingClientRect() let dropType const prevPercent = dropPrev ? (dropInner ? 0.25 : (dropNext ? 0.45 : 1)) : -1 const nextPercent = dropNext ? (dropInner ? 0.75 : (dropPrev ? 0.55 : 0)) : 1 let indicatorTop = -9999 const distance = event.clientY - targetPosition.top if (distance < targetPosition.height * prevPercent) { dropType = 'before' } else if (distance > targetPosition.height * nextPercent) { dropType = 'after' } else if (dropInner) { dropType = 'inner' } else { dropType = 'none' } const iconPosition = dropNode.$el.querySelector('.el-tree-node__expand-icon').getBoundingClientRect() const dropIndicator = dropIndicator$.value if (dropType === 'before') { indicatorTop = iconPosition.top - treePosition.top } else if (dropType === 'after') { indicatorTop = iconPosition.bottom - treePosition.top } dropIndicator.style.top = indicatorTop + 'px' dropIndicator.style.left = (iconPosition.right - treePosition.left) + 'px' if (dropType === 'inner') { addClass(dropNode.$el, 'is-drop-inner') } else { removeClass(dropNode.$el, 'is-drop-inner') } dragState.value.showDropIndicator = dropType === 'before' || dropType === 'after' dragState.value.allowDrop = dragState.value.showDropIndicator || userAllowDropInner dragState.value.dropType = dropType ctx.emit('node-drag-over', draggingNode.node, dropNode.node, event) }) emitter.on('tree-node-drag-end', (event: DragEvent) => { const { draggingNode, dropType, dropNode } = dragState.value event.preventDefault() event.dataTransfer.dropEffect = 'move' if (draggingNode && dropNode) { const draggingNodeCopy = { data: draggingNode.node.data } if (dropType !== 'none') { draggingNode.node.remove() } if (dropType === 'before') { dropNode.node.parent.insertBefore(draggingNodeCopy, dropNode.node) } else if (dropType === 'after') { dropNode.node.parent.insertAfter(draggingNodeCopy, dropNode.node) } else if (dropType === 'inner') { dropNode.node.insertChild(draggingNodeCopy) } if (dropType !== 'none') { store.value.registerNode(draggingNodeCopy) } removeClass(dropNode.$el, 'is-drop-inner') ctx.emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event) if (dropType !== 'none') { ctx.emit('node-drop', draggingNode.node, dropNode.node, dropType, event) } } if (draggingNode && !dropNode) { ctx.emit('node-drag-end', draggingNode.node, null, dropType, event) } dragState.value.showDropIndicator = false dragState.value.draggingNode = null dragState.value.dropNode = null dragState.value.allowDrop = true }) return { dragState, } } interface DragNodeEmitter { emitter: Emitter } export function useDragNodeEmitter(): DragNodeEmitter { const emitter = inject<Emitter>('DragNodeEmitter') return { emitter, } }