From 6b7bf8ff3b2f9eb1d17e79af3410f8317a21b2ca Mon Sep 17 00:00:00 2001 From: tuchg Date: Wed, 22 Jun 2022 11:18:14 +0800 Subject: [PATCH] feat(comp:modal): add draggable props (#905) feat(cdk:drag-drop): add handle&resetPosition api --- packages/cdk/drag-drop/demo/WithHandle.md | 12 +++ packages/cdk/drag-drop/demo/WithHandle.vue | 17 +++++ packages/cdk/drag-drop/docs/Index.zh.md | 10 +++ .../drag-drop/src/composables/useDraggable.ts | 73 +++++++++++++++++-- .../drag-drop/src/composables/useDroppable.ts | 26 ++++--- .../{useDragFree.ts => withDragFree.ts} | 2 +- .../src/composables/withDragHandle.ts | 32 ++++++++ packages/cdk/drag-drop/src/types.ts | 11 ++- packages/cdk/drag-drop/style/index.less | 2 +- .../components/modal/demo/DraggableModal.md | 14 ++++ .../components/modal/demo/DraggableModal.vue | 14 ++++ packages/components/modal/docs/Index.zh.md | 1 + .../components/modal/src/ModalWrapper.tsx | 38 +++++++++- packages/components/modal/src/types.ts | 1 + 14 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 packages/cdk/drag-drop/demo/WithHandle.md create mode 100644 packages/cdk/drag-drop/demo/WithHandle.vue rename packages/cdk/drag-drop/src/composables/{useDragFree.ts => withDragFree.ts} (89%) create mode 100644 packages/cdk/drag-drop/src/composables/withDragHandle.ts create mode 100644 packages/components/modal/demo/DraggableModal.md create mode 100644 packages/components/modal/demo/DraggableModal.vue diff --git a/packages/cdk/drag-drop/demo/WithHandle.md b/packages/cdk/drag-drop/demo/WithHandle.md new file mode 100644 index 000000000..23a6f10f9 --- /dev/null +++ b/packages/cdk/drag-drop/demo/WithHandle.md @@ -0,0 +1,12 @@ +--- +order: 2 +title: + zh: 通过把手的自由拖拽 + en: handle usage +--- + +## zh + +可通过把手对整体进行拖拽 + +## en diff --git a/packages/cdk/drag-drop/demo/WithHandle.vue b/packages/cdk/drag-drop/demo/WithHandle.vue new file mode 100644 index 000000000..21db0fcbb --- /dev/null +++ b/packages/cdk/drag-drop/demo/WithHandle.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/cdk/drag-drop/docs/Index.zh.md b/packages/cdk/drag-drop/docs/Index.zh.md index ed73db47e..325625479 100644 --- a/packages/cdk/drag-drop/docs/Index.zh.md +++ b/packages/cdk/drag-drop/docs/Index.zh.md @@ -29,6 +29,10 @@ export function useDraggable( dragging: ComputedRef // 当前拖动中的位置信息 position: ComputedRef; + //重置拖拽 + reset:()=>void; + // 取消可拖拽设置 + stop: () => void; } export interface DragPosition { @@ -49,6 +53,8 @@ export interface DraggableOptions { boundary?: BoundaryType // 允许元素自由拖放 free?: boolean + // 拖拽把手 除此元素外的区域将不再触发拖动 + handle?: MaybeElementRef onDragStart?: DnDEvent onDrag?: DnDEvent onDragEnd?: DnDEvent @@ -75,6 +81,10 @@ export function useDroppable( * @param source */ connect: (source: MaybeElementRef) => void + /** + * 取消连接拖拽源和放置目标 + */ + stop:()=>void } export interface DroppableOptions { diff --git a/packages/cdk/drag-drop/src/composables/useDraggable.ts b/packages/cdk/drag-drop/src/composables/useDraggable.ts index 5ae354b0e..548c55505 100644 --- a/packages/cdk/drag-drop/src/composables/useDraggable.ts +++ b/packages/cdk/drag-drop/src/composables/useDraggable.ts @@ -13,11 +13,23 @@ import { ComputedRef, computed, onScopeDispose, toRaw, watch } from 'vue' import { type MaybeElementRef, convertElement, useEventListener } from '@idux/cdk/utils' import { initContext } from '../utils' -import { useDragFree } from './useDragFree' +import { withDragFree } from './withDragFree' +import { withDragHandle } from './withDragHandle' export interface DraggableOptions { + /** + * 作为限制拖拽范围的元素,需自定义droppable时需指定为空 + */ boundary?: BoundaryType + /** + * 指定是否可以拖拽 + */ free?: boolean + /** + * 拖拽把手 + */ + handle?: MaybeElementRef + onDragStart?: DnDEvent onDrag?: DnDEvent onDragEnd?: DnDEvent @@ -39,6 +51,8 @@ export function useDraggable( canDrop: ComputedRef dragging: ComputedRef position: ComputedRef + reset: () => void + stop: () => void } { context = initContext(context) let firstPosition: DragEvent | null = null @@ -60,19 +74,33 @@ export function useDraggable( // free drag-drop if (options?.free) { - useDragFree(source, context!) + withDragFree(source, context!) + } + + // drag-handle + if (options?.handle) { + withDragHandle(source, options.handle, context!) } + installBoundary() + sourceElement.setAttribute('draggable', 'true') - sourceElement.classList.add('cdk-draggable') + + !options?.handle && sourceElement.classList.add('cdk-draggable') } const offDraggable = (sourceElement: HTMLElement) => { context!.registry.off(sourceElement, 'source') sourceElement.setAttribute('draggable', 'false') - sourceElement.classList.remove('cdk-draggable') + + if (options?.handle) { + convertElement(options.handle)?.classList.remove('cdk-draggable-handle') + } else { + sourceElement.classList.remove('cdk-draggable') + } } + const installBoundary = () => { // avoid repeated install listeners const boundaryElement = getBoundaryElement.value @@ -90,10 +118,12 @@ export function useDraggable( context!.registry.exec(source, 'source', 'dragstart', [evt]) options?.onDragStart?.(evt, toRaw(context!.state.currPosition.value)) } + const onDrag = (evt: DragEvent) => { context!.registry.exec(source, 'source', 'drag', [evt]) options?.onDrag?.(evt, toRaw(context!.state.currPosition.value)) } + const onDragEnd = (evt: DragEvent) => { const diffOffset = diff(firstPosition || evt, evt) // sync status @@ -105,13 +135,24 @@ export function useDraggable( context!.registry.exec(source, 'source', 'dragend', [evt]) options?.onDragEnd?.(evt, toRaw(context!.state.currPosition.value)) } + const diff = (oldT: DragEvent, newT: DragEvent) => { return { // fix scroll offset bug + // TODO: the calc way has scale problem offsetLeft: newT.pageX - oldT.pageX, offsetTop: newT.pageY - oldT.pageY, } } + + const onPointerDown = (evt: MouseEvent) => { + context!.registry.exec(source, 'source', 'pointerdown', [evt as DragEvent]) + } + + const onPointerUp = (evt: MouseEvent) => { + context!.registry.exec(source, 'source', 'pointerup', [evt as DragEvent]) + } + const stopWatch = watch( [() => convertElement(source), () => options?.free, () => options?.boundary, () => context], ([currSourceEl], [prevSourceEl]) => { @@ -124,20 +165,36 @@ export function useDraggable( }, ) + const reset = () => { + firstPosition = null + if (options?.free) { + convertElement(source)!.style.transform = '' + } + } + + const { stop: stopDragStart } = useEventListener(source, 'dragstart', onDragStart) + const { stop: stopDrag } = useEventListener(source, 'drag', onDrag) + const { stop: stopDragEnd } = useEventListener(source, 'dragend', onDragEnd) + const { stop: stopPointerDown } = useEventListener(source, 'pointerdown', onPointerDown) + const { stop: stopPointerUp } = useEventListener(source, 'pointerup', onPointerUp) + const stop = () => { offDraggable(convertElement(source)!) stopWatch() + stopDragStart() + stopDrag() + stopDragEnd() + stopPointerDown() + stopPointerUp() } - useEventListener(source, 'dragstart', onDragStart) - useEventListener(source, 'drag', onDrag) - useEventListener(source, 'dragend', onDragEnd) - onScopeDispose(stop) return { canDrop: computed(() => context!.state.canDrop.value), dragging: computed(() => context!.state.isDragging.value), position: computed(() => context!.state.currPosition.value), + reset, + stop, } } diff --git a/packages/cdk/drag-drop/src/composables/useDroppable.ts b/packages/cdk/drag-drop/src/composables/useDroppable.ts index c49394f3b..cf50cfc79 100644 --- a/packages/cdk/drag-drop/src/composables/useDroppable.ts +++ b/packages/cdk/drag-drop/src/composables/useDroppable.ts @@ -34,6 +34,7 @@ export function useDroppable( context?: DnDContext, ): { connect: (source: MaybeElementRef) => void + stop: () => void } { context = initContext(context) @@ -77,12 +78,6 @@ export function useDroppable( { immediate: true, flush: 'post' }, ) - const stop = () => { - offDroppable(convertElement(target)!) - stopWatch() - stopConnectWatch() - } - const onDragEnter = (evt: DragEvent) => { context?.registry.exec(target, 'target', 'dragenter', [evt]) options?.onDragEnter?.(evt, toRaw(context!.state.currPosition.value)) @@ -103,14 +98,25 @@ export function useDroppable( options?.onDrop?.(evt, toRaw(context!.state.currPosition.value)) } - useEventListener(target, 'dragenter', onDragEnter) - useEventListener(target, 'dragover', onDragOver) - useEventListener(target, 'dragleave', onDragLeave) - useEventListener(target, 'drop', onDrop) + const { stop: stopDragEnter } = useEventListener(target, 'dragenter', onDragEnter) + const { stop: stopDragOver } = useEventListener(target, 'dragover', onDragOver) + const { stop: stopDragLeave } = useEventListener(target, 'dragleave', onDragLeave) + const { stop: stopDrop } = useEventListener(target, 'drop', onDrop) + + const stop = () => { + offDroppable(convertElement(target)!) + stopWatch() + stopConnectWatch() + stopDragEnter() + stopDragOver() + stopDragLeave() + stopDrop() + } onScopeDispose(stop) return { connect, + stop, } } diff --git a/packages/cdk/drag-drop/src/composables/useDragFree.ts b/packages/cdk/drag-drop/src/composables/withDragFree.ts similarity index 89% rename from packages/cdk/drag-drop/src/composables/useDragFree.ts rename to packages/cdk/drag-drop/src/composables/withDragFree.ts index 89a0593ec..99c8ac46d 100644 --- a/packages/cdk/drag-drop/src/composables/useDragFree.ts +++ b/packages/cdk/drag-drop/src/composables/withDragFree.ts @@ -9,7 +9,7 @@ import { type MaybeElementRef, convertElement } from '@idux/cdk/utils' import { type DnDContext } from './useDragDropContext' -export function useDragFree(target: MaybeElementRef, context: DnDContext): void { +export function withDragFree(target: MaybeElementRef, context: DnDContext): void { const sourceElement = convertElement(target)! context.registry.on(sourceElement, 'source', 'dragend', (evt: DragEvent) => { diff --git a/packages/cdk/drag-drop/src/composables/withDragHandle.ts b/packages/cdk/drag-drop/src/composables/withDragHandle.ts new file mode 100644 index 000000000..68841a5f0 --- /dev/null +++ b/packages/cdk/drag-drop/src/composables/withDragHandle.ts @@ -0,0 +1,32 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { DnDContext } from './useDragDropContext' + +import { MaybeElementRef, convertElement } from '@idux/cdk/utils' + +export const withDragHandle = (source: MaybeElementRef, handle: MaybeElementRef, context: DnDContext): void => { + let dragTarget: HTMLElement | null = null + const sourceEl = convertElement(source)! + const handleEl = convertElement(handle)! + + handleEl.classList.add('cdk-draggable-handle') + + context.registry.on(sourceEl, 'source', 'pointerdown', e => { + dragTarget = e.target as HTMLElement + }) + + context.registry.on(sourceEl, 'source', 'pointerup', _ => { + dragTarget = null + }) + + context.registry.on(sourceEl, 'source', 'dragstart', e => { + if (!handleEl.contains(dragTarget)) { + e.preventDefault() + } + }) +} diff --git a/packages/cdk/drag-drop/src/types.ts b/packages/cdk/drag-drop/src/types.ts index 124539ff9..6ed8d0f07 100644 --- a/packages/cdk/drag-drop/src/types.ts +++ b/packages/cdk/drag-drop/src/types.ts @@ -19,7 +19,16 @@ export interface DragPosition { export type DnDEvent = (evt: DragEvent, position?: DragPosition) => void export type DnDElement = HTMLElement | Window | EventTarget export type DnDElementType = 'source' | 'target' -export type DnDEventName = 'drag' | 'dragstart' | 'dragend' | 'dragenter' | 'dragover' | 'dragleave' | 'drop' +export type DnDEventName = + | 'drag' + | 'dragstart' + | 'dragend' + | 'dragenter' + | 'dragover' + | 'dragleave' + | 'drop' + | 'pointerdown' + | 'pointerup' export type BoundaryType = 'parent' | 'window' | Window | MaybeElementRef | null export interface DnDState { diff --git a/packages/cdk/drag-drop/style/index.less b/packages/cdk/drag-drop/style/index.less index 8928819ae..f4e359695 100644 --- a/packages/cdk/drag-drop/style/index.less +++ b/packages/cdk/drag-drop/style/index.less @@ -1,6 +1,6 @@ .cdk-draggable { - &[draggable] { + &-handle,&[draggable] { cursor: move; > * { diff --git a/packages/components/modal/demo/DraggableModal.md b/packages/components/modal/demo/DraggableModal.md new file mode 100644 index 000000000..0c6d96cad --- /dev/null +++ b/packages/components/modal/demo/DraggableModal.md @@ -0,0 +1,14 @@ +--- +title: + zh: 可拖拽的对话框 + en: Quickly create +order: 8 +--- + +## zh + +启用`draggable`属性,以支持对话框的自由拖放 + +## en + +enable `draggable` attribute, support draggable dialog diff --git a/packages/components/modal/demo/DraggableModal.vue b/packages/components/modal/demo/DraggableModal.vue new file mode 100644 index 000000000..67e302e3a --- /dev/null +++ b/packages/components/modal/demo/DraggableModal.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/components/modal/docs/Index.zh.md b/packages/components/modal/docs/Index.zh.md index 9be8b54ff..fb001f26d 100644 --- a/packages/components/modal/docs/Index.zh.md +++ b/packages/components/modal/docs/Index.zh.md @@ -22,6 +22,7 @@ order: 0 | `closeIcon` | 自定义关闭图标 | `string \| VNode \| #closeIcon='{onClose}'` | `close` | ✅ | - | | `closeOnEsc` | 是否支持键盘 `esc` 关闭 | `boolean` | `true` | ✅ | - | | `destroyOnHide` | 关闭时销毁子元素 | `boolean` | `false` | - | - | +| `draggable` | 是否支持拖放 | `boolean` | `false` | - | - | | `footer` | 自定义底部按钮 | `boolean \| ModalButtonProps[] \| VNode \| #footer` | `true` | - | 默认会根据 `type` 的不同渲染相应的按钮,如果传入 `false` 则不显示 | | `header` | 对话框标题 | `string \| HeaderProps \| #header={closable, closeIcon, onClose}` | - | - | - | | `icon` | 自定义图标 | `string \| VNode \| #icon` | - | ✅ | 当 `type` 不为 `default` 时有效 | diff --git a/packages/components/modal/src/ModalWrapper.tsx b/packages/components/modal/src/ModalWrapper.tsx index 35af4817e..a5ba7748d 100644 --- a/packages/components/modal/src/ModalWrapper.tsx +++ b/packages/components/modal/src/ModalWrapper.tsx @@ -14,12 +14,14 @@ import { inject, onBeforeUnmount, onMounted, + onUnmounted, ref, watch, } from 'vue' import { isFunction, isString } from 'lodash-es' +import { useDraggable } from '@idux/cdk/drag-drop' import { callEmit, convertCssPixel, getOffset } from '@idux/cdk/utils' import { ɵFooter } from '@idux/components/_private/footer' import { ɵHeader, type ɵHeaderProps } from '@idux/components/_private/header' @@ -46,7 +48,10 @@ export default defineComponent({ okLoading, } = inject(modalToken)! const { close, cancel, ok } = inject(MODAL_TOKEN)! - const { centered, closable, closeIcon, closeOnEsc, width, mask, maskClosable, zIndex } = useConfig(props, config) + const { centered, closable, closeIcon, closeOnEsc, draggable, width, mask, maskClosable, zIndex } = useConfig( + props, + config, + ) const cancelVisible = computed(() => props.type === 'default' || props.type === 'confirm') @@ -85,6 +90,7 @@ export default defineComponent({ }) const wrapperRef = ref() + const headerRef = ref() const modalRef = ref() const sentinelStartRef = ref() const sentinelEndRef = ref() @@ -94,6 +100,7 @@ export default defineComponent({ mask, maskClosable, closeOnEsc, + draggable, sentinelStartRef, sentinelEndRef, ) @@ -105,8 +112,25 @@ export default defineComponent({ animatedVisible, modalTransformOrigin, ) + const resetDraggableRef = ref<() => void>(() => {}) - onMounted(() => watchVisibleChange(props, wrapperRef, sentinelStartRef, mask)) + onMounted(() => watchVisibleChange(props, wrapperRef, sentinelStartRef, mask, resetDraggableRef)) + + const stopWatch = watch( + () => draggable, + draggable => { + if (draggable) { + const { reset } = useDraggable(wrapperRef, { handle: headerRef, free: true }) + resetDraggableRef.value = reset + } + }, + { immediate: true }, + ) + + onUnmounted(() => { + stopWatch() + resetDraggableRef.value = () => {} + }) return () => { const prefixCls = mergedPrefixCls.value @@ -151,6 +175,7 @@ export default defineComponent({
<ɵHeader + ref={headerRef} v-slots={slots} closable={closable.value} closeIcon={closeIcon.value} @@ -187,12 +212,13 @@ function useConfig(props: ModalProps, config: ModalConfig) { const closable = computed(() => props.closable ?? config.closable) const closeIcon = computed(() => props.closeIcon ?? config.closeIcon) const closeOnEsc = computed(() => props.closeOnEsc ?? config.closeOnEsc) + const draggable = computed(() => props.draggable) const mask = computed(() => props.mask ?? config.mask) const maskClosable = computed(() => props.maskClosable ?? config.maskClosable) const width = computed(() => convertCssPixel(props.width ?? config.width)) const zIndex = computed(() => props.zIndex ?? config.zIndex) - return { centered, closable, closeIcon, closeOnEsc, width, mask, maskClosable, zIndex } + return { centered, closable, closeIcon, closeOnEsc, draggable, width, mask, maskClosable, zIndex } } function watchVisibleChange( @@ -200,8 +226,10 @@ function watchVisibleChange( wrapperRef: Ref, sentinelStartRef: Ref, mask: ComputedRef, + resetDraggableRef: Ref<() => void>, ) { let lastOutSideActiveElement: HTMLElement | null = null + watch( () => props.visible, visible => { @@ -212,6 +240,7 @@ function watchVisibleChange( lastOutSideActiveElement = activeElement as HTMLElement sentinelStartRef.value?.focus() } + resetDraggableRef.value?.() } else { if (mask.value) { lastOutSideActiveElement?.focus?.() @@ -228,6 +257,7 @@ function useEvent( mask: ComputedRef, maskClosable: ComputedRef, closeOnEsc: ComputedRef, + draggable: ComputedRef, sentinelStartRef: Ref, sentinelEndRef: Ref, ) { @@ -242,7 +272,7 @@ function useEvent( } const onWrapperClick = (evt: MouseEvent) => { - if (evt.target === evt.currentTarget && !mouseDown && mask.value && maskClosable.value) { + if (evt.target === evt.currentTarget && (!mouseDown || draggable.value) && mask.value && maskClosable.value) { close(evt) } } diff --git a/packages/components/modal/src/types.ts b/packages/components/modal/src/types.ts index 5e8b96425..9b334ec70 100644 --- a/packages/components/modal/src/types.ts +++ b/packages/components/modal/src/types.ts @@ -54,6 +54,7 @@ export const modalProps = { width: IxPropTypes.oneOfType([String, Number]), wrapperClassName: IxPropTypes.string, zIndex: IxPropTypes.number, + draggable: { type: Boolean, default: false }, // events 'onUpdate:visible': IxPropTypes.emit<(visible: boolean) => void>(),