Skip to content

Commit

Permalink
feat(cdk:utils, pro:search): add tree utils, add pro search `'treeSel…
Browse files Browse the repository at this point in the history
…ect'` field (#1391)

* fix(pro:search): clicking input container should focus temp item
when certain item is active, temp item should be focused when clicking elsewhere within input container

* feat(pro:search): add `treeSelect` type search field
  • Loading branch information
sallerli1 committed Jan 16, 2023
1 parent fec5fee commit 4bf719d
Show file tree
Hide file tree
Showing 15 changed files with 579 additions and 42 deletions.
41 changes: 41 additions & 0 deletions packages/pro/search/demo/Basic.vue
Expand Up @@ -94,6 +94,47 @@ const searchFields: SearchField[] = [
],
},
},
{
type: 'treeSelect',
label: 'Tree Data',
key: 'tree_data',
fieldConfig: {
multiple: true,
searchable: true,
checkable: true,
cascaderStrategy: 'all',
dataSource: [
{
label: 'Node 0',
key: '0',
children: [
{
label: 'Node 0-0',
key: '0-0',
children: [
{
label: 'Node 0-0-0',
key: '0-0-0',
},
{
label: 'Node 0-0-1',
key: '0-0-1',
},
],
},
{
label: 'Node 0-1',
key: '0-1',
children: [
{ label: 'Node 0-1-0', key: '0-1-0' },
{ label: 'Node 0-1-1', key: '0-1-1' },
],
},
],
},
],
},
},
{
type: 'datePicker',
label: 'Date',
Expand Down
46 changes: 45 additions & 1 deletion packages/pro/search/docs/Api.zh.md
Expand Up @@ -93,7 +93,7 @@ SelectSearchFieldConfig
| --- | --- | --- | --- | --- | --- |
| `dataSource` | 类型 | `SelectPanelData[]` | - | - | 继承自`SelectData`,但`key``label`为必填,不支持可配,详情参考[Select](/components/select/zh) |
| `multiple` | 是否为多选 | `boolean` | - | - | 默认为单选 |
| `searchable` | 是否支持筛选 | `boolean` | false | - | 默认不支持 |
| `searchable` | 是否支持筛选 | `boolean` | `false` | - | 默认不支持 |
| `searchFn` | 搜索函数 | `(data: SelectPanelData, searchText: string) => boolean` | - | - | 默认模糊匹配 |
| `separator` | 多选分隔符 | `string` | `'|'` | - | - |
| `showSelectAll` | 是否支持全选 | `boolean` | `true` | - | - |
Expand All @@ -106,6 +106,49 @@ SelectSearchFieldConfig
type SelectPanelData = Required<Pick<SelectData, 'key' | 'label'>> & SelectData
```
#### TreeSelectSearchField
树选择类型
| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 |
| --- | --- | --- | --- | --- | --- |
| `type` | 类型 | `'treeSelect'` | - | - | 固定为 `'select'` |
| `fieldConfig` | 配置 | `TreeSelectSearchFieldConfig` | - | - | - |
TreeSelectSearchFieldConfig
| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 |
| --- | --- | --- | --- | --- | --- |
| `dataSource` | 类型 | `TreeSelectPanelData[]` | - | - | 继承自`TreeSelectNode`,但`key``label`为必填,不支持可配,且`childrenKey`固定为`'children'`,详情参考[Tree](/components/tree/zh) |
| `multiple` | 是否为多选 | `boolean` | - | - | 默认为单选 |
| `checkable` | 是否可勾选 | `boolean` | - | - | 默认不可勾选 |
| `cascaderStrategy` | 级联策略 | `CascaderStrategy` | - | - | 详情参考[Tree](/components/tree/zh) |
| `draggable` | 是否可拖拽 | `boolean` | - | - | 详情参考[Tree](/components/tree/zh) |
| `draggableIcon` | 拖拽图标 | `string` | - | - | 详情参考[Tree](/components/tree/zh) |
| `showLine` | 是否展示连线 | `boolean` | - | - | 详情参考[Tree](/components/tree/zh) |
| `searchable` | 是否支持筛选 | `boolean` | false | - | 默认不支持 |
| `searchFn` | 搜索函数 | `(node: TreeSelectPanelData, searchValue?: string) => boolean` | - | - | 默认模糊匹配 |
| `separator` | 多选分隔符 | `string` | `'|'` | - | - |
| `virtual` | 是否支持虚拟滚动 | `boolean` | `false` | - | 默认不支持 |
| `onCheck` | 勾选回调函数 | `(checked: boolean, node: TreeSelectPanelData) => void | ((checked: boolean, node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
| `onDragstart` | `dragstart` 触发时调用 | `(options: TreeDragDropOptions<any>) => void | ((options: TreeDragDropOptions<any>) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
| `onDragend` | `dragend` 触发时调用 | `(options: TreeDragDropOptions<any>) => void | ((options: TreeDragDropOptions<any>) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
| `onDragenter` | `dragenter` 触发时调用 | `(options: TreeDragDropOptions<any>) => void | ((options: TreeDragDropOptions<any>) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
| `onDragleave` | `dragleave` 触发时调用 | `(options: TreeDragDropOptions<any>) => void | ((options: TreeDragDropOptions<any>) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
| `onDragover` | `dragover` 触发时调用 | `(options: TreeDragDropOptions<any>) => void | ((options: TreeDragDropOptions<any>) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
| `onDrop` | `drop` 触发时调用 | `(options: TreeDragDropOptions<any>) => void | ((options: TreeDragDropOptions<any>) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
| `onExpand` | 点击展开图标时触发 | `(expanded: boolean, node: TreeSelectPanelData) => void | ((expanded: boolean, node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
| `onSelect` | 选中状态发生变化时触发 | `(selected: boolean, node: TreeSelectPanelData) => void | ((selected: boolean, node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
| `onLoaded` | 子节点加载完毕时触发 | `(loadedKeys: any[], node: TreeSelectPanelData) => void | ((loadedKeys: any[], node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) |
```typescript
type TreeSelectPanelData = TreeSelectNode &
Required<Pick<TreeSelectNode, 'key' | 'label'>> & {
children?: TreeSelectPanelData[]
}
```
#### DatePickerSearchField
日期选择类型
Expand Down Expand Up @@ -169,5 +212,6 @@ interface PanelRenderContext<V = unknown> {
ok: () => void // 确认
cancel: () => void // 取消
setValue: (value: V) => void // 设置搜索值
setOnKeyDown: (onKeyDown: ((evt: KeyboardEvent) => boolean) | undefined) => void // 设置 `keydown` 回调函数
}
```
2 changes: 1 addition & 1 deletion packages/pro/search/src/composables/useActiveSegment.ts
Expand Up @@ -102,7 +102,7 @@ export function useActiveSegment(
} else {
setActiveSegment({
itemKey: tempSearchStateKey,
name: 'name',
name: activeSegment.value?.itemKey === tempSearchStateKey ? activeSegment.value.name : 'name',
overlayOpened,
})
}
Expand Down
26 changes: 17 additions & 9 deletions packages/pro/search/src/composables/useFocusedState.ts
Expand Up @@ -6,7 +6,6 @@
*/

import type { ActiveSegmentContext } from './useActiveSegment'
import type { SearchStateContext } from './useSearchStates'
import type { ProSearchProps } from '../types'
import type { ɵOverlayProps } from '@idux/components/_private/overlay'

Expand All @@ -17,6 +16,8 @@ import { isFunction, isString } from 'lodash-es'
import { useSharedFocusMonitor } from '@idux/cdk/a11y'
import { MaybeElementRef, callEmit, useState } from '@idux/cdk/utils'

import { type SearchStateContext, tempSearchStateKey } from './useSearchStates'

export interface FocusEventContext {
focused: ComputedRef<boolean>
focus: (options?: FocusOptions) => void
Expand All @@ -34,7 +35,7 @@ export function useFocusedState(
const { activeSegment, setInactive, setTempActive } = activeSegmentContext
const [focused, setFocused] = useState(false)

const { handleFocus, handleBlur } = useFocusHandlers(props, focused, setFocused, setInactive, initTempSearchState)
const { handleFocus, handleBlur } = useFocusHandlers(props, focused, setFocused, setInactive)

watch([activeSegment, searchStates], ([segment]) => {
if (!segment && focused.value) {
Expand All @@ -47,10 +48,19 @@ export function useFocusedState(
return
}

handleFocus(evt, () => {
if (evt.target === elementRef.value) {
setTempActive(true)
}
if (evt.target === elementRef.value) {
setTempActive(activeSegment.value?.itemKey !== tempSearchStateKey)
}

handleFocus(evt)
}
const _handleBlur = (evt: FocusEvent) => {
if (props.disabled) {
return
}

handleBlur(evt, () => {
initTempSearchState()
})
}

Expand All @@ -62,7 +72,7 @@ export function useFocusedState(
setFocused(false)
}

registerHandlers(elementRef, () => getContainerEl(commonOverlayProps.value), _handleFocus, handleBlur)
registerHandlers(elementRef, () => getContainerEl(commonOverlayProps.value), _handleFocus, _handleBlur)

return { focused, focus, blur }
}
Expand All @@ -83,7 +93,6 @@ function useFocusHandlers(
focused: ComputedRef<boolean>,
setFocused: (focused: boolean) => void,
setInactive: (blur?: boolean) => void,
initTempSearchState: () => void,
): {
handleFocus: (evt: FocusEvent, cb?: () => void) => void
handleBlur: (evt: FocusEvent, cb?: () => void) => void
Expand Down Expand Up @@ -112,7 +121,6 @@ function useFocusHandlers(
return
}

initTempSearchState()
cb?.()

setInactive(true)
Expand Down
3 changes: 3 additions & 0 deletions packages/pro/search/src/composables/useSearchItem.ts
Expand Up @@ -16,6 +16,7 @@ import { createDateRangePickerSegment } from '../segments/CreateDateRangePickerS
import { createNameSegment } from '../segments/CreateNameSegment'
import { createOperatorSegment } from '../segments/CreateOperatorSegment'
import { createSelectSegment } from '../segments/CreateSelectSegment'
import { createTreeSelectSegment } from '../segments/CreateTreeSelectSegment'
import { createCustomSegment } from '../segments/createCustomSegment'
import { createInputSegment } from '../segments/createInputSegment'

Expand Down Expand Up @@ -69,6 +70,8 @@ function createSearchItemContentSegment(
switch (searchField.type) {
case 'select':
return createSelectSegment(prefixCls, searchField)
case 'treeSelect':
return createTreeSelectSegment(prefixCls, searchField)
case 'input':
return createInputSegment(prefixCls, searchField)
case 'datePicker':
Expand Down
175 changes: 175 additions & 0 deletions packages/pro/search/src/panel/TreeSelectPanel.tsx
@@ -0,0 +1,175 @@
/**
* @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 ComputedRef, computed, defineComponent, inject } from 'vue'

import { isFunction } from 'lodash-es'

import { NoopFunction, type VKey, callEmit, traverseTree, useState } from '@idux/cdk/utils'
import { IxButton } from '@idux/components/button'
import { IxTree, type TreeProps } from '@idux/components/tree'

import { proSearchContext } from '../token'
import { type ProSearchTreeSelectPanelProps, type TreeSelectPanelData, proSearchTreeSelectPanelProps } from '../types'

export default defineComponent({
props: proSearchTreeSelectPanelProps,
setup(props) {
const { mergedPrefixCls, locale } = inject(proSearchContext)!

const mergedCheckable = computed(() => props.multiple && props.checkable)
const mergedCascaderStrategy = computed(() => {
if (!mergedCheckable.value) {
return 'off'
}
return props.cascaderStrategy
})

const { expandedKeys, setExpandedKeys } = useExpandedKeys(props)

const changeSelected = (keys: VKey[]) => {
if (!props.multiple && !keys.length) {
return
}

props.onChange?.(keys)
}

const handleConfirm = () => {
props.onConfirm?.()
}
const handleCancel = () => {
props.onCancel?.()
}
const handleCheck = (checked: boolean, node: TreeSelectPanelData) => {
const { onCheck } = props
callEmit(onCheck, checked, node)
}
const handleSelect = (selected: boolean, node: TreeSelectPanelData) => {
const { onSelect } = props

if (!props.multiple && props.value?.[0] !== node.key) {
callEmit(onSelect, selected, node)
}
}
const handleExpand = (expanded: boolean, node: TreeSelectPanelData) => {
const { onExpand } = props
callEmit(onExpand, expanded, node)
}
const onLoaded = async (loadedKeys: VKey[], node: TreeSelectPanelData) => {
callEmit(props.onLoaded, loadedKeys, node)
}

const renderFooter = (prefixCls: string) => {
if (!props.multiple) {
return
}

return (
<div class={`${prefixCls}-footer`}>
<IxButton mode="primary" size="xs" onClick={handleConfirm}>
{locale.ok}
</IxButton>
<IxButton size="xs" onClick={handleCancel}>
{locale.cancel}
</IxButton>
</div>
)
}

return () => {
const {
dataSource,
draggable,
draggableIcon,
expandIcon,
multiple,
leafLineIcon,
virtual,
showLine,
searchValue,
onDragstart,
onDragend,
onDragenter,
onDragleave,
onDragover,
onDrop,
droppable,
loadChildren,
searchFn,
} = props

const treeProps = {
blocked: true,
checkOnClick: true,
checkedKeys: props.value,
labelKey: 'label',
checkable: mergedCheckable.value,
cascaderStrategy: mergedCascaderStrategy.value,
childrenKey: 'children',
dataSource,
draggable,
draggableIcon,
droppable,
expandedKeys: expandedKeys.value,
expandIcon: expandIcon,
getKey: 'key',
autoHeight: true,
loadChildren,
leafLineIcon,
virtual,
selectable: multiple ? 'multiple' : true,
selectedKeys: props.value,
searchValue,
searchFn: isFunction(searchFn) ? searchFn : undefined,
showLine: showLine,
onCheck: handleCheck,
onDragstart,
onDragend,
onDragenter,
onDragleave,
onDragover,
onDrop,
onExpand: handleExpand,
onSelect: handleSelect,
onLoaded,
onCheckedChange: changeSelected,
onSelectedChange: !mergedCheckable.value ? changeSelected : NoopFunction,
onExpandedChange: setExpandedKeys,
} as TreeProps

const prefixCls = `${mergedPrefixCls.value}-tree-select-panel`

return (
<div class={prefixCls} tabindex={-1} onMousedown={evt => evt.preventDefault()}>
<IxTree class={`${prefixCls}-body`} {...treeProps} />
{renderFooter(prefixCls)}
</div>
)
}
},
})

function useExpandedKeys(props: ProSearchTreeSelectPanelProps): {
expandedKeys: ComputedRef<VKey[]>
setExpandedKeys: (keys: VKey[]) => void
} {
const initialExpandedKeySet = new Set<VKey>()
props.dataSource &&
traverseTree(props.dataSource, 'children', (item, parents) => {
if (props.value?.includes(item.key)) {
parents.forEach(parent => initialExpandedKeySet.add(parent.key))
}
})

const [expandedKeys, setExpandedKeys] = useState<VKey[]>([...initialExpandedKeySet])

return {
expandedKeys,
setExpandedKeys,
}
}

0 comments on commit 4bf719d

Please sign in to comment.