Skip to content

Commit

Permalink
feat(comp:select): add support for dnd sortable (#1955)
Browse files Browse the repository at this point in the history
  • Loading branch information
sallerli1 authored Jul 10, 2024
1 parent 22698bf commit 6281bc2
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 94 deletions.
13 changes: 13 additions & 0 deletions packages/components/select/demo/DndSortable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title:
zh: 拖拽排序
en: Dnd sortable
order: 23
---
## zh

通过 `dndSortable` 配置拖拽排序。

## en

enable drag sorting by `dndSortable` prop.
43 changes: 43 additions & 0 deletions packages/components/select/demo/DndSortable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<IxSelect
:selectedKeys="value"
:dataSource="dataSource"
:onOptionClick="onOptionClick"
:onDndSortChange="onDndSortChange"
multiple
:dndSortable="{
dragHandle: true,
}"
>
</IxSelect>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { SelectData } from '@idux/components/select'
const dataSource = ref<SelectData[]>([])
const tempData: SelectData[] = []
for (let index = 0; index < 20; index++) {
const prefix = index > 9 ? 'B' : 'A'
tempData.push({ key: index, label: prefix + index, disabled: index % 3 === 0 })
}
dataSource.value = tempData
const value = ref([1])
const onOptionClick = (option: SelectData) => {
const index = value.value.findIndex(key => key === option.key)
if (index > -1) {
value.value.splice(index, 1)
} else {
value.value.push(option.key as number)
}
}
const onDndSortChange = (newOptions: SelectData[]) => {
console.log('onDndSortChange', newOptions)
dataSource.value = newOptions
}
</script>
16 changes: 16 additions & 0 deletions packages/components/select/docs/Api.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
| `customAdditional` | 自定义下拉选项的额外属性 | `SelectCustomAdditional` | - | - | 例如 `class`, 或者原生事件 |
| `dataSource` | 选项数据源 | `SelectData[]` | - | - | 优先级高于 `default` 插槽, 性能会更好 |
| `disabled` | 是否禁用状态 | `boolean` | `false` | - | 使用 `control` 时,此配置无效 |
| `dndSortable` | 拖拽排序配置 | `boolean` | `SelectDndSortable` | `false` | - |
| `empty` | 自定义当下拉列表为空时显示的内容 | `'default' \| 'simple' \| EmptyProps` | `'simple'` | - | - |
| `getKey` | 获取数据的唯一标识 | `string \| (data: SelectData) => VKey` | `key` || 为了兼容之前的版本,默认值也会支持 `value` |
| `labelKey` | 选项 label 的 key | `string` | `label` || 仅在使用 `dataSource` 时有效 |
Expand Down Expand Up @@ -45,6 +46,21 @@
| `onScroll` | 滚动事件 | `(evt: Event) => void` | - | - | - |
| `onScrolledChange` | 滚动的位置发生变化 | `(startIndex: number, endIndex: number, visibleData: SelectData[]) => void` | - | - |`virtual` 模式下可用 |
| `onScrolledBottom` | 滚动到底部时触发 | `() => void` | - | - |`virtual` 模式下可用 |
| `onScrolledChange` | 滚动的位置发生变化 | `(startIndex: number, endIndex: number, visibleData: SelectData[]) => void` | - | - |`virtual` 模式下可用 |
| `onDndSortReorder` | 数据重排序之后的回调 | `(reorderInfo: DndSortableReorderInfo) => void` | - | - | - |
| `onDndSortChange` | 数据排序改变之后的回调 | `(newData: SelectData[], oldData: SelectData[]) => void` | - | - | - |

```ts
export interface DndSortableReorderInfo {
sourceIndex: number
targetIndex: number
sourceKey: VKey
targetKey: VKey
sourceData: SelectData
targetData: SelectData
operation: 'insertBefore' | 'insertAfter' | 'insertChild'
}
```

```ts
export type SelectData = SelectOptionProps | SelectOptionGroupProps
Expand Down
12 changes: 11 additions & 1 deletion packages/components/select/src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ export default defineComponent({
return spinProps ? <IxSpin {...spinProps}>{children}</IxSpin> : children
}

const handleContentMouseDown = (evt: MouseEvent) => {
if (evt.target instanceof HTMLInputElement) {
return
}

setTimeout(() => {
focus()
})
}

const renderContent: ControlTriggerSlots['overlay'] = () => {
const children = [renderLoading(<Panel ref={panelRef} v-slots={slots} {...panelProps.value} />)]
const { searchable, overlayRender } = props
Expand Down Expand Up @@ -231,7 +241,7 @@ export default defineComponent({
)
}

return [<div>{overlayRender ? overlayRender(children) : children}</div>]
return [<div onMousedown={handleContentMouseDown}>{overlayRender ? overlayRender(children) : children}</div>]
}

return () => {
Expand Down
54 changes: 2 additions & 52 deletions packages/components/select/src/composables/useOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

/* eslint-disable @typescript-eslint/no-explicit-any */

import type { SelectData, SelectProps, SelectSearchFn } from '../types'
import type { FlattenedOption, SelectData, SelectProps, SelectSearchFn } from '../types'
import type { ComputedRef, Ref, Slots, VNode } from 'vue'

import { computed } from 'vue'
Expand All @@ -18,17 +18,9 @@ import { type VKey, flattenNode } from '@idux/cdk/utils'

import { GetKeyFn } from './useGetOptionKey'
import { optionGroupKey, optionKey } from '../option'
import { flattenOptions } from '../utils/flattenOptions'
import { generateOption } from '../utils/generateOption'

export interface FlattenedOption {
key: VKey
label: string
disabled?: boolean
rawData: SelectData
type: 'group' | 'item'
parentKey?: VKey
}

export function useConvertedOptions(props: SelectProps, slots: Slots): ComputedRef<SelectData[]> {
return computed(() => {
return props.dataSource ?? convertOptions(slots.default?.())
Expand Down Expand Up @@ -133,48 +125,6 @@ function filterOptions(
return filteredOptions
}

function flattenOptions(options: SelectData[] | undefined, childrenKey: string, getKeyFn: GetKeyFn, labelKey: string) {
const mergedOptions: FlattenedOption[] = []
const appendOption = (item: SelectData, index: number | undefined, parentKey?: VKey) => {
const parsedOption = parseOption(item, item => getKeyFn(item) ?? index, childrenKey, labelKey, parentKey)
mergedOptions.push(parsedOption)

return parsedOption.key
}

options?.forEach((item, index) => {
const children = item[childrenKey] as SelectData[]

const optionKey = appendOption(item, index)

if (children && children.length > 0) {
children.forEach(child => {
appendOption(child, undefined, optionKey)
})
}
})

return mergedOptions
}

function parseOption(
option: SelectData,
getKey: GetKeyFn,
childrenKey: string,
labelKey: string,
parentKey?: VKey,
): FlattenedOption {
const children = option[childrenKey] as SelectData[] | undefined
return {
key: getKey(option),
parentKey,
label: option[labelKey],
disabled: !!option.disabled,
type: children && children.length > 0 ? 'group' : 'item',
rawData: option,
}
}

function useSearchFn(props: SelectProps, mergedLabelKey: ComputedRef<string>) {
return computed(() => {
const searchFn = props.searchFn
Expand Down
3 changes: 3 additions & 0 deletions packages/components/select/src/composables/usePanelProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function usePanelProps(
childrenKey: props.childrenKey,
customAdditional: props.customAdditional,
empty: props.empty,
dndSortable: props.dndSortable,
getKey: props.getKey,
labelKey: props.labelKey,
multiple: props.multiple,
Expand All @@ -36,6 +37,8 @@ export function usePanelProps(
onScroll: props.onScroll,
onScrolledChange: props.onScrolledChange,
onScrolledBottom: props.onScrolledBottom,
onDndSortReorder: props.onDndSortReorder,
onDndSortChange: props.onDndSortChange,
_virtualScrollHeight: props.overlayHeight,
}))
}
42 changes: 29 additions & 13 deletions packages/components/select/src/panel/Option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { computed, defineComponent, inject } from 'vue'

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

import { CdkDndSortableHandle, CdkDndSortableItem } from '@idux/cdk/dnd'
import { callEmit } from '@idux/cdk/utils'
import { IxCheckbox } from '@idux/components/checkbox'
import { IxIcon } from '@idux/components/icon'

import { selectPanelContext } from '../token'
import { optionProps } from '../types'
Expand All @@ -22,6 +24,7 @@ export default defineComponent({
const {
props: selectPanelProps,
mergedPrefixCls,
mergedDndSortable,
selectedKeys,
selectedLimit,
selectedLimitTitle,
Expand All @@ -41,6 +44,7 @@ export default defineComponent({
return {
[prefixCls]: true,
[`${prefixCls}-active`]: isActive.value,
[`${prefixCls}-with-drag-handle`]: mergedDndSortable.value && mergedDndSortable.value.handle,
[`${prefixCls}-disabled`]: isDisabled.value,
[`${prefixCls}-grouped`]: !isNil(parentKey),
[`${prefixCls}-selected`]: isSelected.value,
Expand All @@ -67,19 +71,31 @@ export default defineComponent({
? selectPanelProps.customAdditional({ data: rawData!, index: props.index! })
: undefined

return (
<div
class={classes.value}
title={title}
onMouseenter={disabled ? undefined : handleMouseEnter}
onClick={disabled ? undefined : handleClick}
aria-label={_label}
aria-selected={selected}
{...customAdditional}
>
{multiple && <IxCheckbox checked={selected} disabled={disabled} />}
<span class={`${prefixCls}-label`}>{renderOptionLabel(slots, rawData!, _label)}</span>
</div>
const optionContentNodes = [
multiple && <IxCheckbox checked={selected} disabled={disabled} />,
<span class={`${prefixCls}-label`}>{renderOptionLabel(slots, rawData!, _label)}</span>,
]
const optionsProps = {
class: classes.value,
title,
onMouseenter: disabled ? undefined : handleMouseEnter,
onClick: disabled ? undefined : handleClick,
'aria-label': _label,
'aria-selected': selected,
...customAdditional,
}

return mergedDndSortable.value ? (
<CdkDndSortableItem itemKey={props.optionKey} canDrag={!isDisabled.value} {...optionsProps}>
{mergedDndSortable.value.dragHandle ? (
<CdkDndSortableHandle class={`${prefixCls}-drag-handle`}>
{!isDisabled.value && <IxIcon name={mergedDndSortable.value.dragHandle} />}
</CdkDndSortableHandle>
) : undefined}
{optionContentNodes}
</CdkDndSortableItem>
) : (
<div {...optionsProps}>{optionContentNodes}</div>
)
}
},
Expand Down
Loading

0 comments on commit 6281bc2

Please sign in to comment.