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>>,
onDragend: [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>>,
// eslint-disable-next-line vue/prop-name-casing
'onUpdate:expandedKeys': [Function, Array] as PropType<
@ -257,13 +258,18 @@ export default defineComponent({
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 highlightKeysRef = 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'), () => {
loadingKeysRef.value = []
expandTimerIdRef.value = undefined
@ -420,14 +426,17 @@ export default defineComponent({
const { onDragstart } = props
if (onDragstart) call(onDragstart, info)
}
function doDragOver (info: DragInfo): void {
const { onDragover } = props
if (onDragover) call(onDragover, info)
}
function doDrop (info: DropInfo): void {
const { onDrop } = props
if (onDrop) call(onDrop, info)
}
function resetDragStatus (): void {
draggingNodeKeyRef.value = null
draggingNodeRef.value = null
droppingNodeKeyRef.value = null
droppingNodeRef.value = null
}
function handleCheck (node: TmNode, checked: boolean): void {
if (props.disabled || node.disabled) return
@ -482,18 +491,22 @@ export default defineComponent({
}
}
}
// Dnd
function handleDragEnter ({ event, node }: InternalDragInfo): void {
// node should be a tmNode
if (!props.draggable || props.disabled || node.disabled) return
doDragEnter({ event, node: node.rawNode })
if (!props.expandOnDragenter) return
droppingNodeKeyRef.value = node.key
if (node.key === draggingNodeKeyRef.value) return
droppingNodeRef.value = node
const { value: draggingNode } = draggingNodeRef
if (draggingNode && node.key === draggingNode.key) return
if (!mergedExpandedKeysRef.value.includes(node.key) && !node.isLeaf) {
window.clearTimeout(expandTimerIdRef.value)
const expand = (): void => {
const { value: droppingNode } = droppingNodeRef
if (
droppingNodeKeyRef.value === node.key &&
droppingNode &&
droppingNode.key === node.key &&
!mergedExpandedKeysRef.value.includes(node.key)
) {
doExpandedKeysChange(mergedExpandedKeysRef.value.concat(node.key))
@ -522,7 +535,7 @@ export default defineComponent({
}
function handleDragLeave ({ event, node }: InternalDragInfo): void {
if (!props.draggable || props.disabled || node.disabled) return
droppingNodeKeyRef.value = null
droppingNodeRef.value = null
doDragLeave({ event, node: node.rawNode })
}
function handleDragEnd ({ event, node }: InternalDragInfo): void {
@ -532,10 +545,12 @@ export default defineComponent({
}
function handleDragStart ({ event, node }: InternalDragInfo): void {
if (!props.draggable || props.disabled || node.disabled) return
draggingNodeKeyRef.value = node.key
draggingNodeRef.value = node.rawNode
draggingNodeRef.value = node
doDragStart({ event, node: node.rawNode })
}
function handleDragOver ({ event, node }: InternalDragInfo): void {
doDragOver({ event, node: node.rawNode })
}
function handleDrop ({ event, node, dropPosition }: InternalDropInfo): void {
if (
!props.draggable ||
@ -548,7 +563,7 @@ export default defineComponent({
doDrop({
event,
node: node.rawNode,
dragNode: draggingNodeRef.value,
dragNode: draggingNodeRef.value.rawNode,
dropPosition
})
resetDragStatus()
@ -571,12 +586,17 @@ export default defineComponent({
onLoadRef: toRef(props, 'onLoad'),
draggableRef: toRef(props, 'draggable'),
checkableRef: toRef(props, 'checkable'),
blockLineRef: toRef(props, 'blockLine'),
droppingNodeRef,
droppingNodeParentRef,
draggingNodeRef,
handleSwitcherClick,
handleDragEnd,
handleDragEnter,
handleDragLeave,
handleDragStart,
handleDrop,
handleDragOver,
handleSelect,
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 NTreeNodeSwitcher from './TreeNodeSwitcher'
import NTreeNodeCheckbox from './TreeNodeCheckbox'
@ -45,20 +45,47 @@ const TreeNode = defineComponent({
function handleContentClick (e: MouseEvent): void {
NTree.handleSelect(props.tmNode)
}
function handleDragEnter (e: DragEvent): void {
NTree.handleDragEnter({
event: e,
node: props.tmNode
})
function handleCheck (checked: boolean): void {
NTree.handleCheck(props.tmNode, checked)
}
// Dnd
const pendingPositionRef = ref<'top' | 'center' | 'bottom' | null>(null)
function handleDragStart (e: DragEvent): void {
NTree.handleDragStart({
event: e,
node: props.tmNode
})
}
function handleDragLeave (e: DragEvent): void {
NTree.handleDragLeave({
function handleDragEnter (e: DragEvent): void {
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,
node: props.tmNode
})
@ -69,18 +96,35 @@ const TreeNode = defineComponent({
node: props.tmNode
})
}
function handleDrop (
e: DragEvent,
dropPosition: 'bottom' | 'center' | 'top'
): void {
NTree.handleDrop({
function handleDragLeave (e: DragEvent): void {
if (
e.currentTarget &&
e.relatedTarget &&
(e.currentTarget as HTMLElement).contains(
e.relatedTarget as HTMLElement
)
) {
return
}
NTree.handleDragLeave({
event: e,
node: props.tmNode,
dropPosition
node: props.tmNode
})
}
function handleCheck (checked: boolean): void {
NTree.handleCheck(props.tmNode, checked)
function handleDrop (e: DragEvent): void {
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 {
loading: useMemo(() =>
@ -104,10 +148,13 @@ const TreeNode = defineComponent({
icon: computed(() => props.tmNode.rawNode.icon),
checkable: NTree.checkableRef,
draggable: NTree.draggableRef,
blockLine: NTree.blockLineRef,
pendingPosition: pendingPositionRef,
handleCheck,
handleDrop,
handleDragStart,
handleDragEnter,
handleDragOver,
handleDragEnd,
handleDragLeave,
handleContentClick,
@ -115,51 +162,75 @@ const TreeNode = defineComponent({
}
},
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 (
<li
class={[
`${clsPrefix}-tree-node`,
{
[`${clsPrefix}-tree-node--selected`]: selected,
[`${clsPrefix}-tree-node--checkable`]: checkable,
[`${clsPrefix}-tree-node--hightlight`]: highlight
}
]}
<div
class={`${clsPrefix}-tree-node-wrapper`}
{...(blockLine ? dragEventHandlers : undefined)}
>
{Array.apply(null, { length: tmNode.level } as any).map(() => (
<div class={`${clsPrefix}-tree-node-indent`}></div>
))}
<NTreeNodeSwitcher
clsPrefix={clsPrefix}
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}
onDragenter={this.handleDragEnter}
onDragstart={this.handleDragStart}
onDragleave={this.handleDragLeave}
onDragend={this.handleDragEnd}
onDrop={this.handleDrop}
<div
class={[
`${clsPrefix}-tree-node`,
{
[`${clsPrefix}-tree-node--selected`]: selected,
[`${clsPrefix}-tree-node--checkable`]: checkable,
[`${clsPrefix}-tree-node--hightlight`]: highlight
}
]}
draggable={draggable && blockLine}
onDragstart={
draggable && blockLine ? this.handleDragStart : undefined
}
>
{{
default: () => tmNode.rawNode.label
}}
</NTreeNodeContent>
{this.icon ? this.icon() : null}
</li>
{Array.apply(null, { length: tmNode.level } as any).map(() => (
<div class={`${clsPrefix}-tree-node-indent`}></div>
))}
<NTreeNodeSwitcher
clsPrefix={clsPrefix}
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
},
onClick: Function as PropType<(e: MouseEvent) => 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
>
onDragstart: Function as PropType<(e: DragEvent) => void>
},
setup (props) {
const pendingRef = ref(false)
const pendingPositionRef = ref<'top' | 'center' | 'bottom' | null>(null)
const selfRef = ref<HTMLElement | null>(null)
function doClick (e: MouseEvent): void {
const { onClick } = props
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 {
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 {
selfRef,
pending: pendingRef,
pendingPosition: pendingPositionRef,
handleContentDragLeave,
handleContentDragStart,
handleDragOverContent,
handleContentDragEnd,
handleContentDragEnter,
handleContentDrop,
handleClick
}
},
render () {
const {
clsPrefix,
pending,
pendingPosition,
handleContentDragLeave,
handleContentDragStart,
handleDragOverContent,
handleContentDragEnd,
handleContentDragEnter,
handleContentDrop,
handleClick
} = this
const { clsPrefix, handleClick, onDragstart } = this
return (
<span
ref="selfRef"
class={[
`${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}
class={[`${clsPrefix}-tree-node-content`]}
onClick={handleClick}
draggable={!!onDragstart}
onDragstart={onDragstart}
>
<div class={`${clsPrefix}-tree-node-content__padding-box`} />
<div class={`${clsPrefix}-tree-node-content__text`}>{this.$slots}</div>

View File

@ -50,6 +50,10 @@ export interface TreeInjection {
checkableRef: Ref<boolean>
mergedThemeRef: Ref<MergedTheme<TreeTheme>>
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
handleSelect: (node: TreeNode<TreeOption>) => void
handleCheck: (node: TreeNode<TreeOption>, checked: boolean) => void
@ -57,6 +61,7 @@ export interface TreeInjection {
handleDragEnter: (info: InternalDragInfo) => void
handleDragLeave: (info: InternalDragInfo) => void
handleDragEnd: (info: InternalDragInfo) => void
handleDragOver: (info: InternalDragInfo) => 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', {
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:
@ -67,8 +53,8 @@ export default cB('tree', {
})
])
]),
cB('tree-node-wrapper', 'padding: 3px 0;'),
cB('tree-node', `
margin: 6px 0 0 0;
display: flex;
border-radius: var(--node-border-radius);
transition: background-color .3s var(--bezier);
@ -196,20 +182,6 @@ export default cB('tree', {
}),
c('&:active', {
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)'
})
])
})
])
])