wip(tree): refactor drag and drop

This commit is contained in:
07akioni 2021-05-22 10:49:12 +08:00
parent 0a1b65f2a7
commit cf561be670
6 changed files with 196 additions and 235 deletions

View File

@ -132,6 +132,7 @@ const treeProps = {
onDragleave: [Function, Array] as PropType<MaybeArray<(e: DragInfo) => void>>, onDragleave: [Function, Array] as PropType<MaybeArray<(e: DragInfo) => void>>,
onDragend: [Function, Array] as PropType<MaybeArray<(e: DragInfo) => void>>, onDragend: [Function, Array] as PropType<MaybeArray<(e: DragInfo) => void>>,
onDragstart: [Function, Array] as PropType<MaybeArray<(e: DragInfo) => void>>, onDragstart: [Function, Array] as PropType<MaybeArray<(e: DragInfo) => void>>,
onDragover: [Function, Array] as PropType<MaybeArray<(e: DragInfo) => void>>,
onDrop: [Function, Array] as PropType<MaybeArray<(e: DragInfo) => void>>, onDrop: [Function, Array] as PropType<MaybeArray<(e: DragInfo) => void>>,
// eslint-disable-next-line vue/prop-name-casing // eslint-disable-next-line vue/prop-name-casing
'onUpdate:expandedKeys': [Function, Array] as PropType< 'onUpdate:expandedKeys': [Function, Array] as PropType<
@ -257,13 +258,18 @@ export default defineComponent({
treeMateRef.value.getFlattenedNodes(mergedExpandedKeysRef.value) treeMateRef.value.getFlattenedNodes(mergedExpandedKeysRef.value)
) )
const draggingNodeKeyRef = ref<Key | null>(null)
const draggingNodeRef = ref<TreeOption | null>(null)
const droppingNodeKeyRef = ref<Key | null>(null)
const expandTimerIdRef = ref<number | undefined>(undefined) const expandTimerIdRef = ref<number | undefined>(undefined)
const highlightKeysRef = ref<Key[]>([]) const highlightKeysRef = ref<Key[]>([])
const loadingKeysRef = ref<Key[]>([]) const loadingKeysRef = ref<Key[]>([])
const draggingNodeRef = ref<TmNode | null>(null)
const droppingNodeRef = ref<TmNode | null>(null)
const droppingNodeParentRef = computed(() => {
const { value: droppingNode } = droppingNodeRef
if (droppingNode) return droppingNode.parent
return null
})
watch(toRef(props, 'data'), () => { watch(toRef(props, 'data'), () => {
loadingKeysRef.value = [] loadingKeysRef.value = []
expandTimerIdRef.value = undefined expandTimerIdRef.value = undefined
@ -420,14 +426,17 @@ export default defineComponent({
const { onDragstart } = props const { onDragstart } = props
if (onDragstart) call(onDragstart, info) if (onDragstart) call(onDragstart, info)
} }
function doDragOver (info: DragInfo): void {
const { onDragover } = props
if (onDragover) call(onDragover, info)
}
function doDrop (info: DropInfo): void { function doDrop (info: DropInfo): void {
const { onDrop } = props const { onDrop } = props
if (onDrop) call(onDrop, info) if (onDrop) call(onDrop, info)
} }
function resetDragStatus (): void { function resetDragStatus (): void {
draggingNodeKeyRef.value = null
draggingNodeRef.value = null draggingNodeRef.value = null
droppingNodeKeyRef.value = null droppingNodeRef.value = null
} }
function handleCheck (node: TmNode, checked: boolean): void { function handleCheck (node: TmNode, checked: boolean): void {
if (props.disabled || node.disabled) return if (props.disabled || node.disabled) return
@ -482,18 +491,22 @@ export default defineComponent({
} }
} }
} }
// Dnd
function handleDragEnter ({ event, node }: InternalDragInfo): void { function handleDragEnter ({ event, node }: InternalDragInfo): void {
// node should be a tmNode // node should be a tmNode
if (!props.draggable || props.disabled || node.disabled) return if (!props.draggable || props.disabled || node.disabled) return
doDragEnter({ event, node: node.rawNode }) doDragEnter({ event, node: node.rawNode })
if (!props.expandOnDragenter) return if (!props.expandOnDragenter) return
droppingNodeKeyRef.value = node.key droppingNodeRef.value = node
if (node.key === draggingNodeKeyRef.value) return const { value: draggingNode } = draggingNodeRef
if (draggingNode && node.key === draggingNode.key) return
if (!mergedExpandedKeysRef.value.includes(node.key) && !node.isLeaf) { if (!mergedExpandedKeysRef.value.includes(node.key) && !node.isLeaf) {
window.clearTimeout(expandTimerIdRef.value) window.clearTimeout(expandTimerIdRef.value)
const expand = (): void => { const expand = (): void => {
const { value: droppingNode } = droppingNodeRef
if ( if (
droppingNodeKeyRef.value === node.key && droppingNode &&
droppingNode.key === node.key &&
!mergedExpandedKeysRef.value.includes(node.key) !mergedExpandedKeysRef.value.includes(node.key)
) { ) {
doExpandedKeysChange(mergedExpandedKeysRef.value.concat(node.key)) doExpandedKeysChange(mergedExpandedKeysRef.value.concat(node.key))
@ -522,7 +535,7 @@ export default defineComponent({
} }
function handleDragLeave ({ event, node }: InternalDragInfo): void { function handleDragLeave ({ event, node }: InternalDragInfo): void {
if (!props.draggable || props.disabled || node.disabled) return if (!props.draggable || props.disabled || node.disabled) return
droppingNodeKeyRef.value = null droppingNodeRef.value = null
doDragLeave({ event, node: node.rawNode }) doDragLeave({ event, node: node.rawNode })
} }
function handleDragEnd ({ event, node }: InternalDragInfo): void { function handleDragEnd ({ event, node }: InternalDragInfo): void {
@ -532,10 +545,12 @@ export default defineComponent({
} }
function handleDragStart ({ event, node }: InternalDragInfo): void { function handleDragStart ({ event, node }: InternalDragInfo): void {
if (!props.draggable || props.disabled || node.disabled) return if (!props.draggable || props.disabled || node.disabled) return
draggingNodeKeyRef.value = node.key draggingNodeRef.value = node
draggingNodeRef.value = node.rawNode
doDragStart({ event, node: node.rawNode }) doDragStart({ event, node: node.rawNode })
} }
function handleDragOver ({ event, node }: InternalDragInfo): void {
doDragOver({ event, node: node.rawNode })
}
function handleDrop ({ event, node, dropPosition }: InternalDropInfo): void { function handleDrop ({ event, node, dropPosition }: InternalDropInfo): void {
if ( if (
!props.draggable || !props.draggable ||
@ -548,7 +563,7 @@ export default defineComponent({
doDrop({ doDrop({
event, event,
node: node.rawNode, node: node.rawNode,
dragNode: draggingNodeRef.value, dragNode: draggingNodeRef.value.rawNode,
dropPosition dropPosition
}) })
resetDragStatus() resetDragStatus()
@ -571,12 +586,17 @@ export default defineComponent({
onLoadRef: toRef(props, 'onLoad'), onLoadRef: toRef(props, 'onLoad'),
draggableRef: toRef(props, 'draggable'), draggableRef: toRef(props, 'draggable'),
checkableRef: toRef(props, 'checkable'), checkableRef: toRef(props, 'checkable'),
blockLineRef: toRef(props, 'blockLine'),
droppingNodeRef,
droppingNodeParentRef,
draggingNodeRef,
handleSwitcherClick, handleSwitcherClick,
handleDragEnd, handleDragEnd,
handleDragEnter, handleDragEnter,
handleDragLeave, handleDragLeave,
handleDragStart, handleDragStart,
handleDrop, handleDrop,
handleDragOver,
handleSelect, handleSelect,
handleCheck handleCheck
}) })

View File

@ -1,4 +1,4 @@
import { h, inject, computed, defineComponent, PropType } from 'vue' import { h, inject, computed, defineComponent, PropType, ref } from 'vue'
import { useMemo } from 'vooks' import { useMemo } from 'vooks'
import NTreeNodeSwitcher from './TreeNodeSwitcher' import NTreeNodeSwitcher from './TreeNodeSwitcher'
import NTreeNodeCheckbox from './TreeNodeCheckbox' import NTreeNodeCheckbox from './TreeNodeCheckbox'
@ -45,20 +45,47 @@ const TreeNode = defineComponent({
function handleContentClick (e: MouseEvent): void { function handleContentClick (e: MouseEvent): void {
NTree.handleSelect(props.tmNode) NTree.handleSelect(props.tmNode)
} }
function handleDragEnter (e: DragEvent): void {
NTree.handleDragEnter({ function handleCheck (checked: boolean): void {
event: e, NTree.handleCheck(props.tmNode, checked)
node: props.tmNode
})
} }
// Dnd
const pendingPositionRef = ref<'top' | 'center' | 'bottom' | null>(null)
function handleDragStart (e: DragEvent): void { function handleDragStart (e: DragEvent): void {
NTree.handleDragStart({ NTree.handleDragStart({
event: e, event: e,
node: props.tmNode node: props.tmNode
}) })
} }
function handleDragLeave (e: DragEvent): void { function handleDragEnter (e: DragEvent): void {
NTree.handleDragLeave({ if (
e.currentTarget &&
e.relatedTarget &&
(e.currentTarget as HTMLElement).contains(
e.relatedTarget as HTMLElement
)
) {
return
}
NTree.handleDragEnter({
event: e,
node: props.tmNode
})
}
function handleDragOver (e: DragEvent): void {
e.preventDefault()
const el = e.currentTarget as HTMLElement
const elOffsetHeight = el.offsetHeight // dangerous
const elClientTop = el.getBoundingClientRect().top
const eventOffsetY = e.clientY - elClientTop
if (eventOffsetY <= 8) {
pendingPositionRef.value = 'top'
} else if (eventOffsetY >= elOffsetHeight - 8) {
pendingPositionRef.value = 'bottom'
} else {
pendingPositionRef.value = 'center'
}
NTree.handleDragOver({
event: e, event: e,
node: props.tmNode node: props.tmNode
}) })
@ -69,18 +96,35 @@ const TreeNode = defineComponent({
node: props.tmNode node: props.tmNode
}) })
} }
function handleDrop ( function handleDragLeave (e: DragEvent): void {
e: DragEvent, if (
dropPosition: 'bottom' | 'center' | 'top' e.currentTarget &&
): void { e.relatedTarget &&
NTree.handleDrop({ (e.currentTarget as HTMLElement).contains(
e.relatedTarget as HTMLElement
)
) {
return
}
NTree.handleDragLeave({
event: e, event: e,
node: props.tmNode, node: props.tmNode
dropPosition
}) })
} }
function handleCheck (checked: boolean): void { function handleDrop (e: DragEvent): void {
NTree.handleCheck(props.tmNode, checked) e.preventDefault()
if (pendingPositionRef.value !== null) {
const dropPosition = ({
top: 'top',
bottom: 'bottom',
center: 'center'
} as const)[pendingPositionRef.value]
NTree.handleDrop({
event: e,
node: props.tmNode,
dropPosition
})
}
} }
return { return {
loading: useMemo(() => loading: useMemo(() =>
@ -104,10 +148,13 @@ const TreeNode = defineComponent({
icon: computed(() => props.tmNode.rawNode.icon), icon: computed(() => props.tmNode.rawNode.icon),
checkable: NTree.checkableRef, checkable: NTree.checkableRef,
draggable: NTree.draggableRef, draggable: NTree.draggableRef,
blockLine: NTree.blockLineRef,
pendingPosition: pendingPositionRef,
handleCheck, handleCheck,
handleDrop, handleDrop,
handleDragStart, handleDragStart,
handleDragEnter, handleDragEnter,
handleDragOver,
handleDragEnd, handleDragEnd,
handleDragLeave, handleDragLeave,
handleContentClick, handleContentClick,
@ -115,51 +162,75 @@ const TreeNode = defineComponent({
} }
}, },
render () { render () {
const { tmNode, clsPrefix, checkable, selected, highlight } = this const {
tmNode,
clsPrefix,
checkable,
selected,
highlight,
draggable,
blockLine
} = this
// drag start not inside
// it need to be append to node itself, not wrapper
const dragEventHandlers = draggable
? {
onDragenter: this.handleDragEnter,
onDragleave: this.handleDragLeave,
onDragend: this.handleDragEnd,
onDrop: this.handleDrop
}
: undefined
return ( return (
<li <div
class={[ class={`${clsPrefix}-tree-node-wrapper`}
`${clsPrefix}-tree-node`, {...(blockLine ? dragEventHandlers : undefined)}
{
[`${clsPrefix}-tree-node--selected`]: selected,
[`${clsPrefix}-tree-node--checkable`]: checkable,
[`${clsPrefix}-tree-node--hightlight`]: highlight
}
]}
> >
{Array.apply(null, { length: tmNode.level } as any).map(() => ( <div
<div class={`${clsPrefix}-tree-node-indent`}></div> class={[
))} `${clsPrefix}-tree-node`,
<NTreeNodeSwitcher {
clsPrefix={clsPrefix} [`${clsPrefix}-tree-node--selected`]: selected,
expanded={this.expanded} [`${clsPrefix}-tree-node--checkable`]: checkable,
loading={this.loading} [`${clsPrefix}-tree-node--hightlight`]: highlight
hide={tmNode.isLeaf} }
onClick={this.handleSwitcherClick} ]}
/> draggable={draggable && blockLine}
{checkable ? ( onDragstart={
<NTreeNodeCheckbox draggable && blockLine ? this.handleDragStart : undefined
clsPrefix={clsPrefix} }
checked={this.checked}
indeterminate={this.indeterminate}
onCheck={this.handleCheck}
/>
) : null}
<NTreeNodeContent
clsPrefix={clsPrefix}
onClick={this.handleContentClick}
onDragenter={this.handleDragEnter}
onDragstart={this.handleDragStart}
onDragleave={this.handleDragLeave}
onDragend={this.handleDragEnd}
onDrop={this.handleDrop}
> >
{{ {Array.apply(null, { length: tmNode.level } as any).map(() => (
default: () => tmNode.rawNode.label <div class={`${clsPrefix}-tree-node-indent`}></div>
}} ))}
</NTreeNodeContent> <NTreeNodeSwitcher
{this.icon ? this.icon() : null} clsPrefix={clsPrefix}
</li> expanded={this.expanded}
loading={this.loading}
hide={tmNode.isLeaf}
onClick={this.handleSwitcherClick}
/>
{checkable ? (
<NTreeNodeCheckbox
clsPrefix={clsPrefix}
checked={this.checked}
indeterminate={this.indeterminate}
onCheck={this.handleCheck}
/>
) : null}
<NTreeNodeContent
clsPrefix={clsPrefix}
onClick={this.handleContentClick}
onDragstart={this.handleDragStart}
{...(!blockLine ? dragEventHandlers : undefined)}
>
{{
default: () => tmNode.rawNode.label
}}
</NTreeNodeContent>
{this.icon ? this.icon() : null}
</div>
</div>
) )
} }
}) })

View File

@ -12,159 +12,31 @@ export default defineComponent({
default: false default: false
}, },
onClick: Function as PropType<(e: MouseEvent) => void>, onClick: Function as PropType<(e: MouseEvent) => void>,
onDragstart: Function as PropType<(e: DragEvent) => void>, onDragstart: Function as PropType<(e: DragEvent) => void>
onDragend: Function as PropType<(e: DragEvent) => void>,
onDragenter: Function as PropType<(e: DragEvent) => void>,
onDragover: Function as PropType<(e: DragEvent) => void>,
onDragleave: Function as PropType<(e: DragEvent) => void>,
onDrop: Function as PropType<
(e: DragEvent, dropPosition: 'bottom' | 'center' | 'top') => void
>
}, },
setup (props) { setup (props) {
const pendingRef = ref(false)
const pendingPositionRef = ref<'top' | 'center' | 'bottom' | null>(null)
const selfRef = ref<HTMLElement | null>(null) const selfRef = ref<HTMLElement | null>(null)
function doClick (e: MouseEvent): void { function doClick (e: MouseEvent): void {
const { onClick } = props const { onClick } = props
if (onClick) onClick(e) if (onClick) onClick(e)
} }
function doDragStart (e: DragEvent): void {
const { onDragstart } = props
if (onDragstart) onDragstart(e)
}
function doDragEnter (e: DragEvent): void {
const { onDragenter } = props
if (onDragenter) onDragenter(e)
}
function doDragEnd (e: DragEvent): void {
const { onDragend } = props
if (onDragend) onDragend(e)
}
function doDragLeave (e: DragEvent): void {
const { onDragleave } = props
if (onDragleave) onDragleave(e)
}
// function doDragOver (e: DragEvent) {
// const { onDragOver } = props
// if (onDragOver) onDragOver(e)
// }
function doDrop (
e: DragEvent,
dropPosition: 'top' | 'bottom' | 'center'
): void {
const { onDrop } = props
if (onDrop) onDrop(e, dropPosition)
}
function handleClick (e: MouseEvent): void { function handleClick (e: MouseEvent): void {
doClick(e) doClick(e)
} }
function handleContentDragStart (e: DragEvent): void {
doDragStart(e)
}
function handleContentDragEnter (e: DragEvent): void {
if (
e.currentTarget &&
e.relatedTarget &&
(e.currentTarget as HTMLElement).contains(
e.relatedTarget as HTMLElement
)
) {
return
}
doDragEnter(e)
}
function handleDragOverContent (e: DragEvent): void {
e.preventDefault()
const el = selfRef.value as HTMLElement
pendingRef.value = true
const elOffsetHeight = el.offsetHeight
const elClientTop = el.getBoundingClientRect().top
const eventOffsetY = e.clientY - elClientTop
if (eventOffsetY <= 8) {
pendingPositionRef.value = 'top'
} else if (eventOffsetY >= elOffsetHeight - 8) {
pendingPositionRef.value = 'bottom'
} else {
pendingPositionRef.value = 'center'
}
}
function handleContentDragEnd (e: DragEvent): void {
doDragEnd(e)
}
function handleContentDragLeave (e: DragEvent): void {
if (
e.currentTarget &&
e.relatedTarget &&
(e.currentTarget as HTMLElement).contains(
e.relatedTarget as HTMLElement
)
) {
return
}
pendingRef.value = false
doDragLeave(e)
}
function handleContentDrop (e: DragEvent): void {
e.preventDefault()
pendingRef.value = false
if (pendingPositionRef.value !== null) {
const dropPosition = {
top: 'top',
bottom: 'bottom',
center: 'center'
}[pendingPositionRef.value]
doDrop(e, dropPosition as 'top' | 'bottom' | 'center')
}
}
return { return {
selfRef, selfRef,
pending: pendingRef,
pendingPosition: pendingPositionRef,
handleContentDragLeave,
handleContentDragStart,
handleDragOverContent,
handleContentDragEnd,
handleContentDragEnter,
handleContentDrop,
handleClick handleClick
} }
}, },
render () { render () {
const { const { clsPrefix, handleClick, onDragstart } = this
clsPrefix,
pending,
pendingPosition,
handleContentDragLeave,
handleContentDragStart,
handleDragOverContent,
handleContentDragEnd,
handleContentDragEnter,
handleContentDrop,
handleClick
} = this
return ( return (
<span <span
ref="selfRef" ref="selfRef"
class={[ class={[`${clsPrefix}-tree-node-content`]}
`${clsPrefix}-tree-node-content`,
{
[`${clsPrefix}-tree-node-content--pending`]: pending,
[`${clsPrefix}-tree-node-content--pending-bottom`]:
pendingPosition === 'bottom',
[`${clsPrefix}-tree-node-content--pending-body`]:
pendingPosition === 'center',
[`${clsPrefix}-tree-node-content--pending-top`]:
pendingPosition === 'top'
}
]}
onDragleave={handleContentDragLeave}
onDragstart={handleContentDragStart}
onDragover={handleDragOverContent}
onDragend={handleContentDragEnd}
onDragenter={handleContentDragEnter}
onDrop={handleContentDrop}
onClick={handleClick} onClick={handleClick}
draggable={!!onDragstart}
onDragstart={onDragstart}
> >
<div class={`${clsPrefix}-tree-node-content__padding-box`} /> <div class={`${clsPrefix}-tree-node-content__padding-box`} />
<div class={`${clsPrefix}-tree-node-content__text`}>{this.$slots}</div> <div class={`${clsPrefix}-tree-node-content__text`}>{this.$slots}</div>

View File

@ -50,6 +50,10 @@ export interface TreeInjection {
checkableRef: Ref<boolean> checkableRef: Ref<boolean>
mergedThemeRef: Ref<MergedTheme<TreeTheme>> mergedThemeRef: Ref<MergedTheme<TreeTheme>>
onLoadRef: Ref<((node: TreeOption) => Promise<void>) | undefined> onLoadRef: Ref<((node: TreeOption) => Promise<void>) | undefined>
blockLineRef: Ref<boolean>
draggingNodeRef: Ref<TmNode | null>
droppingNodeRef: Ref<TmNode | null>
droppingNodeParentRef: Ref<TmNode | null>
handleSwitcherClick: (node: TreeNode<TreeOption>) => void handleSwitcherClick: (node: TreeNode<TreeOption>) => void
handleSelect: (node: TreeNode<TreeOption>) => void handleSelect: (node: TreeNode<TreeOption>) => void
handleCheck: (node: TreeNode<TreeOption>, checked: boolean) => void handleCheck: (node: TreeNode<TreeOption>, checked: boolean) => void
@ -57,6 +61,7 @@ export interface TreeInjection {
handleDragEnter: (info: InternalDragInfo) => void handleDragEnter: (info: InternalDragInfo) => void
handleDragLeave: (info: InternalDragInfo) => void handleDragLeave: (info: InternalDragInfo) => void
handleDragEnd: (info: InternalDragInfo) => void handleDragEnd: (info: InternalDragInfo) => void
handleDragOver: (info: InternalDragInfo) => void
handleDrop: (info: InternalDropInfo) => void handleDrop: (info: InternalDropInfo) => void
} }

View File

@ -0,0 +1,21 @@
import { CSSProperties, h, VNode } from 'vue'
export function renderDropMark (position: 'top' | 'center' | 'bottom'): VNode {
const style: CSSProperties = {
position: 'absolute',
boxSizing: 'border-box',
right: 0,
left: 0
}
if (position === 'center') {
style.top = 0
style.bottom = 0
style.borderRadius = 'inherit'
style.border = '2px solid black'
} else {
style[position] = 0
style.height = '2px'
style.backgroundColor = 'black'
}
return <div style={style}></div>
}

View File

@ -8,21 +8,7 @@ const nodeStateStyle = [
}), }),
c('&:active', { c('&:active', {
backgroundColor: 'var(--node-color-pressed)' backgroundColor: 'var(--node-color-pressed)'
}), })
cM('pending', [
c('&:hover', {
backgroundColor: '#0000'
}),
cM('pending-bottom', {
borderBottom: '3px solid var(--node-color-hover)'
}),
cM('pending-top', {
borderTop: '3px solid var(--node-color-hover)'
}),
cM('pending-body', {
backgroundColor: 'var(--node-color-hover)'
})
])
] ]
// vars: // vars:
@ -67,8 +53,8 @@ export default cB('tree', {
}) })
]) ])
]), ]),
cB('tree-node-wrapper', 'padding: 3px 0;'),
cB('tree-node', ` cB('tree-node', `
margin: 6px 0 0 0;
display: flex; display: flex;
border-radius: var(--node-border-radius); border-radius: var(--node-border-radius);
transition: background-color .3s var(--bezier); transition: background-color .3s var(--bezier);
@ -196,20 +182,6 @@ export default cB('tree', {
}), }),
c('&:active', { c('&:active', {
backgroundColor: 'var(--node-color-pressed)' backgroundColor: 'var(--node-color-pressed)'
}), })
cM('pending', [
c('&:hover', {
backgroundColor: '#0000'
}),
cM('pending-bottom', {
borderBottom: '3px solid var(--node-color-hover)'
}),
cM('pending-top', {
borderTop: '3px solid var(--node-color-hover)'
}),
cM('pending-body', {
backgroundColor: 'var(--node-color-hover)'
})
])
]) ])
]) ])