Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(comp:select): add support for dnd sortable #1955

Merged
merged 1 commit into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
return spinProps ? <IxSpin {...spinProps}>{children}</IxSpin> : children
}

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

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

Check warning on line 210 in packages/components/select/src/Select.tsx

View check run for this annotation

Codecov / codecov/patch

packages/components/select/src/Select.tsx#L203-L210

Added lines #L203 - L210 were not covered by tests

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 @@
)
}

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