Skip to content

Commit

Permalink
feat(cdk:dnd): add support for controlled offset prop (#1994)
Browse files Browse the repository at this point in the history
  • Loading branch information
sallerli1 authored Oct 15, 2024
1 parent 003a180 commit 5e982e5
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 30 deletions.
10 changes: 9 additions & 1 deletion packages/cdk/dnd/demo/Movable.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<template>
<IxSpace vertical>
<div class="dnd-movable-wrapper">
<CdkDndMovable class="dnd-movable-item" :mode="mode" :strategy="strategy" :allowedAxis="allowedAxis">
<CdkDndMovable
v-model:offset="offset"
class="dnd-movable-item"
:mode="mode"
:strategy="strategy"
:allowedAxis="allowedAxis"
>
<CdkDndMovableHandle v-if="useHandle" class="dnd-movable-item__handle">
<IxIcon name="holder" />
</CdkDndMovableHandle>
Expand Down Expand Up @@ -35,6 +41,8 @@ import type { RadioData } from '@idux/components/radio'
import { ref } from 'vue'
const offset = ref({ x: 0, y: 0 })
const strategy = ref<DndMovableStrategy>('transform')
const mode = ref<DndMovableMode>('afterDrop')
const allowedAxis = ref<Axis>('all')
Expand Down
4 changes: 4 additions & 0 deletions packages/cdk/dnd/docs/Api.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ interface DndSortableReorderInfo {

| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 |
| --- | --- | --- | --- | --- | --- |
| `v-model:offset` | 偏移量 | `{ x: number, y: number }` | - | 通过该属性可受控控制移动的偏移量 |
| `tag` | 自定义组件根节点 | `string \| Component \| FunctionalComponent` | `'div'` | - | - |
| `allowedAxis` | 可以拖拽移动的方向 | `'horizontal' \| 'vertical' \| 'all'` | `'all'` | - | - |
| `mode` | 拖拽移动模式 | `'immediate' \| 'afterDrop'` | `'afterDrop'` | - | `'immediate'` 模式下,会随着拖拽实时改变元素位置,`'afterDrop'`则会在拖拽结束改变 |
Expand All @@ -138,6 +139,7 @@ interface DndSortableReorderInfo {
| `onDragLeave` | 目标拖拽离开回调函数 | `((args: ElementDropTargetEventBasePayload) => void) \| ((args: ElementDropTargetEventBasePayload) => void)[]` | - | - |
| `onDrop` | 拖拽结束回调函数 | `((args: DndSortableOnDropArgs) => void) \| ((args: DndSortableOnDropArgs) => void)[]` | - | - |
| `onDropOfTarget` | 目标元素拖拽放入回调函数 | `((args: ElementDropTargetEventBasePayload) => void) \| ((args: ElementDropTargetEventBasePayload) => void)[]` | - | - |
| `onOffsetChange` | 偏移量改变回调函数 | `(newOffset: Position, oldOffset: Position) => void` | - | - |

#### DndMovableSlots

Expand Down Expand Up @@ -250,6 +252,7 @@ type DndSortablePreviewOptions =
function useDndMovable(options: DndMovableOptions): DndMovableContext

interface DndMovableOptions extends Omit<DndOptions, 'monitor'> {
offset?: Ref<Position | undefined>
mode?: MaybeRef<DndMovableMode | undefined>
strategy?: MaybeRef<DndMovableStrategy | undefined>
canDrag?: MaybeRef<boolean | undefined>
Expand All @@ -259,6 +262,7 @@ interface DndMovableOptions extends Omit<DndOptions, 'monitor'> {
dragHandle?: MaybeElementRef
allowedAxis?: MaybeRef<Axis>
preview?: MaybeRef<DndMovablePreviewOptions>
onOffsetChange?: (newOffset: Position, oldOffset: Position) => void
}

interface DndMovableContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { useDndContext } from '../useDndContext'

export interface DndMovableContext {
init: () => void
position: ComputedRef<Position>
offset: ComputedRef<Position>
position: ComputedRef<Position | undefined>
offset: ComputedRef<Position | undefined>
}

export function useDndMovable(options: DndMovableOptions): DndMovableContext {
Expand All @@ -38,9 +38,11 @@ export function useDndMovable(options: DndMovableOptions): DndMovableContext {
boundary,
dragHandle,
preview,
offset: optionOffset,
onDragStart,
onDrag,
onDrop,
onOffsetChange,
...rest
} = useResolvedOptions(options)

Expand All @@ -51,7 +53,7 @@ export function useDndMovable(options: DndMovableOptions): DndMovableContext {
start,
end,
update,
} = useMovablePosition(draggableElement, strategy, boundary, allowedAxis)
} = useMovablePosition(draggableElement, strategy, boundary, allowedAxis, optionOffset, onOffsetChange)

let currentTarget: Element | null = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import type { Axis, DndMovableStrategy, Position, ResolvedBoundary } from '../../types'

import { type ComputedRef, watch } from 'vue'
import { type ComputedRef, computed, toRaw, watch } from 'vue'

import { useState } from '@idux/cdk/utils'

Expand All @@ -18,25 +18,74 @@ export function useMovablePosition(
strategy: ComputedRef<DndMovableStrategy>,
boundary: ComputedRef<ResolvedBoundary | undefined>,
allowedAxis: ComputedRef<Axis>,
optionOffset: ComputedRef<Position | undefined>,
onOffsetChange?: (newOffset: Position, oldOffset: Position) => void,
): {
position: ComputedRef<Position>
offset: ComputedRef<Position>
position: ComputedRef<Position | undefined>
offset: ComputedRef<Position | undefined>
init: (reset?: boolean) => void
start: (initial: Position) => void
end: () => void
update: (current: Position) => void
} {
const [position, setPosition] = useState<Position>({ x: 0, y: 0 })
const [offset, setOffset] = useState<Position>({ x: 0, y: 0 })
const [initalPosition, setInitialPosition] = useState<Position | undefined>(undefined)
const [initialOffset, setInitialOffset] = useState<Position | undefined>(undefined)

const [moveOffset, setMoveOffset] = useState<Position>({ x: 0, y: 0 })

watch(optionOffset, offset => {
if (offset) {
setMoveOffset(offset)
}
})

const updateMoveOffset = (newOffset: Position) => {
const oldOffset = toRaw(moveOffset.value)

onOffsetChange?.(newOffset, oldOffset)
setMoveOffset(newOffset)
}

const getMoveOffset = () => {
return optionOffset.value ?? moveOffset.value
}

const position = computed<Position | undefined>(() => {
const _initialPosition = initalPosition.value
if (!_initialPosition) {
return
}

const _moveOffset = getMoveOffset()

return {
x: _initialPosition.x + _moveOffset.x,
y: _initialPosition.y + _moveOffset.y,
}
})
const offset = computed<Position | undefined>(() => {
const _initialOffset = initialOffset.value

if (!_initialOffset) {
return
}

const _moveOffset = getMoveOffset()

return {
x: _initialOffset.x + _moveOffset.x,
y: _initialOffset.y + _moveOffset.y,
}
})

const initPosition = (element: HTMLElement) => {
const _strategy = strategy.value
if (_strategy === 'absolute' || _strategy === 'transform') {
const { offsetTop, offsetLeft } = element
setPosition({ x: offsetLeft, y: offsetTop })
setInitialPosition({ x: offsetLeft, y: offsetTop })
} else {
const { x, y } = element.getBoundingClientRect()
setPosition({ x, y })
setInitialPosition({ x, y })
}
}

Expand All @@ -46,26 +95,32 @@ export function useMovablePosition(
}

if (reset) {
setOffset({ x: 0, y: 0 })
setInitialOffset({ x: 0, y: 0 })
}

const { transform } = getComputedStyle(element)

const offset = getPositionFromMatrix(transform)

if (offset) {
setOffset(offset)
}
setInitialOffset(offset ?? { x: 0, y: 0 })
}

let isDragging = false
let lastDragPosition: Position | undefined

const start = (initial: Position) => {
if (optionOffset.value) {
setMoveOffset({ ...optionOffset.value })
}

lastDragPosition = initial
isDragging = true
}
const end = () => {
if (optionOffset.value) {
setMoveOffset({ ...optionOffset.value })
}

isDragging = false
}

Expand All @@ -91,17 +146,12 @@ export function useMovablePosition(
return
}

const _offset = offset.value
const _position = position.value
const _offset = moveOffset.value

setOffset({
updateMoveOffset({
x: _offset.x + offsetXOfTick,
y: _offset.y + offsetYOfTick,
})
setPosition({
x: _position.x + offsetXOfTick,
y: _position.y + offsetYOfTick,
})

lastDragPosition = current
}
Expand All @@ -115,7 +165,7 @@ export function useMovablePosition(
}
}

watch([elementRef, strategy], () => init())
watch([elementRef, strategy], () => init(), { immediate: true })

return {
position,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import type { DndMovableBoundaryType, DndMovableOptions, ResolvedBoundary, Resol

import { computed, unref } from 'vue'

import { isNil } from 'lodash-es'
import { isFunction, isNil } from 'lodash-es'

import { convertElement, useState } from '@idux/cdk/utils'

import { defaultMovableAllowedAxis, defaultMovableMode, defaultMovableStrategy } from '../../consts'

export function useResolvedOptions(options: DndMovableOptions): ResolvedMovableOptions {
const {
offset: optionOffset,
mode,
strategy,
canDrag,
Expand All @@ -40,6 +41,7 @@ export function useResolvedOptions(options: DndMovableOptions): ResolvedMovableO
}

return {
offset: computed(() => unref(optionOffset)),
mode: computed(() => {
const moveMode = unref(mode)

Expand Down Expand Up @@ -81,6 +83,10 @@ function getBoundary(element: HTMLElement | undefined, boundary: DndMovableBound
return { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight }
}

if (isFunction(_boundary)) {
return _boundary(element)
}

const boundaryElement = _boundary === 'parent' ? element?.parentElement : convertElement(_boundary)

if (!boundaryElement) {
Expand Down
14 changes: 12 additions & 2 deletions packages/cdk/dnd/src/movable/DndMovable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Teleport, computed, defineComponent, provide, shallowRef, toRef } from

import { isNil } from 'lodash-es'

import { callEmit, convertCssPixel, useState } from '@idux/cdk/utils'
import { callEmit, convertCssPixel, useControlledProp, useState } from '@idux/cdk/utils'

import { useDndMovable } from '../composables/useDndMovable'
import { defaultMovableStrategy } from '../consts'
Expand All @@ -21,6 +21,7 @@ export default defineComponent({
props: dndMovableProps,
setup(props, { slots, expose }) {
const [previewState, setPreviewState] = useState<{ container: HTMLElement } | null>(null)
const [offsetProp, setOffsetProp] = useControlledProp(props, 'offset')

const elementRef = shallowRef<HTMLElement>()
const dragHandleRef = shallowRef<HTMLElement>()
Expand Down Expand Up @@ -67,6 +68,7 @@ export default defineComponent({
})

const { init, position, offset } = useDndMovable({
offset: offsetProp,
mode: toRef(props, 'mode'),
strategy: mergedStrategy,
canDrag: toRef(props, 'canDrag'),
Expand Down Expand Up @@ -94,6 +96,10 @@ export default defineComponent({
onDropOfTarget(args) {
callEmit(props.onDropOfTarget, args)
},
onOffsetChange(newOffset, oldOffset) {
callEmit(props.onOffsetChange, newOffset, oldOffset)
setOffsetProp(newOffset)
},
})

expose({
Expand All @@ -104,14 +110,18 @@ export default defineComponent({
const strategy = mergedStrategy.value

if (strategy === 'fixed' || strategy === 'absolute') {
if (!position.value) {
return
}

return {
position: strategy,
top: convertCssPixel(position.value.y),
left: convertCssPixel(position.value.x),
}
}

if (offset.value.x === 0 && offset.value.y === 0) {
if (!offset.value) {
return
}

Expand Down
7 changes: 5 additions & 2 deletions packages/cdk/dnd/src/types/comp/movable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import type { Axis } from '../dnd'
import type { DndMovableMode, DndMovableStrategy } from '../movable'
import type { DndMovableBoundaryType, DndMovableMode, DndMovableStrategy, Position } from '../movable'
import type {
ElementDropTargetEventBasePayload,
ElementEventBasePayload,
Expand All @@ -17,6 +17,7 @@ import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, MaybeEl
import type { Component, DefineComponent, FunctionalComponent, HTMLAttributes, PropType } from 'vue'

export const dndMovableProps = {
offset: Object as PropType<Position>,
tag: { type: [String, Object, Function] as PropType<string | Component | FunctionalComponent>, default: 'div' },
allowedAxis: { type: String as PropType<Axis>, default: 'all' },
mode: { type: String as PropType<DndMovableMode>, default: undefined },
Expand All @@ -28,14 +29,16 @@ export const dndMovableProps = {
canDrag: { type: Boolean, default: true },
dragHandle: { type: Object as PropType<MaybeElement>, default: undefined },
dropTargets: { type: Array as PropType<(HTMLElement | undefined)[]>, default: undefined },
boundary: { type: [String, Object] as PropType<'parent' | 'viewport' | MaybeElement>, default: undefined },
boundary: { type: [String, Object, Function] as PropType<DndMovableBoundaryType>, default: undefined },

'onUpdate:offset': [Function, Array] as PropType<(offset: Position) => void>,
onDragStart: [Function, Array] as PropType<MaybeArray<(args: ElementEventBasePayload) => void>>,
onDrag: [Function, Array] as PropType<MaybeArray<(args: ElementEventBasePayload) => void>>,
onDragEnter: [Function, Array] as PropType<MaybeArray<(args: ElementDropTargetEventBasePayload) => void>>,
onDragLeave: [Function, Array] as PropType<MaybeArray<(args: ElementDropTargetEventBasePayload) => void>>,
onDrop: [Function, Array] as PropType<MaybeArray<(args: ElementEventBasePayload) => void>>,
onDropOfTarget: [Function, Array] as PropType<MaybeArray<(args: ElementDropTargetEventBasePayload) => void>>,
onOffsetChange: [Function, Array] as PropType<MaybeArray<(newOffset: Position, oldOffset: Position) => void>>,
} as const

export interface DndMovableBindings {
Expand Down
Loading

0 comments on commit 5e982e5

Please sign in to comment.