diff --git a/packages/cdk/dnd/__tests__/dnd.spec.ts b/packages/cdk/dnd/__tests__/dnd.spec.ts new file mode 100644 index 000000000..871db5b14 --- /dev/null +++ b/packages/cdk/dnd/__tests__/dnd.spec.ts @@ -0,0 +1,3 @@ +describe.skip('useDnd.ts', () => { + test('init test', () => {}) +}) diff --git a/packages/cdk/dnd/demo/ListSortable.md b/packages/cdk/dnd/demo/ListSortable.md new file mode 100644 index 000000000..9e215889d --- /dev/null +++ b/packages/cdk/dnd/demo/ListSortable.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh: 列表拖拽排序 + en: List dnd sortable +--- + +## zh + +使用 `CdkDndSortable` 实现列表拖拽排序。 + +## en + +Impliment list sortting by drag-and-drop with `CdkDndSortable`. diff --git a/packages/cdk/dnd/demo/ListSortable.vue b/packages/cdk/dnd/demo/ListSortable.vue new file mode 100644 index 000000000..e1d20fd95 --- /dev/null +++ b/packages/cdk/dnd/demo/ListSortable.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/packages/cdk/dnd/demo/Movable.md b/packages/cdk/dnd/demo/Movable.md new file mode 100644 index 000000000..ec14080cb --- /dev/null +++ b/packages/cdk/dnd/demo/Movable.md @@ -0,0 +1,14 @@ +--- +order: 40 +title: + zh: 拖拽移动 + en: Drag move +--- + +## zh + +使用 `CdkDndMovable` 实现元素拖拽移动。 + +## en + +Impliment element dragging with `CdkDndMovable`. diff --git a/packages/cdk/dnd/demo/Movable.vue b/packages/cdk/dnd/demo/Movable.vue new file mode 100644 index 000000000..4feabb81d --- /dev/null +++ b/packages/cdk/dnd/demo/Movable.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/packages/cdk/dnd/demo/MovableWithBoundary.md b/packages/cdk/dnd/demo/MovableWithBoundary.md new file mode 100644 index 000000000..e7f51d0da --- /dev/null +++ b/packages/cdk/dnd/demo/MovableWithBoundary.md @@ -0,0 +1,14 @@ +--- +order: 50 +title: + zh: 拖拽移动(设置边界) + en: Drag move with boundary +--- + +## zh + +配置 `CdkDndMovable` 的 `boundary` 属性设置拖拽移动边界。 + +## en + +Configure dragging move boundary by setting `boundary` prop of `CdkDndMovable`. diff --git a/packages/cdk/dnd/demo/MovableWithBoundary.vue b/packages/cdk/dnd/demo/MovableWithBoundary.vue new file mode 100644 index 000000000..a74b62752 --- /dev/null +++ b/packages/cdk/dnd/demo/MovableWithBoundary.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/packages/cdk/dnd/demo/MovableWithTargets.md b/packages/cdk/dnd/demo/MovableWithTargets.md new file mode 100644 index 000000000..084aeb97c --- /dev/null +++ b/packages/cdk/dnd/demo/MovableWithTargets.md @@ -0,0 +1,18 @@ +--- +order: 60 +title: + zh: 拖拽移动(设置拖放目标) + en: Drag move with targets +--- + +## zh + +配置 `CdkDndMovable` 的 `dropTargets` 属性设置拖拽移动的目标元素。 + +仅支持 `mode` 为 `afterDrop` 的方式。 + +## en + +Configure dragging move targets by setting `dropTargets` prop of `CdkDndMovable`. + +Only supported under `afterDrop` mode. diff --git a/packages/cdk/dnd/demo/MovableWithTargets.vue b/packages/cdk/dnd/demo/MovableWithTargets.vue new file mode 100644 index 000000000..636810356 --- /dev/null +++ b/packages/cdk/dnd/demo/MovableWithTargets.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/cdk/dnd/demo/SortablePreview.md b/packages/cdk/dnd/demo/SortablePreview.md new file mode 100644 index 000000000..348dae35d --- /dev/null +++ b/packages/cdk/dnd/demo/SortablePreview.md @@ -0,0 +1,14 @@ +--- +order: 20 +title: + zh: 拖拽预览 + en: Drag preview +--- + +## zh + +设置拖拽元素的预览。 + +## en + +Set preview of item being dragged. diff --git a/packages/cdk/dnd/demo/SortablePreview.vue b/packages/cdk/dnd/demo/SortablePreview.vue new file mode 100644 index 000000000..2478294e0 --- /dev/null +++ b/packages/cdk/dnd/demo/SortablePreview.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/packages/cdk/dnd/demo/SortableWithHandle.md b/packages/cdk/dnd/demo/SortableWithHandle.md new file mode 100644 index 000000000..d203706ba --- /dev/null +++ b/packages/cdk/dnd/demo/SortableWithHandle.md @@ -0,0 +1,14 @@ +--- +order: 30 +title: + zh: 拖拽排序 handle + en: Dnd sortable handle +--- + +## zh + +使用 `CdkDndSortableHandle` 来自定义拖拽的 handle。 + +## en + +Customize drag-and-drop handle with `CdkDndSortableHandle`. diff --git a/packages/cdk/dnd/demo/SortableWithHandle.vue b/packages/cdk/dnd/demo/SortableWithHandle.vue new file mode 100644 index 000000000..62c6685d3 --- /dev/null +++ b/packages/cdk/dnd/demo/SortableWithHandle.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/packages/cdk/dnd/demo/TreeSortable.md b/packages/cdk/dnd/demo/TreeSortable.md new file mode 100644 index 000000000..b8a64bdd7 --- /dev/null +++ b/packages/cdk/dnd/demo/TreeSortable.md @@ -0,0 +1,14 @@ +--- +order: 10 +title: + zh: 树拖拽排序 + en: Tree dnd sortable +--- + +## zh + +使用 `CdkDndSortable` 实现树拖拽排序。 + +## en + +Impliment tree sortting by drag-and-drop with `CdkDndSortable`. diff --git a/packages/cdk/dnd/demo/TreeSortable.vue b/packages/cdk/dnd/demo/TreeSortable.vue new file mode 100644 index 000000000..247b3726f --- /dev/null +++ b/packages/cdk/dnd/demo/TreeSortable.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/packages/cdk/dnd/docs/Api.en.md b/packages/cdk/dnd/docs/Api.en.md new file mode 100644 index 000000000..cd52eb216 --- /dev/null +++ b/packages/cdk/dnd/docs/Api.en.md @@ -0,0 +1,19 @@ +### IxDnd + +#### DndProps + +| Name | Description | Type | Default | Global Config | Remark | +| --- | --- | --- | --- | --- | --- | +| - | - | - | - | ✅ | - | + +#### DndSlots + +| Name | Description | Parameter Type | Remark | +| --- | --- | --- | --- | +| - | - | - | - | + +#### DndMethods + +| Name | Description | Parameter Type | Remark | +| --- | --- | --- | --- | +| - | - | - | - | diff --git a/packages/cdk/dnd/docs/Api.zh.md b/packages/cdk/dnd/docs/Api.zh.md new file mode 100644 index 000000000..23019b270 --- /dev/null +++ b/packages/cdk/dnd/docs/Api.zh.md @@ -0,0 +1,299 @@ +## 组件 + +### CdkDndSortable + +拖拽排序 + +#### DndSortableProps + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `tag` | 自定义组件根节点 | `string \| Component \| FunctionalComponent` | - | - | - | +| `strategy` | 拖拽排序策略 | `DndSortableStrategy` | `'list'` | - | - | +| `dataSource` | 拖拽排序数据 | `any[]` | - | - | - | +| `direction` | 拖拽排序列表的方向 | `DndSortableDirection` | `'vertical'` | - | - | +| `preview` | 拖拽元素的预览 | `boolean \| 'native' \| { offset: { x: number; y: number } }` | - | - | `'native'` 为原生的拖拽预览,配置 `true` 可以搭配 `#preview` 插槽进行自定义 | +| `getKey` | 数据项的唯一标识 | `string \| (item) => VKey` | `key` | - | - | +| `childrenKey` | 树形数据的子数据key | `string` | `children` | - | - | +| `treeIndent` | 树的层级间缩进 | `number` | `32` | - | +| `isSticky` | 是否是 `sticky` | `boolean \| ((options: DndSortableIsStickyOptions) => boolean)` | `32` | 配置了 `sticky` 后,即使拖拽离开所有的元素,还是会停留在最后一个拖入的元素 | +| `isTreeItemExpanded` | 树的某个节点是否被展开的判断函数 | `(key: VKey, data: DndSortableData) => void` | - | - | +| `canDrag` | 是否可以拖拽 | `boolean \| ((options: CanDragOptions) => boolean)` | - | - | +| `canDrop` | 是否可以拖拽放置 | `boolean \| ((options: CanDropOptions) => boolean)` | - | - | +| `onDragStart` | 开始拖拽的回调函数 | `((args: DndSortableOnDragStartArgs) => void) \| ((args: DndSortableOnDragStartArgs) => void)[]` | - | - | +| `onDrag` | 拖拽回调函数 | `((args: DndSortableOnDragArgs) => void) \| ((args: DndSortableOnDragArgs) => void)[]` | - | - | +| `onDragEnter` | 目标拖拽进入回调函数 | `((args: DndSortableOnDragEnterArgs) => void) \| ((args: DndSortableOnDragEnterArgs) => void)[]` | - | - | +| `onDragLeave` | 目标拖拽离开回调函数 | `((args: DndSortableOnDragLeaveArgs) => void) \| ((args: DndSortableOnDragLeaveArgs) => void)[]` | - | - | +| `onDrop` | 目标结束回调函数 | `((args: DndSortableOnDropArgs) => void) \| ((args: DndSortableOnDropArgs) => void)[]` | - | - | +| `onSortReorder` | 重新排序的回调函数 | `((reorderInfo: DndSortableReorderInfo) \| ((reorderInfo: DndSortableReorderInfo)[]` | - | - | +| `onSortChange` | 排序后数据变化的回调函数 | `((newDataSource: any[], oldDataSource: any[]) => void) \| ((newDataSource: any[], oldDataSource: any[]) => void)[]` | - | - | + +```typescript +type DndSortableStrategy = 'list' | 'tree' +type DndSortableDirection = 'vertical' | 'horizontal' + +interface CanDragOptions { + sourceKey: VKey + sourceIndex: number | undefined + sourceData: DndSortableData | undefined +} +interface CanDropOptions extends Omit { + sourceKey: VKey | undefined + targetKey: VKey + targetIndex: number | undefined + targetData: DndSortableData | undefined +} + +interface DndSortableTransferData { + key: VKey + listData: DndSortableInnerData + listDataIndex: number + direction?: DndSortableDirection + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string | symbol]: any +} + +interface BaseDndSortableEventArgs { + location: DragLocationHistory + key: VKey + data: DndSortableData +} +interface DndSortableEvetWithSourceArgs extends BaseDndSortableEventArgs { + sourceKey: VKey + sourceData: DndSortableData +} +interface DndSortableOnDragArgs extends BaseDndSortableEventArgs {} +interface DndSortableOnDragStartArgs extends BaseDndSortableEventArgs {} +interface DndSortableOnDragEnterArgs extends DndSortableEvetWithSourceArgs {} +interface DndSortableOnDragLeaveArgs extends DndSortableEvetWithSourceArgs {} +interface DndSortableOnDropArgs extends DndSortableEvetWithSourceArgs {} + +interface DndSortableReorderInfo { + sourceIndex: number + targetIndex: number + sourceKey: VKey + targetKey: VKey + sourceData: DndSortableData + targetData: DndSortableData + operation: 'insertBefore' | 'insertAfter' | 'insertChild' +} +``` + +#### DndSortableSlots + +| 名称 | 说明 | 参数类型 | 备注 | +| --- | --- | --- | --- | +| `default` | 默认插槽 | - | - | +| `preview` | 预览 | `{ key: VKey data: DndSortableData \| undefined index: number \| undefined container: HTMLElement }` | - | + +### CdkDndSortableItem + +用来绑定拖拽排序的内部元素 + +#### DndSortableItemProps + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `itemKey` | 对应的数据key | `string \| number \| symbol` | - | - | - | +| `direction` | 可以独立设置某个元素的方向 | `DndSortableDirection` | - | - | - | +| `isSticky` | 是否是 `sticky` | `boolean \| ((options: DndSortableIsStickyOptions) => boolean)` | `32` | 配置了 `sticky` 后,即使拖拽离开所有的元素,还是会停留在最后一个拖入的元素 | +| `canDrag` | 是否可以拖拽 | `boolean \| ((options: CanDragOptions) => boolean)` | - | - | +| `canDrop` | 是否可以拖拽放置 | `boolean \| ((options: CanDropOptions) => boolean)` | - | - | +| `onDragStart` | 开始拖拽的回调函数 | `((args: DndSortableOnDragStartArgs) => void) \| ((args: DndSortableOnDragStartArgs) => void)[]` | - | - | +| `onDrag` | 拖拽回调函数 | `((args: DndSortableOnDragArgs) => void) \| ((args: DndSortableOnDragArgs) => void)[]` | - | - | +| `onDragEnter` | 目标元素拖拽进入回调函数 | `((args: DndSortableOnDragEnterArgs) => void) \| ((args: DndSortableOnDragEnterArgs) => void)[]` | - | - | +| `onDragLeave` | 目标元素拖拽离开回调函数 | `((args: DndSortableOnDragLeaveArgs) => void) \| ((args: DndSortableOnDragLeaveArgs) => void)[]` | - | - | +| `onDrop` | 拖拽结束回调函数 | `((args: DndSortableOnDropArgs) => void) \| ((args: DndSortableOnDropArgs) => void)[]` | - | - | + +#### DndSortableItemSlots + +| 名称 | 说明 | 参数类型 | 备注 | +| --- | --- | --- | --- | +| `default` | 默认插槽 | - | - | + +### CdkDndSortableHandle + +用来绑定拖拽排序元素的把手 + +### CdkDndMovable + +拖拽移动 + +#### DndMovableProps + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `tag` | 自定义组件根节点 | `string \| Component \| FunctionalComponent` | `'div'` | - | - | +| `allowedAxis` | 可以拖拽移动的方向 | `'horizontal' \| 'vertical' \| 'all'` | `'all'` | - | - | +| `mode` | 拖拽移动模式 | `'immediate' \| 'afterDrop'` | `'afterDrop'` | - | `'immediate'` 模式下,会随着拖拽实时改变元素位置,`'afterDrop'`则会在拖拽结束改变 | +| `strategy` | 拖拽移动策略 | `'fixed' \| 'absolute' \| 'transform'` | `'transform'` | - | 使用哪种方式设置元素位置 | +| `preview` | 拖拽元素的预览 | `boolean \| 'native' \| { offset: { x: number; y: number } }` | - | - | +| `canDrag` | 是否可以拖拽 | `boolean` | `true` | - | +| `dragHandle` | 设置拖拽的把手 | `HTMLElement \| undefined` | - | 设置把手之后仅可以通过把手拖拽,也可以使用 `CdkDndMovableHandle` 组件自定义 | +| `dropTargets` | 设置可以拖入的目标元素 | `(HTMLElement \| undefined)[]` | - | 仅在 `afterDrop` 模式下可用 | +| `boundary` | 设置可以拖拽的边界 | `'parent' \| 'viewport' \| HTMLElement \| undefined` | - | - | +| `onDragStart` | 开始拖拽的回调函数 | `((args: ElementEventBasePayload) => void) \| ((args: ElementEventBasePayload) => void)[]` | - | - | +| `onDrag` | 拖拽回调函数 | `((args: ElementEventBasePayload) => void) \| ((args: ElementEventBasePayload) => void)[]` | - | - | +| `onDragEnter` | 目标拖拽进入回调函数 | `((args: ElementDropTargetEventBasePayload) => void) \| ((args: ElementDropTargetEventBasePayload) => void)[]` | - | - | +| `onDragLeave` | 目标拖拽离开回调函数 | `((args: ElementDropTargetEventBasePayload) => void) \| ((args: ElementDropTargetEventBasePayload) => void)[]` | - | - | +| `onDrop` | 拖拽结束回调函数 | `((args: DndSortableOnDropArgs) => void) \| ((args: DndSortableOnDropArgs) => void)[]` | - | - | +| `onDropOfTarget` | 目标元素拖拽放入回调函数 | `((args: ElementDropTargetEventBasePayload) => void) \| ((args: ElementDropTargetEventBasePayload) => void)[]` | - | - | + +#### DndMovableSlots + +| 名称 | 说明 | 参数类型 | 备注 | +| --- | --- | --- | --- | +| `default` | 默认插槽 | - | - | +| `preview` | 预览 | `{ container: HTMLElement }` | - | + +#### DndMovableMethods + +| 名称 | 说明 | 参数类型 | 备注 | +| --- | --- | --- | --- | +| `init` | 重新初始化位置 | - | - | + +### CdkDndMovableHandle + +用来绑定拖拽移动元素的把手 + +## 组合式API + +### useDndContext + +创建一个拖拽监听上下文 + +```ts +function useDndContext(options?: DndOptions): DndContext + +interface DndOptions { + monitor?: MonitorOptions | boolean + onDrag?: MonitorOptions['onDrag'] + onDragOfTarget?: DropTargetOptions['onDrag'] + onDragStart?: MonitorOptions['onDragStart'] + onDragEnter?: DropTargetOptions['onDragEnter'] + onDragLeave?: DropTargetOptions['onDragLeave'] + onDrop?: MonitorOptions['onDrop'] + onDropOfTarget?: DropTargetOptions['onDrop'] +} + +interface DndContext { + registerDraggable: (options: DraggableOptions) => () => void + registerDropTarget: (options: DropTargetOptions) => () => void +} +``` + +详细参数类型请参考 [pragmatic-drag-and-drop](https://atlassian.design/components/pragmatic-drag-and-drop/about) + +### useDndSortable + +创建拖拽排序上下文 + +```ts +function useDndSortable(options: DndSortableOptions): DndSortableContext + +interface DndSortableOptions> { + dataSource: Ref[]> + direction?: Ref + childrenKey?: string | Ref + treeIndent?: number | Ref + getKey?: Ref + preview?: Ref + strategy?: DndSortableStrategy | Ref + canDrag?: (options: CanDragOptions) => boolean | undefined + canDrop?: (options: CanDropOptions) => boolean | undefined + isSticky?: (options: DndSortableIsStickyOptions) => boolean | undefined + isTreeItemExpanded?: (key: VKey, data: DndSortableData) => void + + onDragStart?: (args: DndSortableOnDragStartArgs) => void + onDrag?: (args: DndSortableOnDragArgs) => void + onDragEnter?: (args: DndSortableOnDragEnterArgs) => void + onDragLeave?: (args: DndSortableOnDragLeaveArgs) => void + onDrop?: (args: DndSortableOnDropArgs) => void + onSortReorder?: (info: DndSortableReorderInfo) => void + onSortChange?: (newDataSource: DndSortableData[], oldDataSource: DndSortableData[]) => void +} + +interface DndSortableContext { + registerDraggable: (options: DndSortableDraggableOptions) => () => void // 注册可拖拽的元素 + registerDropTarget: (options: DndSortableDropTargetOptions) => () => void // 注册可拖拽放入的元素 + draggingOverState: ComputedRef + draggingState: ComputedRef +} + +type DndSortableDraggableOptions = Omit & { + key: VKey + preview?: DndSortablePreviewOptions + canDrag?: (args: ElementGetFeedbackArgs & CanDragOptions) => boolean | undefined +} + +type DndSortableDropTargetOptions = Omit & { + key: VKey + direction?: DndSortableDirection + canDrop?: (args: ElementDropTargetGetFeedbackArgs & CanDropOptions) => boolean | undefined + isSticky?: (options: DndSortableIsStickyOptions) => boolean | undefined +} + +type DndSortablePreviewOptions = + | boolean + | { + offset?: { x: number; y: number } + mount?: (state: DndSortablePreviewState) => void + unmount?: (state: DndSortablePreviewState) => void + } +``` + +### useDndMovable + +拖拽移动元素 + +```ts +function useDndMovable(options: DndMovableOptions): DndMovableContext + +interface DndMovableOptions extends Omit { + mode?: MaybeRef + strategy?: MaybeRef + canDrag?: MaybeRef + draggableElement: MaybeElementRef + dropTargets?: MaybeRef + boundary?: MaybeRef + dragHandle?: MaybeElementRef + allowedAxis?: MaybeRef + preview?: MaybeRef +} + +interface DndMovableContext { + init: () => void + position: ComputedRef + offset: ComputedRef +} + +interface Position { + x: number + y: number +} + +type DndMovablePreviewOptions = + | boolean + | { + offset?: { x: number; y: number } + mount?: (args: { container: HTMLElement }) => void + unmount?: (args: { container: HTMLElement }) => void + } +``` + +### useDndAutoScroll + +绑定某个滚动容器,使拖拽的元素可以移动到边界时自动滚动 + +```ts +function useDndAutoScroll( + elementRef: Ref, + options?: AutoScrollOptions | Ref, +): void + +interface AutoScrollOptions { + canScroll: boolean + maxScrollSpeed?: 'standard' | 'fast' + allowedAxis?: 'horizontal' \| 'vertical' \| 'all' +} +``` diff --git a/packages/cdk/dnd/docs/Design.en.md b/packages/cdk/dnd/docs/Design.en.md new file mode 100644 index 000000000..1aa4a9cf2 --- /dev/null +++ b/packages/cdk/dnd/docs/Design.en.md @@ -0,0 +1 @@ +### Usage scenarios diff --git a/packages/cdk/dnd/docs/Design.zh.md b/packages/cdk/dnd/docs/Design.zh.md new file mode 100644 index 000000000..aa5080171 --- /dev/null +++ b/packages/cdk/dnd/docs/Design.zh.md @@ -0,0 +1 @@ +### 使用场景 diff --git a/packages/cdk/dnd/docs/Index.en.md b/packages/cdk/dnd/docs/Index.en.md new file mode 100644 index 000000000..0385fac82 --- /dev/null +++ b/packages/cdk/dnd/docs/Index.en.md @@ -0,0 +1,8 @@ +--- +category: cdk +type: +order: 0 +title: Dnd +subtitle: +--- + diff --git a/packages/cdk/dnd/docs/Index.zh.md b/packages/cdk/dnd/docs/Index.zh.md new file mode 100644 index 000000000..86903e99f --- /dev/null +++ b/packages/cdk/dnd/docs/Index.zh.md @@ -0,0 +1,8 @@ +--- +category: cdk +type: +order: 0 +title: Dnd +subtitle: 拖拽 +--- + diff --git a/packages/cdk/dnd/docs/Theme.en.md b/packages/cdk/dnd/docs/Theme.en.md new file mode 100644 index 000000000..06e4c3fce --- /dev/null +++ b/packages/cdk/dnd/docs/Theme.en.md @@ -0,0 +1,3 @@ +| name | default | seer | mark | +| --- | --- | --- | --- | +| - | - | - | - | diff --git a/packages/cdk/dnd/docs/Theme.zh.md b/packages/cdk/dnd/docs/Theme.zh.md new file mode 100644 index 000000000..6921a3cbe --- /dev/null +++ b/packages/cdk/dnd/docs/Theme.zh.md @@ -0,0 +1,3 @@ +| 名称 | default | seer | 备注 | +| --- | --- | --- | --- | +| - | - | - | - | diff --git a/packages/cdk/dnd/index.ts b/packages/cdk/dnd/index.ts new file mode 100644 index 000000000..142c01e7e --- /dev/null +++ b/packages/cdk/dnd/index.ts @@ -0,0 +1,88 @@ +/** + * @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 { + DndBoxIndicatorComponent, + DndMovableComponent, + DndSortableComponent, + DndSortableItemComponent, + DndTreeIndicatorComponent, +} from './src/types' + +import DndBoxIndicator from './src/indicator/DndBoxIndicator' +import DndTreeIndicator from './src/indicator/DndTreeIndicator' +import DndMovable from './src/movable/DndMovable' +import DndMovableHandle from './src/movable/DndMovableHandle' +import DndSortable from './src/sortable/DndSortable' +import DndSortableHandle from './src/sortable/DndSortableHandle' +import DndSortableItem from './src/sortable/DndSortableItem' + +const CdkDndSortable = DndSortable as DndSortableComponent +const CdkDndSortableItem = DndSortableItem as DndSortableItemComponent +const CdkDndSortableHandle = DndSortableHandle +const CdkDndBoxIndicator = DndBoxIndicator as DndBoxIndicatorComponent +const CdkDndTreeIndicator = DndTreeIndicator as DndTreeIndicatorComponent + +const CdkDndMovable = DndMovable as DndMovableComponent +const CdkDndMovableHandle = DndMovableHandle + +export * from './src/composables/useDndContext' +export * from './src/composables/useDndSortable' +export * from './src/composables/useDndAutoScroll' +export { reorderList, reorderTree, triggerPostMoveFlash } from './src/utils' + +export { CDK_DND_SORTABLE_TOKEN } from './src/tokens' + +export { + CdkDndSortable, + CdkDndSortableItem, + CdkDndSortableHandle, + CdkDndBoxIndicator, + CdkDndTreeIndicator, + CdkDndMovable, + CdkDndMovableHandle, +} + +export type { + Axis, + CanDragOptions, + CanDropOptions, + DndSortableData, + DndSortableDraggingState, + DndSortableDraggingOverState, + DndSortablePreviewState, + DndSortableDirection, + DndSortableReorderInfo, + DndBoxIndicatorProps, + DndBoxIndicatorComponent, + DndTreeIndicatorProps, + DndTreeIndicatorComponent, + DndSortableProps, + DndSortableComponent, + DndSortableInstance, + DndSortableItemComponent, + DndSortableItemProps, + DndSortableItemInstance, + DndMovableProps, + DndMovableComponent, + DndMovableInstance, + DndMovableMode, + DndMovableStrategy, + DndMovableOptions, + DndMovableBoundaryType, + DndMovablePreviewOptions, + DndSortableIsStickyOptions, + DndSortableOnDragArgs, + DndSortableOnDragStartArgs, + DndSortableOnDragEnterArgs, + DndSortableOnDragLeaveArgs, + DndSortableOnDropArgs, + DndSortableOptions, + DndSortableStrategy, + ListDraggingOverState, + TreeDraggingOverState, +} from './src/types' diff --git a/packages/cdk/dnd/src/composables/useDndAutoScroll.ts b/packages/cdk/dnd/src/composables/useDndAutoScroll.ts new file mode 100644 index 000000000..cd948358e --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndAutoScroll.ts @@ -0,0 +1,65 @@ +/** + * @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 { AutoScrollOptions } from '../types' + +import { type Ref, computed, isRef, watch } from 'vue' + +import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element' + +import { tryOnScopeDispose } from '@idux/cdk/utils' + +export function useDndAutoScroll( + elementRef: Ref, + options?: AutoScrollOptions | Ref, +): void { + let cleanUp: (() => void) | undefined + + const resolvedOptions = computed(() => { + const resolvedOptions = isRef(options) ? options.value : options + + const { canScroll = true, maxScrollSpeed = 'standard', allowedAxis = 'all' } = resolvedOptions ?? {} + + return { + canScroll, + maxScrollSpeed, + allowedAxis, + } + }) + + watch( + elementRef, + element => { + cleanUp?.() + + if (!element) { + return + } + + cleanUp = autoScrollForElements({ + element, + canScroll() { + return resolvedOptions.value.canScroll + }, + getConfiguration() { + return { maxScrollSpeed: resolvedOptions.value.maxScrollSpeed } + }, + getAllowedAxis() { + return resolvedOptions.value.allowedAxis + }, + }) + }, + { + immediate: true, + deep: true, + }, + ) + + tryOnScopeDispose(() => { + cleanUp?.() + }) +} diff --git a/packages/cdk/dnd/src/composables/useDndContext/index.ts b/packages/cdk/dnd/src/composables/useDndContext/index.ts new file mode 100644 index 000000000..e4c1252b1 --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndContext/index.ts @@ -0,0 +1,8 @@ +/** + * @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 + */ + +export * from './useDndContext' diff --git a/packages/cdk/dnd/src/composables/useDndContext/useContextRegistry.ts b/packages/cdk/dnd/src/composables/useDndContext/useContextRegistry.ts new file mode 100644 index 000000000..ca7e55ca3 --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndContext/useContextRegistry.ts @@ -0,0 +1,244 @@ +/** + * @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 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { ContextData, DraggableOptions, DropTargetOptions } from '../../types' + +import { isArray } from 'lodash-es' + +import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter' + +import { createSharedComposable, tryOnScopeDispose } from '@idux/cdk/utils' + +import { dndContextPropertyKey } from '../../consts' + +export interface ContextRegistry { + registerDraggable: (contextKey: number, options: DraggableOptions) => () => void + registerDropTarget: (contextKey: number, options: DropTargetOptions) => () => void + isRegiseredInContext: (contextKey: number, data: Record) => boolean +} + +function isContextData(data: Record): data is ContextData { + return dndContextPropertyKey in data && isArray(data[dndContextPropertyKey]) +} + +export const useContextRegistry = createSharedComposable((): ContextRegistry => { + const draggableRegistry = new Map>() + const dropTargetRegistry = new Map>() + + const draggableContextKeyMap = new Map>() + const dropTargetContextKeyMap = new Map>() + + const draggableCleanupMap = new Map void>() + const dropTargetCleanupMap = new Map void>() + + const _registerDraggable = (options: DraggableOptions) => { + const { element, dragHandle, onGenerateDragPreview } = options + + return draggable({ + element, + dragHandle, + getInitialData(args) { + const data = { + [dndContextPropertyKey]: [...(draggableContextKeyMap.get(element) ?? [])], + } + + return getMergedData(data, args, draggableRegistry.get(element)) + }, + canDrag(args) { + return getMergedBoolean(args, draggableRegistry.get(element), 'canDrag', true) + }, + onGenerateDragPreview, + onDrag(args) { + callEventHandlers(args, draggableRegistry.get(element), 'onDrag') + }, + onDragStart(args) { + callEventHandlers(args, draggableRegistry.get(element), 'onDragStart') + }, + onDropTargetChange(args) { + callEventHandlers(args, draggableRegistry.get(element), 'onDropTargetChange') + }, + onDrop(args) { + callEventHandlers(args, draggableRegistry.get(element), 'onDrop') + }, + }) + } + + const _registerDropTarget = (options: DropTargetOptions) => { + const { element, getDropEffect, onGenerateDragPreview } = options + + return dropTargetForElements({ + element, + getData(args) { + const data = { + [dndContextPropertyKey]: [...(dropTargetContextKeyMap.get(element) ?? [])], + } + + return getMergedData(data, args, dropTargetRegistry.get(element)) + }, + canDrop(args) { + return getMergedBoolean(args, dropTargetRegistry.get(element), 'canDrop', true) + }, + getDropEffect, + getIsSticky(args) { + return getMergedBoolean(args, dropTargetRegistry.get(element), 'getIsSticky', false) + }, + onGenerateDragPreview, + onDrag(args) { + callEventHandlers(args, dropTargetRegistry.get(element), 'onDrag') + }, + onDragEnter(args) { + callEventHandlers(args, dropTargetRegistry.get(element), 'onDragEnter') + }, + onDragLeave(args) { + callEventHandlers(args, dropTargetRegistry.get(element), 'onDragLeave') + }, + onDragStart(args) { + callEventHandlers(args, dropTargetRegistry.get(element), 'onDragStart') + }, + onDropTargetChange(args) { + callEventHandlers(args, dropTargetRegistry.get(element), 'onDropTargetChange') + }, + onDrop(args) { + callEventHandlers(args, dropTargetRegistry.get(element), 'onDrop') + }, + }) + } + + const getRegisterContext = (isDraggable: boolean) => { + return isDraggable + ? { + registry: draggableRegistry, + contextKeyMap: draggableContextKeyMap, + cleanupMap: draggableCleanupMap, + } + : { + registry: dropTargetRegistry, + contextKeyMap: dropTargetContextKeyMap, + cleanupMap: dropTargetCleanupMap, + } + } + + const register = (contextKey: number, options: DraggableOptions | DropTargetOptions, isDraggable: boolean) => { + const { element } = options + + const { registry, contextKeyMap, cleanupMap } = getRegisterContext(isDraggable) + + const registrys = registry.get(element) ?? new Set() + const contextKeys = contextKeyMap.get(element) ?? new Set() + + if (contextKeys.has(contextKey)) { + return () => {} + } + + const alreadyRegistered = !!registrys.size + registrys.add(options) + contextKeys.add(contextKey) + + if (!registry.has(element)) { + registry.set(element, registrys as any) + } + if (!contextKeyMap.has(element)) { + contextKeyMap.set(element, contextKeys) + } + + if (!alreadyRegistered) { + const cleanup = isDraggable ? _registerDraggable(options as DraggableOptions) : _registerDropTarget(options) + cleanupMap.set(element, cleanup) + } + + return () => { + registrys.delete(options) + contextKeys.delete(contextKey) + + if (!registrys.size) { + cleanupMap.get(element)?.() + cleanupMap.delete(element) + } + } + } + + const registerDraggable = (contextKey: number, options: DraggableOptions) => { + return register(contextKey, options, true) + } + + const registerDropTarget = (contextKey: number, options: DropTargetOptions) => { + return register(contextKey, options, false) + } + + const isRegiseredInContext = (contextKey: number, data: Record) => { + return isContextData(data) && data[dndContextPropertyKey].includes(contextKey) + } + + tryOnScopeDispose(() => { + draggableRegistry.clear() + dropTargetRegistry.clear() + draggableContextKeyMap.clear() + dropTargetContextKeyMap.clear() + + draggableCleanupMap.forEach(cleanup => cleanup()) + draggableCleanupMap.clear() + + dropTargetCleanupMap.forEach(cleanup => cleanup()) + dropTargetCleanupMap.clear() + }) + + return { + registerDraggable, + registerDropTarget, + isRegiseredInContext, + } +}) + +function getMergedData( + data: ContextData, + args: any, + registrys: Set | Set | undefined, +) { + let mergedData = data + registrys?.forEach(registry => { + let getData + if ('getInitialData' in registry) { + getData = registry.getInitialData + } else if ('getData' in registry) { + getData = registry.getData + } + + if (getData) { + mergedData = Object.assign(mergedData, getData(args)) + } + }) + + return mergedData +} + +function getMergedBoolean( + args: any, + registrys: Set | Set | undefined, + key: string, + defaultValue: boolean, +) { + let can = false + registrys?.forEach(registry => { + if ((registry[key as keyof typeof registry] as any)?.(args) ?? defaultValue) { + can = true + } + }) + + return can +} + +function callEventHandlers( + args: any, + registrys: Set | Set | undefined, + key: string, +) { + registrys?.forEach(registry => { + ;(registry[key as keyof typeof registry] as any)?.(args) + }) +} diff --git a/packages/cdk/dnd/src/composables/useDndContext/useDndContext.ts b/packages/cdk/dnd/src/composables/useDndContext/useDndContext.ts new file mode 100644 index 000000000..5b7583ad0 --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndContext/useDndContext.ts @@ -0,0 +1,176 @@ +/** + * @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 { DndOptions, DraggableOptions, DropTargetOptions } from '../../types' + +import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter' + +import { tryOnScopeDispose } from '@idux/cdk/utils' + +import { useContextRegistry } from './useContextRegistry' + +export interface DndContext { + registerDraggable: (options: DraggableOptions) => () => void + registerDropTarget: (options: DropTargetOptions) => () => void +} + +let contextKeySeed = 0 + +function genContextKey() { + return contextKeySeed++ +} + +export function useDndContext(options?: DndOptions): DndContext { + const { + monitor = true, + onDrag, + onDragOfTarget, + onDragEnter, + onDragStart, + onDragLeave, + onDrop, + onDropOfTarget, + } = options ?? {} + + const { + registerDraggable: _registerDraggable, + registerDropTarget: _registerDropTarget, + isRegiseredInContext, + } = useContextRegistry() + + const draggableCleanups: Set<() => void> = new Set() + const dropTargetCleanups: Set<() => void> = new Set() + + const key = genContextKey() + + let monitorCleanup: (() => void) | undefined + + if (monitor) { + const resolvedMonitor = monitor === true ? {} : monitor + const { + canMonitor, + onDrag: monitorOnDrag, + onDragStart: monitorOnDragStart, + onDrop: monitorOnDrop, + ...rest + } = resolvedMonitor + monitorCleanup = monitorForElements({ + ...rest, + canMonitor(args) { + const { source } = args + const isRegistered = isRegiseredInContext(key, source.data) + + return canMonitor ? canMonitor(args) && isRegistered : isRegistered + }, + onDrag(args) { + monitorOnDrag?.(args) + onDrag?.(args) + }, + onDragStart(args) { + monitorOnDragStart?.(args) + onDragStart?.(args) + }, + onDrop(args) { + monitorOnDrop?.(args) + onDrop?.(args) + }, + }) + } + + const registerDraggable = (options: DraggableOptions) => { + const { onDrag: itemOnDrag, onDragStart: itemOnDragStart, onDrop: itemOnDrop, ...rest } = options + const cleanup = _registerDraggable(key, { + ...rest, + onDrag(args) { + itemOnDrag?.(args) + if (!monitor) { + onDrag?.(args) + } + }, + onDrop(args) { + if (!monitor) { + onDrop?.(args) + } + + itemOnDrop?.(args) + }, + onDragStart(args) { + itemOnDragStart?.(args) + if (!monitor) { + onDragStart?.(args) + } + }, + }) + + draggableCleanups.add(cleanup) + + return () => { + cleanup() + draggableCleanups.delete(cleanup) + } + } + + const registerDropTarget = (options: DropTargetOptions) => { + const { + onDrop: itemOnDrop, + onDrag: itemOnDrag, + onDragEnter: itemOnDragEneter, + onDragLeave: itemOnDragLeave, + ...rest + } = options + const cleanup = _registerDropTarget(key, { + ...rest, + onDrop(args) { + itemOnDrop?.(args) + onDropOfTarget?.(args) + }, + onDrag(args) { + itemOnDrag?.(args) + onDragOfTarget?.(args) + }, + onDragEnter(args) { + const { source } = args + + if (!isRegiseredInContext(key, source.data)) { + return + } + + itemOnDragEneter?.(args) + onDragEnter?.(args) + }, + onDragLeave(args) { + itemOnDragLeave?.(args) + onDragLeave?.(args) + }, + }) + + dropTargetCleanups.add(cleanup) + + return () => { + cleanup() + dropTargetCleanups.delete(cleanup) + } + } + + const destroy = () => { + draggableCleanups.forEach(cleanup => cleanup()) + dropTargetCleanups.forEach(cleanup => cleanup()) + + draggableCleanups.clear() + dropTargetCleanups.clear() + monitorCleanup?.() + } + + tryOnScopeDispose(() => { + destroy() + }) + + return { + registerDraggable, + registerDropTarget, + } +} diff --git a/packages/cdk/dnd/src/composables/useDndMovable/index.ts b/packages/cdk/dnd/src/composables/useDndMovable/index.ts new file mode 100644 index 000000000..3d727f1f5 --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndMovable/index.ts @@ -0,0 +1,8 @@ +/** + * @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 + */ + +export * from './useDndMovable' diff --git a/packages/cdk/dnd/src/composables/useDndMovable/useDndMovable.ts b/packages/cdk/dnd/src/composables/useDndMovable/useDndMovable.ts new file mode 100644 index 000000000..368a6f302 --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndMovable/useDndMovable.ts @@ -0,0 +1,241 @@ +/** + * @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 { DndMovableOptions, DndOptions, Position } from '../../types' + +import { type ComputedRef, watch } from 'vue' + +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine' +import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview' +import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview' +import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview' + +import { convertCssPixel, tryOnScopeDispose } from '@idux/cdk/utils' + +import { useMovablePosition } from './useMovablePosition' +import { useResolvedOptions } from './useResolvedOptions' +import { isInBoundary, keepInBoundary } from '../../utils' +import { useDndContext } from '../useDndContext' + +export interface DndMovableContext { + init: () => void + position: ComputedRef + offset: ComputedRef +} + +export function useDndMovable(options: DndMovableOptions): DndMovableContext { + const { + mode, + allowedAxis, + strategy, + canDrag, + draggableElement, + dropTargets, + boundary, + dragHandle, + preview, + onDragStart, + onDrag, + onDrop, + ...rest + } = useResolvedOptions(options) + + const { + position, + offset, + init: initPosition, + start, + end, + update, + } = useMovablePosition(draggableElement, strategy, boundary, allowedAxis) + + let currentTarget: Element | null = null + + const onMovableItemDrop: DndOptions['onDrop'] = args => { + if (!draggableElement.value || mode.value !== 'afterDrop') { + return + } + + const { + location: { + current: { + input: { clientX, clientY }, + }, + initial: { + input: { clientX: initialX, clientY: initialY }, + }, + }, + } = args + + if (!dropTargets.value?.length) { + update({ + x: clientX, + y: clientY, + }) + return + } + + const elementRect = draggableElement.value.getBoundingClientRect() + const currentOffset = { + x: clientX - initialX, + y: clientY - initialY, + } + const dropTarget = dropTargets.value.find(target => + isInBoundary(elementRect, target.getBoundingClientRect(), currentOffset), + ) + + if (dropTarget) { + currentTarget = dropTarget + update({ + x: clientX, + y: clientY, + }) + return + } + + if (currentTarget) { + const { x, y } = keepInBoundary(elementRect, currentTarget.getBoundingClientRect(), currentOffset) + update({ + x: x + initialX, + y: y + initialY, + }) + } + } + + const { registerDraggable, registerDropTarget } = useDndContext({ + monitor: false, + ...rest, + onDragStart(args) { + const { + location: { + initial: { + input: { clientX, clientY }, + }, + }, + } = args + + start({ x: clientX, y: clientY }) + onDragStart?.(args) + }, + onDrag(args) { + const { + location: { + current: { + input: { clientX, clientY }, + }, + }, + } = args + + if (mode.value === 'immediate') { + update({ + x: clientX, + y: clientY, + }) + } + + onDrag?.(args) + }, + onDrop(args) { + onMovableItemDrop(args) + end() + onDrop?.(args) + }, + }) + + let draggableCleanup: (() => void) | undefined + let dropTargetCleanup: (() => void) | undefined + + watch( + [draggableElement, dragHandle], + ([element, handleEl]) => { + draggableCleanup?.() + + if (!element) { + return + } + + draggableCleanup = registerDraggable({ + element, + dragHandle: handleEl ?? undefined, + canDrag() { + return canDrag.value + }, + onGenerateDragPreview({ nativeSetDragImage }) { + if (mode.value === 'immediate' || preview.value === false) { + disableNativeDragPreview({ nativeSetDragImage }) + return + } + + if (preview.value === true || !preview.value) { + return + } + + const { offset, mount, unmount } = preview.value + setCustomNativeDragPreview({ + nativeSetDragImage, + getOffset: offset + ? pointerOutsideOfPreview({ x: convertCssPixel(offset.x), y: convertCssPixel(offset.y) }) + : undefined, + render({ container }) { + // this is hack for large preview to render with less transparency gradient + Object.assign(container.style, { + width: '1000px', + height: '1000px', + }) + mount?.({ container }) + + return () => { + unmount?.({ container }) + } + }, + }) + }, + }) + }, + { + immediate: true, + }, + ) + + watch( + dropTargets, + targets => { + dropTargetCleanup?.() + + if (!targets?.length) { + return + } + + dropTargetCleanup = combine( + ...targets.map(target => + registerDropTarget({ + element: target, + }), + ), + ) + }, + { + immediate: true, + }, + ) + + const init = () => { + currentTarget = null + initPosition(true) + } + + tryOnScopeDispose(() => { + draggableCleanup?.() + dropTargetCleanup?.() + }) + + return { + init, + position, + offset, + } +} diff --git a/packages/cdk/dnd/src/composables/useDndMovable/useMovablePosition.ts b/packages/cdk/dnd/src/composables/useDndMovable/useMovablePosition.ts new file mode 100644 index 000000000..e31ee5324 --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndMovable/useMovablePosition.ts @@ -0,0 +1,128 @@ +/** + * @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 { Axis, DndMovableStrategy, Position, ResolvedBoundary } from '../../types' + +import { type ComputedRef, watch } from 'vue' + +import { useState } from '@idux/cdk/utils' + +import { getPositionFromMatrix, keepInAxis, keepInBoundary } from '../../utils' + +export function useMovablePosition( + elementRef: ComputedRef, + strategy: ComputedRef, + boundary: ComputedRef, + allowedAxis: ComputedRef, +): { + position: ComputedRef + offset: ComputedRef + init: (reset?: boolean) => void + start: (initial: Position) => void + end: () => void + update: (current: Position) => void +} { + const [position, setPosition] = useState({ x: 0, y: 0 }) + const [offset, setOffset] = useState({ x: 0, y: 0 }) + + const initPosition = (element: HTMLElement) => { + const _strategy = strategy.value + if (_strategy === 'absolute' || _strategy === 'transform') { + const { offsetTop, offsetLeft } = element + setPosition({ x: offsetLeft, y: offsetTop }) + } else { + const { x, y } = element.getBoundingClientRect() + setPosition({ x, y }) + } + } + + const initOffset = (element: HTMLElement, reset = false) => { + if (strategy.value !== 'transform') { + return + } + + if (reset) { + setOffset({ x: 0, y: 0 }) + } + + const { transform } = getComputedStyle(element) + + const offset = getPositionFromMatrix(transform) + + if (offset) { + setOffset(offset) + } + } + + let isDragging = false + let lastDragPosition: Position | undefined + + const start = (initial: Position) => { + lastDragPosition = initial + isDragging = true + } + const end = () => { + isDragging = false + } + + const update = (current: Position) => { + if (!isDragging || !lastDragPosition) { + return + } + + const element = elementRef.value + if (!element) { + return + } + + const { x: offsetXOfTick, y: offsetYOfTick } = keepInAxis( + allowedAxis.value, + keepInBoundary(elementRef.value.getBoundingClientRect(), boundary.value, { + x: current.x - lastDragPosition.x, + y: current.y - lastDragPosition.y, + }), + ) + + if (offsetXOfTick === 0 && offsetYOfTick === 0) { + return + } + + const _offset = offset.value + const _position = position.value + + setOffset({ + x: _offset.x + offsetXOfTick, + y: _offset.y + offsetYOfTick, + }) + setPosition({ + x: _position.x + offsetXOfTick, + y: _position.y + offsetYOfTick, + }) + + lastDragPosition = current + } + + const init = (reset?: boolean) => { + const element = elementRef.value + + if (element) { + initPosition(element) + initOffset(element, reset) + } + } + + watch([elementRef, strategy], () => init()) + + return { + position, + offset, + init, + start, + end, + update, + } +} diff --git a/packages/cdk/dnd/src/composables/useDndMovable/useResolvedOptions.ts b/packages/cdk/dnd/src/composables/useDndMovable/useResolvedOptions.ts new file mode 100644 index 000000000..749f34bbe --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndMovable/useResolvedOptions.ts @@ -0,0 +1,92 @@ +/** + * @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 { DndMovableBoundaryType, DndMovableOptions, ResolvedBoundary, ResolvedMovableOptions } from '../../types' + +import { computed, unref } from 'vue' + +import { isNil } from 'lodash-es' + +import { convertElement, useState } from '@idux/cdk/utils' + +import { defaultMovableAllowedAxis, defaultMovableMode, defaultMovableStrategy } from '../../consts' + +export function useResolvedOptions(options: DndMovableOptions): ResolvedMovableOptions { + const { + mode, + strategy, + canDrag, + draggableElement, + dropTargets, + boundary, + dragHandle, + allowedAxis, + preview, + onDragStart: optionOnDragStart, + ...rest + } = options + + const resolvedDraggleElement = computed(() => convertElement(unref(draggableElement)) ?? undefined) + + const [resolvedBoundary, setResolvedBoundary] = useState(undefined) + + const onDragStart: ResolvedMovableOptions['onDragStart'] = args => { + setResolvedBoundary(getBoundary(resolvedDraggleElement.value, unref(boundary))) + optionOnDragStart?.(args) + } + + return { + mode: computed(() => { + const moveMode = unref(mode) + + return isNil(moveMode) ? defaultMovableMode : moveMode + }), + strategy: computed(() => { + const _strategy = unref(strategy) + + return isNil(_strategy) ? defaultMovableStrategy : _strategy + }), + dropTargets: computed(() => { + const targets = unref(dropTargets) + + return targets?.filter(Boolean).map(convertElement) as Element[] | undefined + }), + dragHandle: computed(() => convertElement(unref(dragHandle)) ?? undefined), + canDrag: computed(() => { + const _canDrag = unref(canDrag) + + return isNil(_canDrag) ? true : _canDrag + }), + draggableElement: resolvedDraggleElement, + boundary: resolvedBoundary, + allowedAxis: computed(() => { + const axis = unref(allowedAxis) + + return axis ?? defaultMovableAllowedAxis + }), + preview: computed(() => unref(preview)), + onDragStart, + ...rest, + } +} + +function getBoundary(element: HTMLElement | undefined, boundary: DndMovableBoundaryType) { + const _boundary = unref(boundary) + + if (_boundary === 'viewport') { + return { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight } + } + + const boundaryElement = _boundary === 'parent' ? element?.parentElement : convertElement(_boundary) + + if (!boundaryElement) { + return + } + + const { left, right, top, bottom } = boundaryElement.getBoundingClientRect() + return { left, right, top, bottom } +} diff --git a/packages/cdk/dnd/src/composables/useDndSortable/createListStrategyContext.ts b/packages/cdk/dnd/src/composables/useDndSortable/createListStrategyContext.ts new file mode 100644 index 000000000..de4352ff0 --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndSortable/createListStrategyContext.ts @@ -0,0 +1,169 @@ +/** + * @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 { + DndOptions, + DndSortableData, + DndSortableDirection, + DndSortableInnerData, + DndSortableReorderInfo, + DndSortableStrategyContext, + DndSortableTransferData, + GetKey, + ListDraggingOverState, +} from '../../types' + +import { type Ref, computed, toRaw } from 'vue' + +import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge' +import { getReorderDestinationIndex } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index' + +import { type VKey } from '@idux/cdk/utils' + +import { isDndSortableTransferData, reorderList } from '../../utils' + +export function createListStrategyContext(options: { + dataSource: Ref + getKey: Ref + direction: Ref + setDraggingOverState: (state: ListDraggingOverState | null) => void +}): DndSortableStrategyContext { + const { dataSource, getKey, direction, setDraggingOverState } = options + const dataMap = computed(() => { + const map = new Map() + + dataSource.value.forEach((data, index) => { + const key = getKey.value(data) + map.set(key, { ...data, _data_index: index }) + }) + + return map + }) + + const getData = (key: VKey) => dataMap.value.get(key) + const getDataIndex = (key: VKey) => dataMap.value.get(key)?._data_index + const dataExists = (key: VKey) => dataMap.value.has(key) + + const getDropTargetData: DndSortableStrategyContext['getDropTargetData'] = (args, data) => { + const { input, element } = args + const { direction: itemDirection } = data + return attachClosestEdge(data, { + input, + element, + allowedEdges: (itemDirection ?? direction.value) === 'vertical' ? ['top', 'bottom'] : ['left', 'right'], + }) + } + + const reorder = ({ + sourceData, + targetData, + }: { + sourceData: DndSortableTransferData + targetData: DndSortableTransferData + }): { + reorderInfo: DndSortableReorderInfo + newData: DndSortableData[] + oldData: DndSortableData[] + } | null => { + const sourceIndex = sourceData.listDataIndex + const targetIndex = targetData.listDataIndex + const itemDirection = targetData.direction + + const edge = extractClosestEdge(targetData) + + const finishIndex = getReorderDestinationIndex({ + startIndex: sourceIndex, + closestEdgeOfTarget: edge, + indexOfTarget: targetIndex, + axis: itemDirection ?? direction.value, + }) + + if (finishIndex === sourceIndex) { + return null + } + + const oldDataSource = toRaw(dataSource.value) + const reorderInfo = { + sourceIndex, + targetIndex, + sourceKey: sourceData.key, + targetKey: targetData.key, + sourceData: sourceData.listData, + targetData: targetData.listData, + operation: finishIndex === targetIndex ? ('insertAfter' as const) : ('insertBefore' as const), + } + const newDataSource = reorderList(oldDataSource, reorderInfo) + + return { + newData: newDataSource, + oldData: oldDataSource, + reorderInfo, + } + } + + const onDragOfTarget: DndOptions['onDragOfTarget'] = args => { + const { self, source } = args + + const sourceData = source.data + const targetData = self.data + + if ( + source.element === self.element || + !isDndSortableTransferData(sourceData) || + !isDndSortableTransferData(targetData) + ) { + setDraggingOverState(null) + return + } + + const sourceIndex = sourceData.listDataIndex + const targetIndex = targetData.listDataIndex + + const mergedDirection = targetData.direction ?? direction.value + + const edge = extractClosestEdge(targetData) + const isItemBeforeSource = targetIndex === sourceIndex - 1 + const isItemAfterSource = targetIndex === sourceIndex + 1 + + const [beforeEdge, afterEdge] = + mergedDirection === 'horizontal' ? (['left', 'right'] as const) : (['top', 'bottom'] as const) + const unsortted = (isItemBeforeSource && edge === afterEdge) || (isItemAfterSource && edge === beforeEdge) + + if (unsortted) { + setDraggingOverState(null) + return + } + + setDraggingOverState({ + key: targetData.key, + data: targetData.listData, + index: targetIndex, + instruction: edge ? { edge } : null, + }) + } + + const onDragLeave: DndOptions['onDragLeave'] = () => { + setDraggingOverState(null) + } + + const onDrop: DndOptions['onDrag'] = () => { + setDraggingOverState(null) + } + + return { + getData, + getDataIndex, + dataExists, + getDropTargetData, + reorder, + eventHandlers: { + onDragOfTarget, + onDragLeave, + onDrop, + }, + } +} diff --git a/packages/cdk/dnd/src/composables/useDndSortable/createTreeStrategyContext.ts b/packages/cdk/dnd/src/composables/useDndSortable/createTreeStrategyContext.ts new file mode 100644 index 000000000..114e6e3f6 --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndSortable/createTreeStrategyContext.ts @@ -0,0 +1,287 @@ +/** + * @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 { + DndOptions, + DndSortableData, + DndSortableInnerData, + DndSortableReorderInfo, + DndSortableStrategyContext, + DndSortableTransferData, + GetKey, + TreeDraggingOverState, +} from '../../types' + +import { type Ref, computed, toRaw, watch } from 'vue' + +import { isArray, isNil } from 'lodash-es' + +import { attachInstruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item' + +import { type VKey, traverseTree } from '@idux/cdk/utils' + +import { isDndSortableTransferData, reorderTree } from '../../utils' + +export function createTreeStrategyContext(options: { + dataSource: Ref[]> + getKey: Ref + childrenKey: Ref + treeIndent: Ref + isTreeItemExpanded?: (key: VKey, data: DndSortableData) => void + setDraggingOverState: (state: TreeDraggingOverState | null) => void +}): DndSortableStrategyContext { + const { dataSource, getKey, childrenKey, treeIndent, isTreeItemExpanded, setDraggingOverState } = options + + const dataContext = computed(() => { + const dataMap = new Map>() + const parentKeyMap = new Map() + const depthMap = new Map() + + traverseTree(dataSource.value, childrenKey.value, (item, parents, index) => { + const key = getKey.value(item) + const parent = parents[0] + dataMap.set(key, { ...item, _data_index: index }) + depthMap.set(key, parents.length) + + if (parent) { + parentKeyMap.set(key, getKey.value(parent)) + } + }) + + return { + dataMap, + parentKeyMap, + depthMap, + } + }) + + const pathToItemCache = new Map() + const isLastInGroupCache = new Map() + + const getPathToItem = (key: VKey) => { + if (pathToItemCache.has(key)) { + return pathToItemCache.get(key)! + } + + const path = [] + let current: VKey | undefined = key + while (!isNil(current)) { + path.unshift(current) + current = dataContext.value.parentKeyMap.get(current) + } + + pathToItemCache.set(key, path) + + return path + } + const getIsLastInGroup = (key: VKey) => { + if (isLastInGroupCache.has(key)) { + return isLastInGroupCache.get(key)! + } + + const parentKey = dataContext.value.parentKeyMap.get(key) + const dataArray = isNil(parentKey) ? dataSource.value : getData(parentKey)![childrenKey.value] ?? [] + const dataIndex = getDataIndex(key) + + const isLastInGroup = dataIndex === dataArray.length - 1 + + isLastInGroupCache.set(key, isLastInGroup) + + return isLastInGroup + } + watch(dataContext, () => { + pathToItemCache.clear() + isLastInGroupCache.clear() + }) + + const getData = (key: VKey) => dataContext.value.dataMap.get(key) + const getDataIndex = (key: VKey) => dataContext.value.dataMap.get(key)?._data_index + const dataExists = (key: VKey) => dataContext.value.dataMap.has(key) + + const getDropTargetData: DndSortableStrategyContext['getDropTargetData'] = (args, targetData) => { + const { input, element, source } = args + const { key } = targetData + const sourceData = source.data as DndSortableTransferData + const sourceKey = sourceData.key + const targetKey = targetData.key + const targetTreeData = targetData.listData as DndSortableData + + const pathToTarget = getPathToItem(targetKey) + const isTargetChildrenOfSource = pathToTarget.includes(sourceKey) + + return attachInstruction(targetData, { + input, + element, + indentPerLevel: treeIndent.value, + currentLevel: dataContext.value.depthMap.get(key)!, + mode: isTreeItemExpanded?.(key, targetTreeData) + ? 'expanded' + : getIsLastInGroup(key) + ? 'last-in-group' + : 'standard', + block: isTargetChildrenOfSource + ? ['make-child', 'reorder-above', 'reorder-below', 'reparent'] + : !isArray(targetTreeData[childrenKey.value]) + ? ['make-child'] + : undefined, + }) + } + + const reorder = ({ + sourceData, + targetData, + }: { + sourceData: DndSortableTransferData + targetData: DndSortableTransferData + }): { + reorderInfo: DndSortableReorderInfo + newData: DndSortableData[] + oldData: DndSortableData[] + } | null => { + const instruction = extractInstruction(targetData) + + if (!instruction) { + return null + } + + const sourceKey = sourceData.key + const targetKey = targetData.key + const sourceIndex = sourceData.listDataIndex + const targetIndex = targetData.listDataIndex + const oldData = toRaw(dataSource.value) + + let reorderInfo: DndSortableReorderInfo | undefined + const pathToItem = getPathToItem(targetKey) + + if (pathToItem.includes(sourceKey)) { + return null + } + + if (instruction.type === 'reparent') { + const desiredKey = pathToItem[instruction.desiredLevel] + + if (isNil(desiredKey)) { + return null + } + + const desiredIndex = getDataIndex(desiredKey)! + + reorderInfo = { + sourceIndex, + targetIndex: desiredIndex, + sourceKey, + targetKey: desiredKey, + sourceData: sourceData.listData, + targetData: getData(desiredKey)!, + operation: 'insertAfter', + } + } else if (instruction.type === 'make-child') { + reorderInfo = { + sourceIndex, + targetIndex, + sourceKey, + targetKey, + sourceData: sourceData.listData, + targetData, + operation: 'insertChild', + } + } else if (instruction.type === 'reorder-above') { + reorderInfo = { + sourceIndex, + targetIndex, + sourceKey, + targetKey, + sourceData: sourceData.listData, + targetData, + operation: 'insertBefore', + } + } else if (instruction.type === 'reorder-below') { + reorderInfo = { + sourceIndex, + targetIndex, + sourceKey, + targetKey, + sourceData: sourceData.listData, + targetData, + operation: 'insertAfter', + } + } + + if (!reorderInfo) { + return null + } + + const newData = reorderTree(oldData, reorderInfo, childrenKey.value, getKey.value) + + return { + oldData, + newData, + reorderInfo, + } + } + + const onDragOfTarget: DndOptions['onDragOfTarget'] = args => { + const { self, source } = args + + const sourceData = source.data + const targetData = self.data + + if (!isDndSortableTransferData(sourceData) || !isDndSortableTransferData(targetData)) { + setDraggingOverState(null) + return + } + + const targetIndex = targetData.listDataIndex + const instruction = extractInstruction(targetData) + const { parentKeyMap, depthMap } = dataContext.value + const targetKey = targetData.key + const parentKey = parentKeyMap.get(targetKey) + + if (sourceData.key !== targetData.key || instruction?.type === 'reparent') { + setDraggingOverState({ + key: targetKey, + data: targetData, + index: targetIndex, + instruction: instruction + ? { + ...instruction, + parent: !isNil(parentKey) + ? { + key: parentKey, + level: depthMap.get(parentKey)!, + } + : undefined, + } + : null, + }) + return + } + + setDraggingOverState(null) + } + + const onDragLeave: DndOptions['onDragLeave'] = () => { + setDraggingOverState(null) + } + + const onDrop: DndOptions['onDrag'] = () => { + setDraggingOverState(null) + } + + return { + getData, + getDataIndex, + getDropTargetData, + dataExists, + reorder, + eventHandlers: { + onDragOfTarget, + onDragLeave, + onDrop, + }, + } +} diff --git a/packages/cdk/dnd/src/composables/useDndSortable/index.ts b/packages/cdk/dnd/src/composables/useDndSortable/index.ts new file mode 100644 index 000000000..03b5c5cda --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndSortable/index.ts @@ -0,0 +1,8 @@ +/** + * @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 + */ + +export * from './useDndSortable' diff --git a/packages/cdk/dnd/src/composables/useDndSortable/useDndSortable.ts b/packages/cdk/dnd/src/composables/useDndSortable/useDndSortable.ts new file mode 100644 index 000000000..457dfc7ba --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndSortable/useDndSortable.ts @@ -0,0 +1,311 @@ +/** + * @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 { + DndSortableDraggableOptions, + DndSortableDraggingOverState, + DndSortableDraggingState, + DndSortableDropTargetOptions, + DndSortableOptions, + DndSortableTransferData, +} from '../../types' + +import { type ComputedRef, computed } from 'vue' + +import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview' +import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview' +import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview' + +import { type VKey, convertCssPixel, useState } from '@idux/cdk/utils' + +import { useDndSortableStrategy } from './useDndSortableStrategy' +import { callEventHandler, callEventHandlerWithSource, isDndSortableTransferData } from '../../utils' +import { useDndContext } from '../useDndContext' +import { useGetKey } from '../useGetKey' + +export interface DndSortableContext { + registerDraggable: (options: DndSortableDraggableOptions) => () => void + registerDropTarget: (options: DndSortableDropTargetOptions) => () => void + draggingOverState: ComputedRef + draggingState: ComputedRef +} + +export function useDndSortable(options: DndSortableOptions): DndSortableContext { + const { + getKey, + direction, + preview, + isSticky, + canDrag, + canDrop, + onDrag, + onDragStart, + onDragEnter, + onDragLeave, + onDrop, + onSortReorder, + onSortChange, + } = options + + const mergedGetKey = useGetKey(getKey) + const mergedDirection = computed(() => { + if (!direction?.value) { + return 'vertical' + } + + return direction.value + }) + + const [draggingState, setDraggingState] = useState(null) + + const strategyContext = useDndSortableStrategy(options, mergedDirection, mergedGetKey) + + const getListData = (key: VKey) => strategyContext.getData(key) + const getListDataIndex = (key: VKey) => strategyContext.getDataIndex(key) + const listDataExists = (key: VKey) => strategyContext.dataExists(key) + + const reorderDataSource = (args: { sourceData: DndSortableTransferData; targetData: DndSortableTransferData }) => { + const result = strategyContext.reorder(args) + + if (!result) { + return + } + + const { newData, oldData, reorderInfo } = result + + onSortReorder?.(reorderInfo) + onSortChange?.(newData, oldData) + } + + const { registerDraggable: _registerDraggable, registerDropTarget: _registerDropTarget } = useDndContext({ + monitor: true, + onDragStart(args) { + const { source, location } = args + const sourceData = source.data + if (!isDndSortableTransferData(sourceData)) { + return + } + + strategyContext.eventHandlers.onDragStart?.(args) + setDraggingState({ key: sourceData.key, data: sourceData.listData, index: sourceData.listDataIndex }) + + callEventHandler(onDragStart, sourceData, location) + }, + onDrag(args) { + const { source, location } = args + + const sourceData = source.data + + strategyContext.eventHandlers.onDrag?.(args) + callEventHandler(onDrag, sourceData, location) + }, + onDragOfTarget(args) { + strategyContext.eventHandlers.onDragOfTarget?.(args) + }, + onDragEnter(args) { + const { self, source, location } = args + + strategyContext.eventHandlers.onDragEnter?.(args) + callEventHandlerWithSource(onDragEnter, self.data, source.data, location) + }, + onDragLeave(args) { + const { self, source, location } = args + + strategyContext.eventHandlers.onDragLeave?.(args) + callEventHandlerWithSource(onDragLeave, self.data, source.data, location) + }, + onDrop(args) { + const { source, location } = args + setDraggingState(null) + strategyContext.eventHandlers.onDrop?.(args) + const target = location.current.dropTargets[0] + if (!target) { + return + } + + const sourceData = source.data + const targetData = target.data + + if (!isDndSortableTransferData(sourceData) || !isDndSortableTransferData(targetData)) { + return + } + + if ( + listDataExists(mergedGetKey.value(sourceData.listData)) && + listDataExists(mergedGetKey.value(targetData.listData)) + ) { + reorderDataSource({ + sourceData, + targetData, + }) + } + + callEventHandlerWithSource(onDrop, targetData, sourceData, location) + }, + }) + + const registerDraggable = (options: DndSortableDraggableOptions) => { + const { getInitialData, canDrag: innerCanDrag, key, onGenerateDragPreview, preview: itemPreview, ...rest } = options + return _registerDraggable({ + ...rest, + getInitialData(args) { + const listData = getListData(key) + const listDataIndex = getListDataIndex(key) + + return { + ...(getInitialData?.(args) ?? {}), + key, + listData, + listDataIndex, + } + }, + canDrag(args) { + if (!listDataExists(key)) { + return false + } + + if (!canDrag && !innerCanDrag) { + return true + } + + const sourceData = getListData(key) + const sourceIndex = getListDataIndex(key) + + const canDragOptions = { sourceKey: key, sourceData, sourceIndex } + + const listCanDrag = canDrag ? canDrag(canDragOptions) ?? true : true + const itemCanDrag = innerCanDrag ? innerCanDrag({ ...args, ...canDragOptions }) ?? true : true + + return listCanDrag && itemCanDrag + }, + onGenerateDragPreview(args) { + if (onGenerateDragPreview) { + onGenerateDragPreview(args) + return + } + + const { + nativeSetDragImage, + source: { data: sourceData }, + } = args + + if (!isDndSortableTransferData(sourceData)) { + return + } + + const listPreview = preview?.value + + if (listPreview === false || itemPreview === false) { + disableNativeDragPreview({ nativeSetDragImage }) + return + } + + const mergedPreview = itemPreview ?? listPreview + + if (mergedPreview === true || !mergedPreview) { + return + } + + const { offset, mount, unmount } = mergedPreview + setCustomNativeDragPreview({ + nativeSetDragImage, + getOffset: offset + ? pointerOutsideOfPreview({ x: convertCssPixel(offset.x), y: convertCssPixel(offset.y) }) + : undefined, + render({ container }) { + const previewState = { key, data: sourceData.listData, index: sourceData.listDataIndex, container } + + // this is hack for large preview to render with less transparency gradient + Object.assign(container.style, { + width: '1000px', + height: '1000px', + }) + mount?.(previewState) + + return () => { + unmount?.(previewState) + } + }, + }) + }, + }) + } + + const registerDropTarget = (options: DndSortableDropTargetOptions) => { + const { + key, + element, + direction, + canDrop: innerCanDrop, + isSticky: itemIsSticky, + getIsSticky, + getData, + onDrag, + onDragLeave, + onDrop, + ...rest + } = options + return _registerDropTarget({ + element, + ...rest, + getData(args) { + const listData = getListData(key) + const listDataIndex = getListDataIndex(key) + const data = { + ...(getData?.(args) ?? {}), + key, + direction, + listData, + listDataIndex, + } + + return strategyContext.getDropTargetData + ? strategyContext.getDropTargetData(args, data as DndSortableTransferData) + : data + }, + getIsSticky(args) { + if (getIsSticky) { + return getIsSticky(args) + } + + const options = { + key, + data: getListData(key), + index: getListDataIndex(key), + } + + return !!(itemIsSticky?.(options) ?? isSticky?.(options)) + }, + canDrop(args) { + if (!listDataExists(key)) { + return false + } + + if (!canDrop && !innerCanDrop) { + return true + } + + const targetData = getListData(key) + const targetIndex = getListDataIndex(key) + const { key: sourceKey, data: sourceData, index: sourceIndex } = draggingState.value ?? {} + + const canDropOptions = { sourceKey, sourceData, sourceIndex, targetKey: key, targetData, targetIndex } + + const listCanDrop = canDrop ? canDrop(canDropOptions) ?? true : true + const itemCanDrop = innerCanDrop ? innerCanDrop({ ...args, ...canDropOptions }) ?? true : true + return listCanDrop && itemCanDrop + }, + }) + } + + return { + registerDraggable, + registerDropTarget, + draggingState, + draggingOverState: strategyContext.draggingOverState, + } +} diff --git a/packages/cdk/dnd/src/composables/useDndSortable/useDndSortableStrategy.ts b/packages/cdk/dnd/src/composables/useDndSortable/useDndSortableStrategy.ts new file mode 100644 index 000000000..4cd81d09f --- /dev/null +++ b/packages/cdk/dnd/src/composables/useDndSortable/useDndSortableStrategy.ts @@ -0,0 +1,133 @@ +/** + * @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 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { + DndSortableDirection, + DndSortableDraggingOverState, + DndSortableOptions, + DndSortableStrategyContext, + GetKey, +} from '../../types' + +import { type ComputedRef, type EffectScope, type Ref, computed, effectScope, isRef, watch } from 'vue' + +import { tryOnScopeDispose, useState } from '@idux/cdk/utils' + +import { createListStrategyContext } from './createListStrategyContext' +import { createTreeStrategyContext } from './createTreeStrategyContext' +import { defaultChildrenKey, defaultSortableStrategy, defaultTreeIndent } from '../../consts' + +export function useDndSortableStrategy>( + options: DndSortableOptions, + mergedDirection: Ref, + mergedGetKey: Ref, +): DndSortableStrategyContext & { draggingOverState: ComputedRef } { + const { dataSource, childrenKey, treeIndent, strategy, isTreeItemExpanded } = options + const [draggingOverState, setDraggingOverState] = useState(null) + + let contextScope: { dispose: () => void } | undefined + const context: DndSortableStrategyContext & { draggingOverState: ComputedRef } = { + draggingOverState, + } as unknown as DndSortableStrategyContext & { draggingOverState: ComputedRef } + + const resolvedStrategy = computed(() => { + const _strategy = isRef(strategy) ? strategy.value : strategy + + return _strategy ?? defaultSortableStrategy + }) + const mergedChildrenKey = computed(() => { + const _childrenKey = isRef(childrenKey) ? childrenKey.value : childrenKey + + return (_childrenKey ?? defaultChildrenKey) as C + }) + const mergedTreeIndent = computed(() => { + const _treeIndent = isRef(treeIndent) ? treeIndent.value : treeIndent + + return _treeIndent ?? defaultTreeIndent + }) + + watch( + resolvedStrategy, + strategy => { + contextScope?.dispose() + + if (strategy === 'tree') { + const scope = createContextScope(createTreeStrategyContext) + contextScope = scope + Object.assign( + context, + scope.run({ + dataSource, + getKey: mergedGetKey, + childrenKey: mergedChildrenKey as unknown as Ref, + treeIndent: mergedTreeIndent, + setDraggingOverState, + isTreeItemExpanded: isTreeItemExpanded as any, + })!, + ) + } else { + const scope = createContextScope(createListStrategyContext) + contextScope = scope + Object.assign( + context, + scope.run({ + dataSource, + direction: mergedDirection, + getKey: mergedGetKey, + setDraggingOverState, + })!, + ) + } + }, + { + immediate: true, + }, + ) + + return context +} + +function createContextScope ReturnType>( + fn: T, +): { + run: (...args: Parameters) => ReturnType | undefined + dispose: () => void +} { + let scope: EffectScope | undefined = effectScope() + let state: ReturnType | undefined + + const run = (...args: Parameters) => { + if (!scope) { + return + } + + if (state) { + return state + } + + state = scope.run(() => fn(...args)) + + return state + } + + const dispose = () => { + scope?.stop() + state = undefined + scope = undefined + } + + tryOnScopeDispose(() => { + dispose() + }) + + return { + run, + dispose, + } +} diff --git a/packages/cdk/dnd/src/composables/useGetKey.ts b/packages/cdk/dnd/src/composables/useGetKey.ts new file mode 100644 index 000000000..a41ac2e70 --- /dev/null +++ b/packages/cdk/dnd/src/composables/useGetKey.ts @@ -0,0 +1,31 @@ +/** + * @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 { GetKey } from '../types' + +import { type ComputedRef, type Ref, computed } from 'vue' + +import { isString } from 'lodash-es' + +import { Logger } from '@idux/cdk/utils' + +export function useGetKey(getKey: Ref | undefined): ComputedRef { + return computed(() => { + const _getKey = getKey?.value + if (isString(_getKey)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (item: any) => { + const key = item[_getKey] + if (__DEV__ && key === undefined) { + Logger.warn('cdk/dnd', 'Each item in dataSource should have a unique `key` prop.') + } + return key + } + } + return _getKey! + }) +} diff --git a/packages/cdk/dnd/src/consts.ts b/packages/cdk/dnd/src/consts.ts new file mode 100644 index 000000000..dea184447 --- /dev/null +++ b/packages/cdk/dnd/src/consts.ts @@ -0,0 +1,15 @@ +/** + * @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 + */ + +export const dndContextPropertyKey = '__idux_cdk_dnd_context_key__' +export const defaultTreeIndent = 32 +export const defaultSortableStrategy = 'list' as const +export const defaultSortableEffect = 'indicator' as const +export const defaultChildrenKey = 'children' +export const defaultMovableStrategy = 'transform' as const +export const defaultMovableMode = 'afterDrop' as const +export const defaultMovableAllowedAxis = 'all' as const diff --git a/packages/cdk/dnd/src/indicator/DndBoxIndicator.tsx b/packages/cdk/dnd/src/indicator/DndBoxIndicator.tsx new file mode 100644 index 000000000..1a51b59dc --- /dev/null +++ b/packages/cdk/dnd/src/indicator/DndBoxIndicator.tsx @@ -0,0 +1,33 @@ +/** + * @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 { DndBoxIndicatorProps } from '../types' +import type { FunctionalComponent } from 'vue' + +import { isNil } from 'lodash-es' + +import { convertCssPixel } from '@idux/cdk/utils' + +const DndBoxIndicator: FunctionalComponent = ({ edge, gap, isFirst, isLast }) => { + const prefixCls = 'cdk-dnd-box-indicator' + const classes = { + [prefixCls]: true, + [`${prefixCls}-${edge}`]: true, + [`${prefixCls}-first`]: !!isFirst, + [`${prefixCls}-last`]: !!isLast, + } + + const style = !isNil(gap) + ? { + '--cdk-inner-line-gap': convertCssPixel(gap), + } + : undefined + + return
+} + +export default DndBoxIndicator diff --git a/packages/cdk/dnd/src/indicator/DndTreeIndicator.tsx b/packages/cdk/dnd/src/indicator/DndTreeIndicator.tsx new file mode 100644 index 000000000..2075c6141 --- /dev/null +++ b/packages/cdk/dnd/src/indicator/DndTreeIndicator.tsx @@ -0,0 +1,42 @@ +/** + * @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 { DndTreeIndicatorProps, TreeIndicatorInstruction } from '../types' +import type { FunctionalComponent } from 'vue' + +import { convertCssPixel } from '@idux/cdk/utils' + +const DndTreeIndicator: FunctionalComponent = ({ instruction, isFirst, isLast }) => { + const prefixCls = 'cdk-dnd-tree-indicator' + const classes = { + [prefixCls]: true, + [`${prefixCls}-${instruction.type}`]: true, + [`${prefixCls}-first`]: !!isFirst, + [`${prefixCls}-last`]: !!isLast, + } + const style = { + '--cdk-inner-indent': getIndent(instruction), + } + + return
+} + +function getIndent(instruction: TreeIndicatorInstruction) { + if (instruction.type === 'instruction-blocked') { + return + } + + if (instruction.type === 'reparent') { + return convertCssPixel(instruction.desiredLevel * instruction.indentPerLevel) + } + + return convertCssPixel(instruction.currentLevel * instruction.indentPerLevel) +} + +DndTreeIndicator.displayName = 'DndTreeIndicator' + +export default DndTreeIndicator diff --git a/packages/cdk/dnd/src/movable/DndMovable.tsx b/packages/cdk/dnd/src/movable/DndMovable.tsx new file mode 100644 index 000000000..b95c5256b --- /dev/null +++ b/packages/cdk/dnd/src/movable/DndMovable.tsx @@ -0,0 +1,140 @@ +/** + * @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 { Teleport, computed, defineComponent, provide, shallowRef, toRef } from 'vue' + +import { isNil } from 'lodash-es' + +import { callEmit, convertCssPixel, useState } from '@idux/cdk/utils' + +import { useDndMovable } from '../composables/useDndMovable' +import { defaultMovableStrategy } from '../consts' +import { dndMovableToken } from '../tokens' +import { dndMovableProps } from '../types' + +export default defineComponent({ + name: 'DndMovable', + props: dndMovableProps, + setup(props, { slots, expose }) { + const [previewState, setPreviewState] = useState<{ container: HTMLElement } | null>(null) + + const elementRef = shallowRef() + const dragHandleRef = shallowRef() + + const mergedDragHandle = computed(() => props.dragHandle ?? dragHandleRef.value) + + const setDragHandle = (dragHandle: HTMLElement | undefined) => { + dragHandleRef.value = dragHandle + } + + const previewMount = (state: { container: HTMLElement }) => { + setPreviewState(state) + } + const previewUnmount = () => { + setPreviewState(null) + } + + const mergedStrategy = computed(() => props.strategy ?? defaultMovableStrategy) + const mergedPreview = computed(() => { + if (isNil(props.preview) || props.preview === 'native') { + return true + } + + if (props.preview === false) { + return false + } + + if (props.preview === true) { + return { + mount: previewMount, + unmount: previewUnmount, + } + } + + return { + offset: props.preview.offset, + mount: previewMount, + unmount: previewUnmount, + } + }) + + provide(dndMovableToken, { + setDragHandle, + }) + + const { init, position, offset } = useDndMovable({ + mode: toRef(props, 'mode'), + strategy: mergedStrategy, + canDrag: toRef(props, 'canDrag'), + boundary: toRef(props, 'boundary'), + draggableElement: elementRef, + dropTargets: toRef(props, 'dropTargets'), + dragHandle: mergedDragHandle, + preview: mergedPreview, + allowedAxis: toRef(props, 'allowedAxis'), + onDragStart(args) { + callEmit(props.onDragStart, args) + }, + onDrag(args) { + callEmit(props.onDrag, args) + }, + onDragEnter(args) { + callEmit(props.onDragEnter, args) + }, + onDragLeave(args) { + callEmit(props.onDragLeave, args) + }, + onDrop(args) { + callEmit(props.onDrop, args) + }, + onDropOfTarget(args) { + callEmit(props.onDropOfTarget, args) + }, + }) + + expose({ + init, + }) + + const draggableElementStyle = computed(() => { + const strategy = mergedStrategy.value + + if (strategy === 'fixed' || strategy === 'absolute') { + return { + position: strategy, + top: convertCssPixel(position.value.y), + left: convertCssPixel(position.value.x), + } + } + + if (offset.value.x === 0 && offset.value.y === 0) { + return + } + + return { + transform: `translate(${convertCssPixel(offset.value.x)}, ${convertCssPixel(offset.value.y)})`, + } + }) + + return () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const Tag = (props.tag as any) ?? 'div' + const preview = previewState.value + + const contentNodes = [ + slots.default?.(), + preview && {slots.preview?.(preview)}, + ] + + return ( + + {contentNodes} + + ) + } + }, +}) diff --git a/packages/cdk/dnd/src/movable/DndMovableHandle.tsx b/packages/cdk/dnd/src/movable/DndMovableHandle.tsx new file mode 100644 index 000000000..e217ee4a3 --- /dev/null +++ b/packages/cdk/dnd/src/movable/DndMovableHandle.tsx @@ -0,0 +1,31 @@ +/** + * @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 { defineComponent, inject, onMounted, onUnmounted, shallowRef } from 'vue' + +import { dndMovableToken } from '../tokens' + +export default defineComponent({ + setup(_, { slots }) { + const { setDragHandle } = inject(dndMovableToken)! + + const dragHandleRef = shallowRef() + + onMounted(() => { + setDragHandle(dragHandleRef.value) + }) + onUnmounted(() => { + setDragHandle(undefined) + }) + + return () => ( +
+ {slots.default?.()} +
+ ) + }, +}) diff --git a/packages/cdk/dnd/src/sortable/DndSortable.tsx b/packages/cdk/dnd/src/sortable/DndSortable.tsx new file mode 100644 index 000000000..26f21e98a --- /dev/null +++ b/packages/cdk/dnd/src/sortable/DndSortable.tsx @@ -0,0 +1,168 @@ +/** + * @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 { Teleport, computed, defineComponent, nextTick, provide, toRef, watch } from 'vue' + +import { isNil } from 'lodash-es' + +import { type VKey, callEmit, useState } from '@idux/cdk/utils' + +import { useDndSortable } from '../composables/useDndSortable' +import { useGetKey } from '../composables/useGetKey' +import { defaultSortableEffect, defaultSortableStrategy } from '../consts' +import { CDK_DND_SORTABLE_TOKEN, dndSortableToken } from '../tokens' +import { + type CanDragOptions, + type CanDropOptions, + type DndSortableData, + type DndSortableIsStickyOptions, + type DndSortablePreviewState, + type DndSortableReorderInfo, + dndSortableProps, +} from '../types' +import { getMergedCanFn } from '../utils' + +const lastMovedKeyMostWaitTime = 3000 + +export default defineComponent({ + name: 'DndSortable', + props: dndSortableProps, + setup(props, { slots }) { + const dataSource = toRef(props, 'dataSource') + const direction = toRef(props, 'direction') + const getKey = toRef(props, 'getKey') + const childrenKey = toRef(props, 'childrenKey') + const treeIndent = toRef(props, 'treeIndent') + const mergedGetKey = useGetKey(getKey) + const mergedStrategy = computed(() => props.strategy ?? defaultSortableStrategy) + const mergedEffect = computed(() => props.effect ?? defaultSortableEffect) + + const [lastMovedKey, setLastMovedKey] = useState(undefined) + const [previewState, setPreviewState] = useState(null) + + const mergedCanDrag = getMergedCanFn(() => props.canDrag) + const mergedCanDrop = getMergedCanFn(() => props.canDrop) + const mergedIsSticky = getMergedCanFn(() => props.isSticky) + + let tempLastMovedKey: VKey | undefined + let tempLastMovedKeySetTmr: number + + watch( + () => props.dataSource, + () => { + if (!isNil(tempLastMovedKey)) { + clearTimeout(tempLastMovedKeySetTmr) + setLastMovedKey(tempLastMovedKey) + tempLastMovedKey = undefined + nextTick(() => { + setLastMovedKey(undefined) + }) + } + }, + { deep: true }, + ) + + const onSortReorder = (reorderInfo: DndSortableReorderInfo) => { + const { sourceKey } = reorderInfo + clearTimeout(tempLastMovedKeySetTmr) + tempLastMovedKey = sourceKey + tempLastMovedKeySetTmr = setTimeout(() => { + tempLastMovedKey = undefined + }, lastMovedKeyMostWaitTime) + + callEmit(props.onSortReorder, reorderInfo) + } + const onSortChange = (newDataSource: DndSortableData[], oldDataSource: DndSortableData[]) => { + callEmit(props.onSortChange, newDataSource, oldDataSource) + } + + const previewMount = (state: DndSortablePreviewState) => { + setPreviewState(state) + } + const previewUnmount = () => { + setPreviewState(null) + } + + const mergedPreview = computed(() => { + if (isNil(props.preview) || props.preview === 'native') { + return true + } + + if (props.preview === false) { + return false + } + + if (props.preview === true) { + return { + mount: previewMount, + unmount: previewUnmount, + } + } + + return { + offset: props.preview.offset, + mount: previewMount, + unmount: previewUnmount, + } + }) + + const dndSortableContext = useDndSortable({ + strategy: mergedStrategy, + dataSource, + direction, + getKey: mergedGetKey, + childrenKey, + treeIndent, + preview: mergedPreview, + isTreeItemExpanded: props.isTreeItemExpanded, + canDrag: mergedCanDrag, + canDrop: mergedCanDrop, + isSticky: mergedIsSticky, + onDragStart(args) { + callEmit(props.onDragStart, args) + }, + onDrag(args) { + callEmit(props.onDrag, args) + }, + onDragEnter(args) { + callEmit(props.onDragEnter, args) + }, + onDragLeave(args) { + callEmit(props.onDragLeave, args) + }, + onDrop(args) { + callEmit(props.onDrop, args) + }, + onSortChange, + onSortReorder, + }) + + provide(dndSortableToken, { + ...dndSortableContext, + lastMovedKey, + mergedStrategy, + mergedEffect, + }) + provide(CDK_DND_SORTABLE_TOKEN, { + draggingState: dndSortableContext.draggingState, + draggingOverState: dndSortableContext.draggingOverState, + }) + + return () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const Tag = props.tag as any + const preview = previewState.value + + const contentNodes = [ + slots.default?.(), + preview && {slots.preview?.(preview)}, + ] + + return Tag ? {contentNodes} : <>{contentNodes} + } + }, +}) diff --git a/packages/cdk/dnd/src/sortable/DndSortableHandle.tsx b/packages/cdk/dnd/src/sortable/DndSortableHandle.tsx new file mode 100644 index 000000000..6760522e7 --- /dev/null +++ b/packages/cdk/dnd/src/sortable/DndSortableHandle.tsx @@ -0,0 +1,31 @@ +/** + * @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 { defineComponent, inject, onMounted, onUnmounted, shallowRef } from 'vue' + +import { dndSortableItemToken } from '../tokens' + +export default defineComponent({ + setup(_, { slots }) { + const { setDragHandle } = inject(dndSortableItemToken)! + + const dragHandleRef = shallowRef() + + onMounted(() => { + setDragHandle(dragHandleRef.value) + }) + onUnmounted(() => { + setDragHandle(undefined) + }) + + return () => ( +
+ {slots.default?.()} +
+ ) + }, +}) diff --git a/packages/cdk/dnd/src/sortable/DndSortableItem.tsx b/packages/cdk/dnd/src/sortable/DndSortableItem.tsx new file mode 100644 index 000000000..15531c9cf --- /dev/null +++ b/packages/cdk/dnd/src/sortable/DndSortableItem.tsx @@ -0,0 +1,219 @@ +/** + * @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 { computed, defineComponent, inject, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue' + +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine' + +import { VKey, callEmit } from '@idux/cdk/utils' + +import DndBoxIndicator from '../indicator/DndBoxIndicator' +import DndTreeIndicator from '../indicator/DndTreeIndicator' +import { dndSortableItemToken, dndSortableToken } from '../tokens' +import { + type CanDragOptions, + type CanDropOptions, + type DndSortableIsStickyOptions, + type DndSortableOnDragArgs, + type DndSortableOnDragEnterArgs, + type DndSortableOnDragLeaveArgs, + type DndSortableOnDragStartArgs, + type DndSortableOnDropArgs, + type ListDraggingOverState, + type TreeDraggingOverState, + dndSortableItemProps, +} from '../types' +import { callEventHandler, callEventHandlerWithSource, getMergedCanFn, triggerPostMoveFlash } from '../utils' + +export default defineComponent({ + name: 'DndSortableItem', + props: dndSortableItemProps, + setup(props, { slots }) { + const { + mergedStrategy, + mergedEffect, + draggingOverState, + draggingState, + lastMovedKey, + registerDraggable, + registerDropTarget, + } = inject(dndSortableToken)! + + const listItemRef = shallowRef() + const dragHandleRef = shallowRef() + + const mergedCanDrag = getMergedCanFn(() => props.canDrag) + const mergedCanDrop = getMergedCanFn(() => props.canDrop) + const mergedIsSticky = getMergedCanFn(() => props.isSticky) + + const isDragging = computed(() => draggingState.value?.key === props.itemKey) + const resolvedDraggingOverState = computed(() => + draggingOverState.value?.key === props.itemKey ? draggingOverState.value : null, + ) + + const classes = computed(() => { + const prefixCls = 'cdk-dnd-sortable-item' + + return { + [prefixCls]: true, + [`${prefixCls}-dragging`]: isDragging.value, + [`${prefixCls}-dragging-over`]: !!resolvedDraggingOverState.value, + } + }) + + const setDragHandle = (dragHandle: HTMLElement | undefined) => { + dragHandleRef.value = dragHandle + } + + const onDragStart = (args: DndSortableOnDragStartArgs) => { + callEmit(props.onDragStart, args) + } + const onDrag = (args: DndSortableOnDragArgs) => { + callEmit(props.onDrag, args) + } + const onDragEnter = (args: DndSortableOnDragEnterArgs) => { + callEmit(props.onDragEnter, args) + } + const onDragLeave = (args: DndSortableOnDragLeaveArgs) => { + callEmit(props.onDragEnter, args) + } + const onDrop = (args: DndSortableOnDropArgs) => { + callEmit(props.onDrop, args) + } + + let cleanUp: (() => void) | undefined + + const init = () => { + if (!listItemRef.value) { + return + } + + cleanUp?.() + cleanUp = combine( + registerDraggable({ + key: props.itemKey, + element: listItemRef.value, + dragHandle: dragHandleRef.value, + canDrag: mergedCanDrag, + onDragStart(args) { + const { source, location } = args + callEventHandler(onDragStart, source.data, location) + }, + onDrag(args) { + const { source, location } = args + callEventHandler(onDrag, source.data, location) + }, + }), + registerDropTarget({ + key: props.itemKey, + element: listItemRef.value, + canDrop: mergedCanDrop, + isSticky: mergedIsSticky, + onDragEnter(args) { + const { self, source, location } = args + callEventHandlerWithSource(onDragEnter, self.data, source.data, location) + }, + onDragLeave(args) { + const { self, source, location } = args + callEventHandlerWithSource(onDragLeave, self.data, source.data, location) + }, + onDrop(args) { + const { self, source, location } = args + callEventHandlerWithSource(onDrop, self.data, source.data, location) + }, + }), + ) + } + const destroy = () => { + cleanUp?.() + } + + provide(dndSortableItemToken, { + setDragHandle, + }) + + watch([dragHandleRef, () => props.itemKey], init) + watch(lastMovedKey, key => { + if (key === props.itemKey && listItemRef.value) { + triggerPostMoveFlash(listItemRef.value) + } + }) + + onMounted(init) + onUnmounted(destroy) + + const renderIndicator = () => { + if (mergedEffect.value !== 'indicator') { + return + } + + const state = resolvedDraggingOverState.value + if (!state) { + return + } + + if (mergedStrategy.value === 'list') { + return renderEdgeIndicator(state as ListDraggingOverState) + } + + if (mergedStrategy.value === 'tree') { + return renderTreeIndicator(state as TreeDraggingOverState, props.itemKey) + } + + return + } + + return () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const Tag = (props.tag as any) ?? 'div' + + return ( + + {slots.default?.({ + isDragging: isDragging.value, + draggingState: draggingOverState.value, + draggingOverState: resolvedDraggingOverState.value, + })} + {renderIndicator()} + + ) + } + }, +}) + +function renderEdgeIndicator(state: ListDraggingOverState) { + const edge = state.instruction?.edge + if (!edge) { + return + } + + return +} +function renderTreeIndicator(state: TreeDraggingOverState, key: VKey) { + const instruction = state.instruction + if (!instruction || instruction.type === 'instruction-blocked') { + return + } + + if (instruction.parent && instruction.parent.key === key) { + return ( + + ) + } + + if (state.key === key) { + return + } + + return +} diff --git a/packages/cdk/dnd/src/tokens/index.ts b/packages/cdk/dnd/src/tokens/index.ts new file mode 100644 index 000000000..18c0c92e6 --- /dev/null +++ b/packages/cdk/dnd/src/tokens/index.ts @@ -0,0 +1,9 @@ +/** + * @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 + */ + +export * from './movable' +export * from './sortable' diff --git a/packages/cdk/dnd/src/tokens/movable.ts b/packages/cdk/dnd/src/tokens/movable.ts new file mode 100644 index 000000000..f3856d204 --- /dev/null +++ b/packages/cdk/dnd/src/tokens/movable.ts @@ -0,0 +1,14 @@ +/** + * @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 { InjectionKey } from 'vue' + +interface DndMovableCompContext { + setDragHandle: (dragHandle: HTMLElement | undefined) => void +} + +export const dndMovableToken: InjectionKey = Symbol('dndMovableToken') diff --git a/packages/cdk/dnd/src/tokens/sortable.ts b/packages/cdk/dnd/src/tokens/sortable.ts new file mode 100644 index 000000000..8eef4ff15 --- /dev/null +++ b/packages/cdk/dnd/src/tokens/sortable.ts @@ -0,0 +1,29 @@ +/** + * @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 { DndSortableContext } from '../composables/useDndSortable' +import type { DndSortableReorderEffect, DndSortableStrategy } from '../types' +import type { VKey } from '@idux/cdk/utils' +import type { ComputedRef, InjectionKey } from 'vue' + +interface DndSortableCompContext extends DndSortableContext { + lastMovedKey: ComputedRef + mergedStrategy: ComputedRef + mergedEffect: ComputedRef +} + +interface DndSortableItemContext { + setDragHandle: (dragHandle: HTMLElement | undefined) => void +} + +interface PublicDndSortableContext extends Pick {} + +export const dndSortableToken: InjectionKey = Symbol('dndSortableToken') +export const dndSortableItemToken: InjectionKey = Symbol('dndSortableItemToken') + +// public token +export const CDK_DND_SORTABLE_TOKEN: InjectionKey = Symbol('CDK_DND_SORTABLE_TOKEN') diff --git a/packages/cdk/dnd/src/types/autoScroll.ts b/packages/cdk/dnd/src/types/autoScroll.ts new file mode 100644 index 000000000..bd753c1db --- /dev/null +++ b/packages/cdk/dnd/src/types/autoScroll.ts @@ -0,0 +1,14 @@ +/** + * @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 { Axis } from './dnd' + +export interface AutoScrollOptions { + canScroll: boolean + maxScrollSpeed?: 'standard' | 'fast' + allowedAxis?: Axis +} diff --git a/packages/cdk/dnd/src/types/comp/index.ts b/packages/cdk/dnd/src/types/comp/index.ts new file mode 100644 index 000000000..10673c1c2 --- /dev/null +++ b/packages/cdk/dnd/src/types/comp/index.ts @@ -0,0 +1,10 @@ +/** + * @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 + */ + +export * from './sortable' +export * from './movable' +export * from './indicator' diff --git a/packages/cdk/dnd/src/types/comp/indicator.ts b/packages/cdk/dnd/src/types/comp/indicator.ts new file mode 100644 index 000000000..c6650d55d --- /dev/null +++ b/packages/cdk/dnd/src/types/comp/indicator.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 { ListInstruction, TreeInstruction } from '../sortable' +import type { FunctionalComponent, HTMLAttributes, HtmlHTMLAttributes } from 'vue' + +export interface DndBoxIndicatorProps { + edge: ListInstruction['edge'] + gap?: number | string + isFirst?: boolean + isLast?: boolean +} +export type DndBoxIndicatorComponent = FunctionalComponent< + Omit & DndBoxIndicatorProps +> + +export type TreeIndicatorInstruction = + | TreeInstruction + | { type: 'mark-parent'; currentLevel: number; indentPerLevel: number } + +export interface DndTreeIndicatorProps { + instruction: TreeIndicatorInstruction + isFirst?: boolean + isLast?: boolean +} +export type DndTreeIndicatorComponent = FunctionalComponent< + Omit & DndTreeIndicatorProps +> diff --git a/packages/cdk/dnd/src/types/comp/movable.ts b/packages/cdk/dnd/src/types/comp/movable.ts new file mode 100644 index 000000000..01b0ab801 --- /dev/null +++ b/packages/cdk/dnd/src/types/comp/movable.ts @@ -0,0 +1,50 @@ +/** + * @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 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { Axis } from '../dnd' +import type { DndMovableMode, DndMovableStrategy } from '../movable' +import type { + ElementDropTargetEventBasePayload, + ElementEventBasePayload, +} from '@atlaskit/pragmatic-drag-and-drop/element/adapter' +import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, MaybeElement } from '@idux/cdk/utils' +import type { Component, DefineComponent, FunctionalComponent, HTMLAttributes, PropType } from 'vue' + +export const dndMovableProps = { + tag: { type: [String, Object, Function] as PropType, default: 'div' }, + allowedAxis: { type: String as PropType, default: 'all' }, + mode: { type: String as PropType, default: undefined }, + strategy: { type: String as PropType, default: undefined }, + preview: { + type: [Boolean, String, Object] as PropType, + default: undefined, + }, + canDrag: { type: Boolean, default: true }, + dragHandle: { type: Object as PropType, default: undefined }, + dropTargets: { type: Array as PropType<(HTMLElement | undefined)[]>, default: undefined }, + boundary: { type: [String, Object] as PropType<'parent' | 'viewport' | MaybeElement>, default: undefined }, + + onDragStart: [Function, Array] as PropType void>>, + onDrag: [Function, Array] as PropType void>>, + onDragEnter: [Function, Array] as PropType void>>, + onDragLeave: [Function, Array] as PropType void>>, + onDrop: [Function, Array] as PropType void>>, + onDropOfTarget: [Function, Array] as PropType void>>, +} as const + +export interface DndMovableBindings { + init: () => void +} +export type DndMovableProps = ExtractInnerPropTypes +export type DndMovablePublicProps = ExtractPublicPropTypes +export type DndMovableComponent = DefineComponent< + Omit & DndMovablePublicProps, + DndMovableBindings +> +export type DndMovableInstance = InstanceType> diff --git a/packages/cdk/dnd/src/types/comp/sortable.ts b/packages/cdk/dnd/src/types/comp/sortable.ts new file mode 100644 index 000000000..569c65b12 --- /dev/null +++ b/packages/cdk/dnd/src/types/comp/sortable.ts @@ -0,0 +1,95 @@ +/** + * @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 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { + CanDragOptions, + CanDropOptions, + DndSortableData, + DndSortableDirection, + DndSortableIsStickyOptions, + DndSortableOnDragArgs, + DndSortableOnDragEnterArgs, + DndSortableOnDragLeaveArgs, + DndSortableOnDragStartArgs, + DndSortableOnDropArgs, + DndSortableReorderInfo, + DndSortableStrategy, + GetKey, +} from '../sortable' +import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, VKey } from '@idux/cdk/utils' +import type { Component, DefineComponent, FunctionalComponent, HTMLAttributes, PropType } from 'vue' + +export type DndSortableReorderEffect = 'indicator' | 'none' + +export const dndSortableProps = { + tag: [String, Object, Function] as PropType, + strategy: { type: String as PropType, default: undefined }, + dataSource: { type: Array as PropType, default: () => [] }, + direction: { type: String as PropType, default: 'vertical' }, + effect: { type: String as PropType, default: 'indicator' }, + preview: { + type: [Boolean, String, Object] as PropType, + default: undefined, + }, + getKey: { type: [Function, String] as PropType, default: 'key' }, + childrenKey: String, + treeIndent: { type: Number, default: undefined }, + + isSticky: { + type: [Boolean, Function] as PropType boolean)>, + default: undefined, + }, + isTreeItemExpanded: Function as PropType<(key: VKey, data: DndSortableData) => void>, + canDrag: { type: [Boolean, Function] as PropType boolean)>, default: true }, + canDrop: { type: [Boolean, Function] as PropType boolean)>, default: true }, + + onDragStart: [Function, Array] as PropType void>>, + onDrag: [Function, Array] as PropType void>>, + onDragEnter: [Function, Array] as PropType void>>, + onDragLeave: [Function, Array] as PropType void>>, + onDrop: [Function, Array] as PropType void>>, + + onSortReorder: [Function, Array] as PropType void>>, + onSortChange: [Function, Array] as PropType void>>, +} as const + +export const dndSortableItemProps = { + tag: { + type: [String, Object, Function] as PropType, + default: 'div', + }, + itemKey: { type: [String, Number, Symbol] as PropType, required: true }, + direction: String as PropType, + isSticky: { + type: [Boolean, Function] as PropType boolean)>, + default: undefined, + }, + canDrag: { type: [Boolean, Function] as PropType boolean)>, default: true }, + canDrop: { type: [Boolean, Function] as PropType boolean)>, default: true }, + + onDragStart: [Function, Array] as PropType void>>, + onDrag: [Function, Array] as PropType void>>, + onDragEnter: [Function, Array] as PropType void>>, + onDragLeave: [Function, Array] as PropType void>>, + onDrop: [Function, Array] as PropType void>>, +} as const + +export type DndSortableProps = ExtractInnerPropTypes +export type DndSortablePublicProps = ExtractPublicPropTypes +export type DndSortableComponent = DefineComponent< + Omit & DndSortablePublicProps +> +export type DndSortableInstance = InstanceType> + +export type DndSortableItemProps = ExtractInnerPropTypes +export type DndSortableItemPublicProps = ExtractPublicPropTypes +export type DndSortableItemComponent = DefineComponent< + Omit & DndSortableItemPublicProps +> +export type DndSortableItemInstance = InstanceType> diff --git a/packages/cdk/dnd/src/types/dnd.ts b/packages/cdk/dnd/src/types/dnd.ts new file mode 100644 index 000000000..27a322936 --- /dev/null +++ b/packages/cdk/dnd/src/types/dnd.ts @@ -0,0 +1,35 @@ +/** + * @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 { + draggable, + dropTargetForElements, + monitorForElements, +} from '@atlaskit/pragmatic-drag-and-drop/element/adapter' + +import { dndContextPropertyKey } from '../consts' + +export type DraggableOptions = Parameters[0] +export type DropTargetOptions = Parameters[0] +export type MonitorOptions = Parameters[0] + +export type ContextData = { + [key in typeof dndContextPropertyKey]: number[] +} + +export type Axis = 'vertical' | 'horizontal' | 'all' + +export interface DndOptions { + monitor?: MonitorOptions | boolean + onDrag?: MonitorOptions['onDrag'] + onDragOfTarget?: DropTargetOptions['onDrag'] + onDragStart?: MonitorOptions['onDragStart'] + onDragEnter?: DropTargetOptions['onDragEnter'] + onDragLeave?: DropTargetOptions['onDragLeave'] + onDrop?: MonitorOptions['onDrop'] + onDropOfTarget?: DropTargetOptions['onDrop'] +} diff --git a/packages/cdk/dnd/src/types/index.ts b/packages/cdk/dnd/src/types/index.ts new file mode 100644 index 000000000..0837504d4 --- /dev/null +++ b/packages/cdk/dnd/src/types/index.ts @@ -0,0 +1,12 @@ +/** + * @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 + */ + +export * from './autoScroll' +export * from './dnd' +export * from './sortable' +export * from './comp' +export * from './movable' diff --git a/packages/cdk/dnd/src/types/indicator.ts b/packages/cdk/dnd/src/types/indicator.ts new file mode 100644 index 000000000..6a61f651f --- /dev/null +++ b/packages/cdk/dnd/src/types/indicator.ts @@ -0,0 +1,17 @@ +/** + * @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 { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/types' +import type { FunctionalComponent, HTMLAttributes } from 'vue' + +export interface DndBoxIndicatorProps { + edge: Edge + gap?: number | string +} +export type DndBoxIndicatorComponent = FunctionalComponent< + Omit & DndBoxIndicatorProps +> diff --git a/packages/cdk/dnd/src/types/movable.ts b/packages/cdk/dnd/src/types/movable.ts new file mode 100644 index 000000000..35049d0cf --- /dev/null +++ b/packages/cdk/dnd/src/types/movable.ts @@ -0,0 +1,65 @@ +/** + * @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 { Axis, DndOptions } from './dnd' +import type { MaybeElement, MaybeElementRef, MaybeRef } from '@idux/cdk/utils' +import type { ComputedRef } from 'vue' + +export type DndMovableBoundaryType = 'parent' | 'viewport' | MaybeElement | null +export type DndMovableStrategy = 'fixed' | 'absolute' | 'transform' +export type DndMovableMode = 'immediate' | 'afterDrop' + +export interface Position { + x: number + y: number +} + +export interface Rect { + x: number + y: number + width: number + height: number +} + +export interface ResolvedBoundary { + left: number + right: number + top: number + bottom: number +} + +export type DndMovablePreviewOptions = + | boolean + | { + offset?: { x: number; y: number } + mount?: (args: { container: HTMLElement }) => void + unmount?: (args: { container: HTMLElement }) => void + } + +export interface DndMovableOptions extends Omit { + mode?: MaybeRef + strategy?: MaybeRef + canDrag?: MaybeRef + draggableElement: MaybeElementRef + dropTargets?: MaybeRef + boundary?: MaybeRef + dragHandle?: MaybeElementRef + allowedAxis?: MaybeRef + preview?: MaybeRef +} + +export interface ResolvedMovableOptions extends Omit { + mode: ComputedRef + strategy: ComputedRef + canDrag: ComputedRef + draggableElement: ComputedRef + dropTargets: ComputedRef + boundary: ComputedRef + dragHandle: ComputedRef + allowedAxis: ComputedRef + preview: ComputedRef +} diff --git a/packages/cdk/dnd/src/types/sortable.ts b/packages/cdk/dnd/src/types/sortable.ts new file mode 100644 index 000000000..52f981568 --- /dev/null +++ b/packages/cdk/dnd/src/types/sortable.ts @@ -0,0 +1,166 @@ +/** + * @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 { DndOptions, DraggableOptions, DropTargetOptions } from './dnd' +import type { + ElementDropTargetGetFeedbackArgs, + ElementGetFeedbackArgs, +} from '@atlaskit/pragmatic-drag-and-drop/element/adapter' +import type { DragLocationHistory } from '@atlaskit/pragmatic-drag-and-drop/types' +import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item' +import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/types' +import type { VKey } from '@idux/cdk/utils' +import type { Ref } from 'vue' + +export interface CanDragOptions { + sourceKey: VKey + sourceIndex: number | undefined + sourceData: DndSortableData | undefined +} +export interface CanDropOptions extends Omit { + sourceKey: VKey | undefined + targetKey: VKey + targetIndex: number | undefined + targetData: DndSortableData | undefined +} + +export type GetKey = (item: unknown) => VKey +export type DndSortableDirection = 'vertical' | 'horizontal' +export type DndSortableStrategy = 'list' | 'tree' +export type DndSortablePreviewOptions = + | boolean + | { + offset?: { x: number; y: number } + mount?: (state: DndSortablePreviewState) => void + unmount?: (state: DndSortablePreviewState) => void + } + +export type DndSortableDraggableOptions = Omit & { + key: VKey + preview?: DndSortablePreviewOptions + canDrag?: (args: ElementGetFeedbackArgs & CanDragOptions) => boolean | undefined +} +export type DndSortableDropTargetOptions = Omit & { + key: VKey + direction?: DndSortableDirection + canDrop?: (args: ElementDropTargetGetFeedbackArgs & CanDropOptions) => boolean | undefined + isSticky?: (options: DndSortableIsStickyOptions) => boolean | undefined +} + +export type DndSortableData> = { + [c in C]?: DndSortableData[] +} & V + +export type DndSortableInnerData< + C extends keyof V = never, + V extends object = Record, +> = DndSortableData & { + _data_index: number +} + +export interface DndSortableTransferData { + key: VKey + listData: DndSortableInnerData + listDataIndex: number + direction?: DndSortableDirection + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string | symbol]: any +} + +export interface BaseDndSortableEventArgs { + location: DragLocationHistory + key: VKey + data: DndSortableData +} +export interface DndSortableEvetWithSourceArgs extends BaseDndSortableEventArgs { + sourceKey: VKey + sourceData: DndSortableData +} +export interface DndSortableOnDragArgs extends BaseDndSortableEventArgs {} +export interface DndSortableOnDragStartArgs extends BaseDndSortableEventArgs {} +export interface DndSortableOnDragEnterArgs extends DndSortableEvetWithSourceArgs {} +export interface DndSortableOnDragLeaveArgs extends DndSortableEvetWithSourceArgs {} +export interface DndSortableOnDropArgs extends DndSortableEvetWithSourceArgs {} + +export interface DndSortableDraggingState { + key: VKey + data: DndSortableData | undefined + index: number | undefined +} +export interface DndSortableDraggingOverState> { + key: VKey + data: DndSortableData | undefined + index: number | undefined + instruction: Instructions | null +} +export interface DndSortablePreviewState { + key: VKey + data: DndSortableData | undefined + index: number | undefined + container: HTMLElement +} + +export type ListInstruction = { edge: Edge } +export type TreeInstruction = Instruction & { parent?: { key: VKey; level: number } } +export type ListDraggingOverState = DndSortableDraggingOverState +export type TreeDraggingOverState = DndSortableDraggingOverState + +export interface DndSortableIsStickyOptions { + key: VKey + data: DndSortableData | undefined + index: number | undefined +} + +export interface DndSortableReorderInfo { + sourceIndex: number + targetIndex: number + sourceKey: VKey + targetKey: VKey + sourceData: DndSortableData + targetData: DndSortableData + operation: 'insertBefore' | 'insertAfter' | 'insertChild' +} + +export interface DndSortableOptions> { + dataSource: Ref[]> + direction?: Ref + childrenKey?: string | Ref + treeIndent?: number | Ref + getKey?: Ref + preview?: Ref + strategy?: DndSortableStrategy | Ref + canDrag?: (options: CanDragOptions) => boolean | undefined + canDrop?: (options: CanDropOptions) => boolean | undefined + isSticky?: (options: DndSortableIsStickyOptions) => boolean | undefined + isTreeItemExpanded?: (key: VKey, data: DndSortableData) => void + + onDragStart?: (args: DndSortableOnDragStartArgs) => void + onDrag?: (args: DndSortableOnDragArgs) => void + onDragEnter?: (args: DndSortableOnDragEnterArgs) => void + onDragLeave?: (args: DndSortableOnDragLeaveArgs) => void + onDrop?: (args: DndSortableOnDropArgs) => void + onSortReorder?: (info: DndSortableReorderInfo) => void + onSortChange?: (newDataSource: DndSortableData[], oldDataSource: DndSortableData[]) => void +} + +export interface DndSortableStrategyContext { + getData: (key: VKey) => DndSortableInnerData | undefined + getDataIndex: (key: VKey) => number | undefined + getDropTargetData?: ( + args: ElementDropTargetGetFeedbackArgs, + data: DndSortableTransferData, + ) => Record + dataExists: (key: VKey) => boolean + reorder: (args: { sourceData: DndSortableTransferData; targetData: DndSortableTransferData }) => { + reorderInfo: DndSortableReorderInfo + newData: DndSortableData[] + oldData: DndSortableData[] + } | null + eventHandlers: Partial< + Pick + > +} diff --git a/packages/cdk/dnd/src/utils/callEventHandler.ts b/packages/cdk/dnd/src/utils/callEventHandler.ts new file mode 100644 index 000000000..8430cde8f --- /dev/null +++ b/packages/cdk/dnd/src/utils/callEventHandler.ts @@ -0,0 +1,46 @@ +/** + * @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 { BaseDndSortableEventArgs, DndSortableEvetWithSourceArgs } from '../types' +import type { DragLocationHistory } from '@atlaskit/pragmatic-drag-and-drop/types' + +import { isDndSortableTransferData } from './isDndSortableTransferData' + +export function callEventHandler( + handler: ((args: BaseDndSortableEventArgs) => void) | undefined, + transferData: Record, + location: DragLocationHistory, +): void { + if (!isDndSortableTransferData(transferData)) { + return + } + + handler?.({ + key: transferData.key, + data: transferData.listData, + location, + }) +} + +export function callEventHandlerWithSource( + handler: ((args: DndSortableEvetWithSourceArgs) => void) | undefined, + transferData: Record, + sourceTransferData: Record, + location: DragLocationHistory, +): void { + if (!isDndSortableTransferData(transferData) || !isDndSortableTransferData(sourceTransferData)) { + return + } + + handler?.({ + key: transferData.key, + data: transferData.listData, + sourceKey: sourceTransferData.key, + sourceData: sourceTransferData.listData, + location, + }) +} diff --git a/packages/cdk/dnd/src/utils/getMergedCanFn.ts b/packages/cdk/dnd/src/utils/getMergedCanFn.ts new file mode 100644 index 000000000..3a9d141e1 --- /dev/null +++ b/packages/cdk/dnd/src/utils/getMergedCanFn.ts @@ -0,0 +1,25 @@ +/** + * @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 { isBoolean } from 'lodash-es' + +export function getMergedCanFn< + O, + T extends undefined | boolean | ((options: O) => boolean | undefined) = + | undefined + | boolean + | ((options: O) => boolean | undefined), +>(fn: () => T): (options: O) => boolean | undefined { + return (options: O) => { + const v = fn() + if (isBoolean(v)) { + return v + } + + return v?.(options) + } +} diff --git a/packages/cdk/dnd/src/utils/getPositionFromMatrix.ts b/packages/cdk/dnd/src/utils/getPositionFromMatrix.ts new file mode 100644 index 000000000..6b1a0eafa --- /dev/null +++ b/packages/cdk/dnd/src/utils/getPositionFromMatrix.ts @@ -0,0 +1,29 @@ +/** + * @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 { parseSize } from '@idux/cdk/utils' + +const matchReg = /matrix(3d)?\((.*)\)/ + +export function getPositionFromMatrix(matrixStr: string | undefined): { x: number; y: number } | null { + if (!matrixStr) { + return null + } + + const matchRes = matrixStr.match(matchReg) + + if (!matchRes) { + return null + } + + const [, is3d, argStr] = matchRes + + const args = argStr.split(',').map(numStr => parseSize(numStr, 0)) + args.reverse() + + return is3d ? { x: args[3], y: args[2] } : { x: args[1], y: args[0] } +} diff --git a/packages/cdk/dnd/src/utils/index.ts b/packages/cdk/dnd/src/utils/index.ts new file mode 100644 index 000000000..865ebd716 --- /dev/null +++ b/packages/cdk/dnd/src/utils/index.ts @@ -0,0 +1,16 @@ +/** + * @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 + */ + +export * from './reorder' +export * from './triggerPostMoveFlash' +export * from './getMergedCanFn' +export * from './getPositionFromMatrix' +export * from './isDndSortableTransferData' +export * from './isInBoundary' +export * from './callEventHandler' +export * from './keepInAxis' +export * from './keepInBoundary' diff --git a/packages/cdk/dnd/src/utils/isDndSortableTransferData.ts b/packages/cdk/dnd/src/utils/isDndSortableTransferData.ts new file mode 100644 index 000000000..b389df266 --- /dev/null +++ b/packages/cdk/dnd/src/utils/isDndSortableTransferData.ts @@ -0,0 +1,18 @@ +/** + * @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 { DndSortableTransferData } from '../types' + +import { isNumber, isPlainObject, isString, isSymbol } from 'lodash-es' + +export function isDndSortableTransferData(data: Record): data is DndSortableTransferData { + return ( + isNumber(data.listDataIndex) && + (isString(data.key) || isNumber(data.key) || isSymbol(data.key)) && + isPlainObject(data.listData) + ) +} diff --git a/packages/cdk/dnd/src/utils/isInBoundary.ts b/packages/cdk/dnd/src/utils/isInBoundary.ts new file mode 100644 index 000000000..11a1af046 --- /dev/null +++ b/packages/cdk/dnd/src/utils/isInBoundary.ts @@ -0,0 +1,21 @@ +/** + * @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 { Position, Rect, ResolvedBoundary } from '../types' + +export function isInBoundary(rect: Rect, boundary: ResolvedBoundary | undefined, offset: Position): boolean { + if (!boundary) { + return false + } + + const { x, y, width, height } = rect + const { left, right, top, bottom } = boundary + + return ( + right - (x + width) >= offset.x && left - x <= offset.x && bottom - (y + height) >= offset.y && top - y <= offset.y + ) +} diff --git a/packages/cdk/dnd/src/utils/keepInAxis.ts b/packages/cdk/dnd/src/utils/keepInAxis.ts new file mode 100644 index 000000000..02ca1ce8a --- /dev/null +++ b/packages/cdk/dnd/src/utils/keepInAxis.ts @@ -0,0 +1,30 @@ +/** + * @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 { Axis, Position } from '../types' + +export function keepInAxis(allowedAxis: Axis, offset: Position): Position { + if (allowedAxis === 'all') { + return offset + } + + if (allowedAxis === 'horizontal') { + return { + x: offset.x, + y: 0, + } + } + + if (allowedAxis === 'vertical') { + return { + x: 0, + y: offset.y, + } + } + + return offset +} diff --git a/packages/cdk/dnd/src/utils/keepInBoundary.ts b/packages/cdk/dnd/src/utils/keepInBoundary.ts new file mode 100644 index 000000000..e8ef8de06 --- /dev/null +++ b/packages/cdk/dnd/src/utils/keepInBoundary.ts @@ -0,0 +1,25 @@ +/** + * @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 { Position, Rect, ResolvedBoundary } from '../types' + +export function keepInBoundary(rect: Rect, boundary: ResolvedBoundary | undefined, offset: Position): Position { + if (!boundary) { + return { + x: offset.x, + y: offset.y, + } + } + + const { x, y, width, height } = rect + const { left, right, top, bottom } = boundary + + return { + x: Math.floor(offset.x > 0 ? Math.min(right - (x + width), offset.x) : Math.max(left - x, offset.x)), + y: Math.floor(offset.y > 0 ? Math.min(bottom - (y + height), offset.y) : Math.max(top - y, offset.y)), + } +} diff --git a/packages/cdk/dnd/src/utils/reorder.ts b/packages/cdk/dnd/src/utils/reorder.ts new file mode 100644 index 000000000..1ef9eb87c --- /dev/null +++ b/packages/cdk/dnd/src/utils/reorder.ts @@ -0,0 +1,46 @@ +/** + * @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 { DndSortableData, DndSortableReorderInfo, GetKey } from '../types' + +import { reorder } from '@atlaskit/pragmatic-drag-and-drop/reorder' + +import { insertChildTreeItem, insertTreeItemAfter, insertTreeItemBefore, removeTreeItem } from '@idux/cdk/utils' + +export function reorderList(list: DndSortableData[], reorderInfo: DndSortableReorderInfo): DndSortableData[] { + const { sourceIndex, targetIndex, operation } = reorderInfo + const finishIndex = operation === 'insertAfter' ? targetIndex : targetIndex - 1 + + return reorder({ + list, + startIndex: sourceIndex, + finishIndex, + }) +} + +export function reorderTree( + tree: DndSortableData[], + reorderInfo: DndSortableReorderInfo, + childrenKey: C, + getKey: GetKey, +): DndSortableData[] { + const { sourceKey, targetKey, sourceData, operation } = reorderInfo + + let newData = removeTreeItem(tree, sourceKey, childrenKey, getKey) + + if (operation === 'insertAfter') { + newData = insertTreeItemAfter(newData, targetKey, sourceData as DndSortableData, childrenKey, getKey) + } else if (operation === 'insertBefore') { + newData = insertTreeItemBefore(newData, targetKey, sourceData as DndSortableData, childrenKey, getKey) + } else if (operation === 'insertChild') { + newData = insertChildTreeItem(newData, targetKey, sourceData as DndSortableData, childrenKey, getKey) + } else { + newData = [...tree] + } + + return newData +} diff --git a/packages/cdk/dnd/src/utils/triggerPostMoveFlash.ts b/packages/cdk/dnd/src/utils/triggerPostMoveFlash.ts new file mode 100644 index 000000000..fbf1fa469 --- /dev/null +++ b/packages/cdk/dnd/src/utils/triggerPostMoveFlash.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 + */ + +export function triggerPostMoveFlash( + element: HTMLElement, + options?: { + duration?: number + bgColor?: string + easing?: string + }, +): void { + const mergedDuration = options?.duration ?? 200 + const mergedBgColor = options?.bgColor ?? 'rgba(69, 143, 255, 0.3)' + const mergedEasing = options?.easing ?? 'cubic-bezier(0.37, 0, 0.63, 1)' + + element.animate( + [ + { + backgroundColor: mergedBgColor, + }, + {}, + ], + { + duration: mergedDuration, + easing: mergedEasing, + }, + ) +} diff --git a/packages/cdk/dnd/style/index.less b/packages/cdk/dnd/style/index.less new file mode 100644 index 000000000..79593746e --- /dev/null +++ b/packages/cdk/dnd/style/index.less @@ -0,0 +1,139 @@ +@cdk-dnd-indicator-thickness: var(--ix-cdk-dnd-indicator-thickness, var(--ix-line-width-bold, 2px)); +@cdk-dnd-indicator-color: var(--ix-cdk-dnd-indicator-color, var(--ix-color-primary-hover, #458fff)); +@cdk-dnd-indicator-mark-parent-bg-color: var( + --ix-cdk-dnd-indicator-mark-parent-bg-color, + var(--ix-color-primary-hover, #edf1f7) +); +@cdk-dnd-indicator-mark-parent-opacity: var(--ix-cdk-dnd-indicator-mark-parent-opacity, 0.1); +@cdk-dnd-indicator-border-radius: var(--ix-cdk-dnd-indicator-border-radius, var(--ix-border-radius-md, 3px)); +@cdk-dnd-indicator-line-gap: var(--cdk-inner-line-gap, var(--ix-cdk-dnd-indicator-line-gap, 0px)); + +.cdk-dnd { + &-sortable { + &-item { + position: relative; + + &-dragging { + opacity: 0.3; + } + } + &-handle { + cursor: pointer; + } + } + + &-movable { + &-handle { + cursor: move; + } + } + + &-box-indicator { + display: block; + position: absolute; + z-index: 10; + pointer-events: none; + background-color: @cdk-dnd-indicator-color; + + &-top { + .box-indicator-top(); + } + &-bottom { + .box-indicator-bottom(); + } + &-left { + .box-indicator-left(); + } + &-right { + .box-indicator-right(); + } + } + &-tree-indicator { + position: absolute; + top: 0; + bottom: 0; + left: var(--cdk-inner-indent, 0); + right: 0; + pointer-events: none; + + &-reorder-above { + .tree-indicator-line-above(); + } + &-reorder-below, + &-reparent { + .tree-indicator-line-below(); + } + &-make-child { + .tree-indicator-outline(); + } + &-mark-parent { + .tree-indicator-mark-parent(); + } + } +} + +.box-indicator-vertical() { + width: @cdk-dnd-indicator-thickness; + top: 0; + bottom: 0; +} +.box-indicator-horizontal() { + height: @cdk-dnd-indicator-thickness; + right: 0; + left: 0; +} +.box-indicator-top() { + .box-indicator-horizontal(); + + top: calc(-0.5 * (@cdk-dnd-indicator-line-gap + @cdk-dnd-indicator-thickness)); +} +.box-indicator-bottom() { + .box-indicator-horizontal(); + + bottom: calc(-0.5 * (@cdk-dnd-indicator-line-gap + @cdk-dnd-indicator-thickness)); +} +.box-indicator-left() { + .box-indicator-vertical(); + + left: calc(-0.5 * (@cdk-dnd-indicator-line-gap + @cdk-dnd-indicator-thickness)); +} +.box-indicator-right() { + .box-indicator-vertical(); + + right: calc(-0.5 * (@cdk-dnd-indicator-line-gap + @cdk-dnd-indicator-thickness)); +} + +.tree-indicator-line() { + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + display: block; + z-index: 1; + background-color: @cdk-dnd-indicator-color; + height: @cdk-dnd-indicator-thickness; + } +} +.tree-indicator-line-above() { + .tree-indicator-line(); + &::after { + top: calc(-0.5 * @cdk-dnd-indicator-thickness); + } +} +.tree-indicator-line-below() { + .tree-indicator-line(); + &::after { + bottom: calc(-0.5 * @cdk-dnd-indicator-thickness); + } +} +.tree-indicator-outline() { + border: @cdk-dnd-indicator-thickness solid @cdk-dnd-indicator-color; + border-radius: @cdk-dnd-indicator-border-radius; +} +.tree-indicator-mark-parent() { + background-color: @cdk-dnd-indicator-mark-parent-bg-color; + opacity: @cdk-dnd-indicator-mark-parent-opacity; + border-radius: @cdk-dnd-indicator-border-radius; + z-index: 1; +} diff --git a/packages/cdk/drag-drop/docs/Index.zh.md b/packages/cdk/drag-drop/docs/Index.zh.md index 6bb00654a..f18cd7acc 100644 --- a/packages/cdk/drag-drop/docs/Index.zh.md +++ b/packages/cdk/drag-drop/docs/Index.zh.md @@ -3,5 +3,6 @@ category: cdk type: order: 0 title: DragDrop +hidden: true subtitle: 拖放 --- diff --git a/packages/cdk/drag-drop/index.ts b/packages/cdk/drag-drop/index.ts index 20550dded..e394bc847 100644 --- a/packages/cdk/drag-drop/index.ts +++ b/packages/cdk/drag-drop/index.ts @@ -12,23 +12,62 @@ import type { DraggableComponent } from './src/draggable/types' import Draggable from './src/draggable/Draggable' +/** + * @deprecated please use `CdkDndMovable` instead' + */ const CdkDraggable = Draggable as unknown as DraggableComponent +/** + * @deprecated please use `CdkDndMovable` instead' + */ export { CdkDraggable } export type { + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ DnDElement, + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ DnDEvent, + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ DnDPosition, + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ BoundaryType, + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ DnDBackendType, + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ DraggableOptions, + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ DroppableOptions, } from './src/types' +/** + * @deprecated please use `@idux/cdk/dnd` instead' + */ export type { DnDState } from './src/state' export type { + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ DraggableInstance, + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ DraggableComponent, + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ DraggablePublicProps as DraggableProps, } from './src/draggable/types' diff --git a/packages/cdk/drag-drop/src/composables/useDraggable.ts b/packages/cdk/drag-drop/src/composables/useDraggable.ts index ad0261465..451cbede8 100644 --- a/packages/cdk/drag-drop/src/composables/useDraggable.ts +++ b/packages/cdk/drag-drop/src/composables/useDraggable.ts @@ -10,7 +10,7 @@ import type { DnDEventName, DnDEventType, DnDPosition, DraggableOptions } from ' import { type ComputedRef, computed, ref, watch } from 'vue' -import { type MaybeElementRef, convertElement, tryOnScopeDispose, useEventListener } from '@idux/cdk/utils' +import { Logger, type MaybeElementRef, convertElement, tryOnScopeDispose, useEventListener } from '@idux/cdk/utils' import { withBoundary } from './withBoundary' import { withDragFree } from './withDragFree' @@ -37,6 +37,10 @@ export function useDraggable( reset: () => void stop: () => void } { + if (__DEV__) { + Logger.warn('cdk/drag-drop', '@idux/cdk/drag-drop is deprecated, use @idux/cdk/dnd instead') + } + context = initContext(context) const registry = context.registry! diff --git a/packages/cdk/drag-drop/src/composables/useDroppable.ts b/packages/cdk/drag-drop/src/composables/useDroppable.ts index 3a49b9e14..f3374f062 100644 --- a/packages/cdk/drag-drop/src/composables/useDroppable.ts +++ b/packages/cdk/drag-drop/src/composables/useDroppable.ts @@ -9,7 +9,7 @@ import type { DnDEventType, DroppableOptions } from '../types' import { watch } from 'vue' -import { type MaybeElementRef, convertElement, tryOnScopeDispose } from '@idux/cdk/utils' +import { Logger, type MaybeElementRef, convertElement, tryOnScopeDispose } from '@idux/cdk/utils' import { type DnDContext } from './useDragDropContext' import { initContext } from '../utils' @@ -29,6 +29,10 @@ export function useDroppable( connect: (source: MaybeElementRef) => void stop: () => void } { + if (__DEV__) { + Logger.warn('cdk/drag-drop', '@idux/cdk/drag-drop is deprecated, use @idux/cdk/dnd instead') + } + context = initContext(context) const registry = context.registry diff --git a/packages/cdk/drag-drop/src/draggable/Draggable.tsx b/packages/cdk/drag-drop/src/draggable/Draggable.tsx index 83006bd4f..769e61987 100644 --- a/packages/cdk/drag-drop/src/draggable/Draggable.tsx +++ b/packages/cdk/drag-drop/src/draggable/Draggable.tsx @@ -9,7 +9,7 @@ import type { DnDEventType, DnDPosition } from '../types' import { computed, defineComponent, normalizeClass, reactive, ref } from 'vue' -import { callEmit } from '@idux/cdk/utils' +import { Logger, callEmit } from '@idux/cdk/utils' import { draggableProps } from './types' import { useDraggable } from '../composables/useDraggable' @@ -23,6 +23,10 @@ export default defineComponent({ name: 'CdkDraggable', props: draggableProps, setup(props, { slots }) { + if (__DEV__) { + Logger.warn('cdk/drag-drop', '@idux/cdk/drag-drop is deprecated, use @idux/cdk/dnd instead') + } + const elementRef = ref() const onDragStart = (evt: DnDEventType, position?: DnDPosition) => callEmit(props.onDragStart, evt, position) diff --git a/packages/cdk/index.less b/packages/cdk/index.less index 9ff143452..8cedae50d 100644 --- a/packages/cdk/index.less +++ b/packages/cdk/index.less @@ -1,3 +1,4 @@ @import './resize/style/index.less'; @import './scroll/style/index.less'; -@import "./drag-drop/style/index.less"; +@import './drag-drop/style/index.less'; +@import './dnd/style/index.less'; diff --git a/packages/cdk/index.ts b/packages/cdk/index.ts index 86ac0570b..21da1f589 100644 --- a/packages/cdk/index.ts +++ b/packages/cdk/index.ts @@ -8,6 +8,15 @@ import type { App, Directive } from 'vue' import { CdkClickOutside, vClickOutside } from '@idux/cdk/click-outside' +import { + CdkDndBoxIndicator, + CdkDndMovable, + CdkDndMovableHandle, + CdkDndSortable, + CdkDndSortableHandle, + CdkDndSortableItem, + CdkDndTreeIndicator, +} from '@idux/cdk/dnd' import { CdkDraggable } from '@idux/cdk/drag-drop' import { CdkPortal } from '@idux/cdk/portal' import { CdkResizable, CdkResizableHandle, CdkResizeObserver } from '@idux/cdk/resize' @@ -16,7 +25,17 @@ import { version } from '@idux/cdk/version' const components = [ CdkClickOutside, + /** + * @deprecated please use `@idux/cdk/dnd` instead' + */ CdkDraggable, + CdkDndSortable, + CdkDndSortableItem, + CdkDndSortableHandle, + CdkDndBoxIndicator, + CdkDndTreeIndicator, + CdkDndMovable, + CdkDndMovableHandle, CdkPortal, CdkResizable, CdkResizableHandle, @@ -47,6 +66,7 @@ export * from '@idux/cdk/a11y' export * from '@idux/cdk/breakpoint' export * from '@idux/cdk/click-outside' export * from '@idux/cdk/drag-drop' +export * from '@idux/cdk/dnd' export * from '@idux/cdk/clipboard' export * from '@idux/cdk/forms' export * from '@idux/cdk/platform' diff --git a/packages/cdk/package.json b/packages/cdk/package.json index 1b4aa9716..d61c37e3e 100644 --- a/packages/cdk/package.json +++ b/packages/cdk/package.json @@ -40,6 +40,9 @@ "clean": "rimraf dist node_modules" }, "dependencies": { + "@atlaskit/pragmatic-drag-and-drop": "^1.2.1", + "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.4.0", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@floating-ui/dom": "^1.6.7", "lodash-es": "^4.17.21" }, diff --git a/packages/cdk/types.d.ts b/packages/cdk/types.d.ts index c31032e30..783c8fd30 100644 --- a/packages/cdk/types.d.ts +++ b/packages/cdk/types.d.ts @@ -6,6 +6,13 @@ */ import type { ClickOutsideComponent, ClickOutsideDirective } from '@idux/cdk/click-outside' +import type { + DndBoxIndicatorComponent, + DndMovableComponent, + DndSortableComponent, + DndSortableItemComponent, + DndTreeIndicatorComponent, +} from '@idux/cdk/dnd' import type { DraggableComponent } from '@idux/cdk/drag-drop' import type { PortalComponent } from '@idux/cdk/portal' import type { ResizableComponent, ResizableHandleComponent, ResizeObserverComponent } from '@idux/cdk/resize' @@ -14,7 +21,15 @@ import type { VirtualScrollComponent } from '@idux/cdk/scroll' declare module 'vue' { export interface GlobalComponents { CdkClickOutside: ClickOutsideComponent + /** + * @deprecated please use `CdkDndMovable` instead' + */ CdkDraggable: DraggableComponent + CdkDndSortable: DndSortableComponent + CdkDndSortableItem: DndSortableItemComponent + CdkDndMovable: DndMovableComponent + CdkDndBoxIndicator: DndBoxIndicatorComponent + CdkDndTreeIndicator: DndTreeIndicatorComponent CdkPortal: PortalComponent CdkResizable: ResizableComponent CdkResizableHandle: ResizableHandleComponent diff --git a/packages/cdk/utils/src/tree.ts b/packages/cdk/utils/src/tree.ts index f65928446..d72529ce9 100644 --- a/packages/cdk/utils/src/tree.ts +++ b/packages/cdk/utils/src/tree.ts @@ -16,17 +16,17 @@ export type TreeTypeData = { export function traverseTree, C extends keyof V>( data: V[], childrenKey: C, - fn: (item: V, parents: V[]) => void, + fn: (item: V, parents: V[], index: number) => void, traverseStrategy: 'pre' | 'post' = 'pre', ): void { const traverse = (_data: V[], parents: V[]) => { for (let idx = 0; idx < _data.length; idx++) { const item = _data[idx] - traverseStrategy === 'pre' && fn(item, parents) + traverseStrategy === 'pre' && fn(item, parents, idx) if (item[childrenKey]) { traverse(item[childrenKey]!, [item, ...parents]) } - traverseStrategy === 'post' && fn(item, parents) + traverseStrategy === 'post' && fn(item, parents, idx) } } @@ -149,20 +149,20 @@ export function flattenTree, C extends keyof V>( export function flattenTree, R extends object, C extends keyof V>( data: V[], childrenKey: C, - mapFn: (item: V) => R, + mapFn: (item: V, parents: V[]) => R, leafOnly?: boolean, ): (R & TreeTypeData)[] export function flattenTree, R extends object, C extends keyof V>( data: V[], childrenKey: C, - mapFn?: (item: V) => R, + mapFn?: (item: V, parents: V[]) => R, leafOnly = false, ): V[] | (R & TreeTypeData)[] { const res: V[] | (R & TreeTypeData)[] = [] - traverseTree(data, childrenKey, item => { + traverseTree(data, childrenKey, (item, parents) => { if (!leafOnly || !item[childrenKey] || item[childrenKey]!.length <= 0) { - const mappedItem = mapFn ? mapFn(item) : item + const mappedItem = mapFn ? mapFn(item, parents) : item mappedItem && res.push(mappedItem as V & (R & TreeTypeData)) } }) @@ -186,3 +186,115 @@ export function getTreeKeys, C extends keyof V>( return keys } + +function _insertTreeItem, C extends keyof V>( + data: V[], + targetKey: VKey, + newItem: V, + childrenKey: C, + getKey: (item: V) => VKey, + isAfter = true, +): V[] { + return data.flatMap(item => { + if (getKey(item) === targetKey) { + return isAfter ? [item, newItem] : [newItem, item] + } + + const chlidren = item[childrenKey] + if (chlidren?.length) { + return { + ...item, + children: _insertTreeItem(chlidren, targetKey, newItem, childrenKey, getKey, isAfter), + } + } + + return item + }) +} + +export function insertTreeItemBefore, C extends keyof V>( + data: V[], + targetKey: VKey, + newItem: V, + childrenKey: C, + getKey: (item: V) => VKey, +): V[] { + return _insertTreeItem(data, targetKey, newItem, childrenKey, getKey, false) +} + +export function insertTreeItemAfter, C extends keyof V>( + data: V[], + targetKey: VKey, + newItem: V, + childrenKey: C, + getKey: (item: V) => VKey, +): V[] { + return _insertTreeItem(data, targetKey, newItem, childrenKey, getKey, true) +} + +export function insertChildTreeItem, C extends keyof V>( + data: V[], + targetKey: VKey, + newItem: V, + childrenKey: C, + getKey: (item: V) => VKey, +): V[] { + return data.flatMap(item => { + const children = item[childrenKey] ?? [] + if (getKey(item) === targetKey) { + // already a parent: add as first child + return { + ...item, + // opening item so you can see where item landed + isOpen: true, + children: [newItem, ...children], + } + } + + if (!children.length) { + return item + } + + return { + ...item, + children: insertChildTreeItem(children, targetKey, newItem, childrenKey, getKey), + } + }) +} + +export function removeTreeItem, C extends keyof V>( + data: V[], + targetKey: VKey, + childrenKey: C, + getKey: (item: V) => VKey, +): V[] { + let removed = false + + const filterFn = (_data: V[]) => { + if (removed) { + return [..._data] + } + + const filterRes: V[] = [] + + _data.forEach(item => { + if (getKey(item) !== targetKey) { + const children = item[childrenKey] + filterRes.push( + children?.length + ? { + ...item, + [childrenKey]: filterFn(children), + } + : item, + ) + } else { + removed = true + } + }) + + return filterRes + } + + return filterFn(data) +} diff --git a/packages/site/vite.config.ts b/packages/site/vite.config.ts index 30b3c408b..68917dcd6 100644 --- a/packages/site/vite.config.ts +++ b/packages/site/vite.config.ts @@ -20,6 +20,13 @@ import { transformIndexPlugin } from './plugins/transformIndexPlugin' const componentPath: Record = { CdkClickOutside: '@idux/cdk/click-outside', CdkDraggable: '@idux/cdk/drag-drop', + CdkDndSortable: '@idux/cdk/dnd', + CdkDndSortableItem: '@idux/cdk/dnd', + CdkDndSortableHandle: '@idux/cdk/dnd', + CdkDndMovable: '@idux/cdk/dnd', + CdkDndMovableHandle: '@idux/cdk/dnd', + CdkDndSortableBoxIndicator: '@idux/cdk/dnd', + CdkDndSortableTreeIndicator: '@idux/cdk/dnd', CdkResizable: '@idux/cdk/resize', CdkResizableHandle: '@idux/cdk/resize', CdkResizeObserver: '@idux/cdk/resize', diff --git a/scripts/gulp/build/rollup.ts b/scripts/gulp/build/rollup.ts index 03a3e5fc8..0ef14a864 100644 --- a/scripts/gulp/build/rollup.ts +++ b/scripts/gulp/build/rollup.ts @@ -25,7 +25,19 @@ interface Options { minify?: boolean } -const externalDeps = ['vue', '@vue', '@idux', '@floating-ui/dom', '@emotion/hash', 'date-fns', 'lodash-es', 'ajv'] +const externalDeps = [ + 'vue', + '@vue', + '@idux', + '@floating-ui/dom', + '@emotion/hash', + '@atlaskit/pragmatic-drag-and-drop', + '@atlaskit/pragmatic-drag-and-drop-hitbox', + '@atlaskit/pragmatic-drag-and-drop-auto-scroll', + 'date-fns', + 'lodash-es', + 'ajv', +] const replaceOptions = { __DEV__: "process.env.NODE_ENV !== 'production'", __TEST__: false, diff --git a/scripts/gulp/site/utils.ts b/scripts/gulp/site/utils.ts index 5cc811531..08c523a52 100644 --- a/scripts/gulp/site/utils.ts +++ b/scripts/gulp/site/utils.ts @@ -19,6 +19,7 @@ export interface Meta { title: string subtitle?: string lang: string + hidden: boolean path: string order?: number } @@ -45,6 +46,7 @@ export function initSite(): void { 'utils', 'version', ['pro', 'theme'], + ['cdk', 'drag-drop'], ] readdirSync(packageRoot).forEach(packageName => { if (filterPackageName.includes(packageName)) {