From 88b751b276440d0024296c5ccd3ef62c53af6d35 Mon Sep 17 00:00:00 2001 From: saller Date: Mon, 6 Mar 2023 19:18:02 +0800 Subject: [PATCH] feat(pro:search): add `'cascader'` searchField (#1485) --- packages/pro/search/demo/Basic.vue | 122 ++++++++++ packages/pro/search/demo/RemoteSearch.vue | 29 +++ packages/pro/search/docs/Api.zh.md | 54 +++-- .../search/src/composables/useSearchItem.ts | 3 + .../pro/search/src/panel/CascaderPanel.tsx | 110 +++++++++ .../pro/search/src/panel/DatePickerPanel.tsx | 24 +- packages/pro/search/src/panel/PanelFooter.tsx | 30 +++ packages/pro/search/src/panel/SelectPanel.tsx | 16 +- .../pro/search/src/panel/TreeSelectPanel.tsx | 20 +- .../src/segments/CreateCascaderSegment.tsx | 219 ++++++++++++++++++ .../src/segments/CreateSelectSegment.tsx | 28 +-- .../src/segments/CreateTreeSelectSegment.tsx | 22 +- packages/pro/search/src/types/panels.ts | 53 ++++- packages/pro/search/src/types/searchFields.ts | 36 ++- .../src/utils/getSelectableCommonParams.ts | 59 +++++ packages/pro/search/src/utils/index.ts | 10 + packages/pro/search/style/index.less | 47 ++-- .../search/style/themes/default.variable.less | 2 + 18 files changed, 778 insertions(+), 106 deletions(-) create mode 100644 packages/pro/search/src/panel/CascaderPanel.tsx create mode 100644 packages/pro/search/src/panel/PanelFooter.tsx create mode 100644 packages/pro/search/src/segments/CreateCascaderSegment.tsx create mode 100644 packages/pro/search/src/utils/getSelectableCommonParams.ts create mode 100644 packages/pro/search/src/utils/index.ts diff --git a/packages/pro/search/demo/Basic.vue b/packages/pro/search/demo/Basic.vue index 3c5b71494..d582833ec 100644 --- a/packages/pro/search/demo/Basic.vue +++ b/packages/pro/search/demo/Basic.vue @@ -135,6 +135,128 @@ const searchFields: SearchField[] = [ ], }, }, + { + type: 'cascader', + key: 'cascader', + label: 'Cascader', + fieldConfig: { + fullPath: true, + multiple: true, + searchable: true, + dataSource: [ + { + key: 'components', + label: 'Components', + children: [ + { + key: 'general', + label: 'General', + children: [ + { + key: 'button', + label: 'Button', + }, + { + key: 'header', + label: 'Header', + }, + { + key: 'icon', + label: 'Icon', + }, + ], + }, + { + key: 'layout', + label: 'Layout', + children: [ + { + key: 'divider', + label: 'Divider', + }, + { + key: 'grid', + label: 'Grid', + }, + { + key: 'space', + label: 'Space', + }, + ], + }, + { + key: 'navigation', + label: 'Navigation', + children: [ + { + key: 'breadcrumb', + label: 'Breadcrumb', + }, + { + key: 'dropdown', + label: 'Dropdown', + }, + { + key: 'menu', + label: 'Menu', + }, + { + key: 'pagination', + label: 'Pagination', + }, + ], + }, + ], + }, + { + key: 'pro', + label: 'Pro', + children: [ + { + key: 'pro-layout', + label: 'Layout', + }, + { + key: 'pro-table', + label: 'Table', + disabled: true, + }, + { + key: 'pro-transfer', + label: 'Transfer', + }, + ], + }, + { + key: 'cdk', + label: 'CDK', + disabled: true, + children: [ + { + key: 'a11y', + label: 'Accessibility', + }, + { + key: 'breakpoint', + label: 'Breakpoint', + }, + { + key: 'click-outside', + label: 'ClickOutside', + }, + { + key: 'clipboard', + label: 'Clipboard', + }, + { + key: 'forms', + label: 'Forms', + }, + ], + }, + ], + }, + }, { type: 'datePicker', label: 'Date', diff --git a/packages/pro/search/demo/RemoteSearch.vue b/packages/pro/search/demo/RemoteSearch.vue index e905e699c..e05090e81 100644 --- a/packages/pro/search/demo/RemoteSearch.vue +++ b/packages/pro/search/demo/RemoteSearch.vue @@ -24,6 +24,11 @@ interface TreeSelectData { label: string children?: TreeSelectData[] } +interface CascaderData { + key: string + label: string + children?: TreeSelectData[] +} const labels = ['Archer', 'Berserker', 'Lancer', 'Rider', 'Saber', 'Caster', 'Assassin'] const baseSelectData: SelectData[] = Array.from(new Array(50)).map((_, idx) => { @@ -66,6 +71,8 @@ const baseTreeSelectData: TreeSelectData[] = Array.from(new Array(20)).map((_, i ], } }) +const baseCascaderData = baseTreeSelectData as CascaderData[] + const createSelectData = (searchValue: string) => { return baseSelectData.filter(item => new RegExp(searchValue.toLowerCase()).test(item.label.toLowerCase())) } @@ -74,10 +81,16 @@ const createTreeSelectData = (searchValue: string) => { new RegExp(searchValue.toLowerCase()).test(item.label.toLowerCase()), ) } +const createCascaderData = (searchValue: string) => { + return filterTree(baseCascaderData, 'children', item => + new RegExp(searchValue.toLowerCase()).test(item.label.toLowerCase()), + ) +} const value = ref() const selectData = ref(createSelectData('')) const treeSelectData = ref(createTreeSelectData('')) +const cascaderData = ref(createCascaderData('')) const selectOnSearch = (searchValue: string) => { selectData.value = createSelectData(searchValue) @@ -85,6 +98,9 @@ const selectOnSearch = (searchValue: string) => { const treeSelectOnSearch = (searchValue: string) => { treeSelectData.value = createTreeSelectData(searchValue) } +const cascaderOnSearch = (searchValue: string) => { + cascaderData.value = createCascaderData(searchValue) +} const searchFields = computed(() => [ { @@ -113,6 +129,19 @@ const searchFields = computed(() => [ onSearch: treeSelectOnSearch, }, }, + { + type: 'cascader', + label: 'Cascader Data', + key: 'cascader_data', + fieldConfig: { + multiple: true, + searchable: true, + cascaderStrategy: 'all', + dataSource: cascaderData.value, + searchFn: () => true, + onSearch: cascaderOnSearch, + }, + }, ]) const onChange = (value: SearchValue[] | undefined, oldValue: SearchValue[] | undefined) => { diff --git a/packages/pro/search/docs/Api.zh.md b/packages/pro/search/docs/Api.zh.md index 5802f4c0d..7a8919a68 100644 --- a/packages/pro/search/docs/Api.zh.md +++ b/packages/pro/search/docs/Api.zh.md @@ -127,22 +127,21 @@ TreeSelectSearchFieldConfig | `draggable` | 是否可拖拽 | `boolean` | - | - | 详情参考[Tree](/components/tree/zh) | | `draggableIcon` | 拖拽图标 | `string` | - | - | 详情参考[Tree](/components/tree/zh) | | `showLine` | 是否展示连线 | `boolean` | - | - | 详情参考[Tree](/components/tree/zh) | -| `searchable` | 是否支持筛选 | `boolean` | false | - | 默认不支持 | +| `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) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | -| `onDragend` | `dragend` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | -| `onDragenter` | `dragenter` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | -| `onDragleave` | `dragleave` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | -| `onDragover` | `dragover` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | -| `onDrop` | `drop` 触发时调用 | `(options: TreeDragDropOptions) => void | ((options: TreeDragDropOptions) => 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) | -| `onSearch` | 搜索回调函数 | `(searchValue: string) => void | ((searchValue: string) => void)[]` | - | - | 在触发搜索值改变时执行 | -| `onLoaded` | 子节点加载完毕时触发 | `(loadedKeys: any[], node: TreeSelectPanelData) => void | ((loadedKeys: any[], node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onCheck` | 勾选回调函数 | `((checked: boolean, node: TreeSelectPanelData) => void) \| ((checked: boolean, node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragstart` | `dragstart` 触发时调用 | `((options: TreeDragDropOptions) => void) \| ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragend` | `dragend` 触发时调用 | `((options: TreeDragDropOptions) => void) \| ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragenter` | `dragenter` 触发时调用 | `((options: TreeDragDropOptions) => void \| ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragleave` | `dragleave` 触发时调用 | `((options: TreeDragDropOptions) => void) \| ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDragover` | `dragover` 触发时调用 | `((options: TreeDragDropOptions) => void) \| ((options: TreeDragDropOptions) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | +| `onDrop` | `drop` 触发时调用 | `((options: TreeDragDropOptions) => void) \| ((options: TreeDragDropOptions) => 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) | +| `onSearch` | 搜索回调函数 | `((searchValue: string) => void) \| ((searchValue: string) => void)[]` | - | - | 在触发搜索值改变时执行 | +| `onLoaded` | 子节点加载完毕时触发 | `((loadedKeys: any[], node: TreeSelectPanelData) => void) \| ((loadedKeys: any[], node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Tree](/components/tree/zh) | ```typescript type TreeSelectPanelData = TreeSelectNode & @@ -151,6 +150,35 @@ type TreeSelectPanelData = TreeSelectNode & } ``` +#### CascaderSearchField + +级联选择类型 + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `type` | 类型 | `'cascader'` | - | - | 固定为 `'cascader'` | +| `fieldConfig` | 配置 | `'CascaderSearchFieldConfig'` | - | - | - | + +CascaderSearchFieldConfig + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `dataSource` | 类型 | `CascaderPanelData[]` | - | - | 继承自`CascaderData`,但`key`和`label`为必填,不支持可配,且`childrenKey`固定为`'children'`,详情参考[Cascader](/components/cascader/zh) | +| `cascaderStrategy` | 级联策略 | `CascaderStrategy` | `''` | - | 详情参考[Cascader](/components/cascader/zh) | +| `multiple` | 是否为多选 | `boolean` | - | - | 默认为单选 | +| `disableData` | 动态禁用某些项 | `(data: CascaderPanelData) => boolean` | - | - | 详情参考[Cascader](/components/cascader/zh) | +| `expandIcon` | 展开图标 | `string` | - | - | 详情参考[Cascader](/components/cascader/zh) | +| `expandTrigger` | 触发展开的方式 | ``'click' \| 'hover'` | - | - | 详情参考[Cascader](/components/cascader/zh) | +| `fullPath` | 选中后的值是否包含全部路径 | `boolean` | - | `false` | 详情参考[Cascader](/components/cascader/zh) | +| `pathSeparator` | 设置分割符 | `string` | - | - | 详情参考[Cascader](/components/cascader/zh) | +| `searchable` | 是否支持筛选 | `boolean` | false | - | 默认不支持 | +| `searchFn` | 搜索函数 | `(node: TreeSelectPanelData, searchValue?: string) => boolean` | - | - | 默认模糊匹配 | +| `separator` | 多选分隔符 | `string` | `'|'` | - | - +| `virtual` | 是否支持虚拟滚动 | `boolean` | `false` | - | 默认不支持 | +| `onExpand` | 点击展开图标时触发 | `((expanded: boolean, data: CascaderPanelData) => void) \| ((expanded: boolean, data: CascaderPanelData) => void>)[]` | - | - | 详情参考[Cascader](/components/cascader/zh) | +| `onSearch` | 开启搜索功能后,输入后的回调 | `((searchValue: string) => void) \| ((searchValue: string) => void)[]` | - | - | 详情参考[Cascader](/components/cascader/zh) | +| `onLoaded` | 子节点加载完毕时触发 | `((loadedKeys: any[], node: TreeSelectPanelData) => void) \| ((loadedKeys: any[], node: TreeSelectPanelData) => void)[]` | - | - | 详情参考[Cascader](/components/cascader/zh) | + #### DatePickerSearchField 日期选择类型 diff --git a/packages/pro/search/src/composables/useSearchItem.ts b/packages/pro/search/src/composables/useSearchItem.ts index 0977f2483..5dc7211af 100644 --- a/packages/pro/search/src/composables/useSearchItem.ts +++ b/packages/pro/search/src/composables/useSearchItem.ts @@ -11,6 +11,7 @@ import type { DateConfig } from '@idux/components/config' import { type ComputedRef, type Slots, computed } from 'vue' +import { createCascaderSegment } from '../segments/CreateCascaderSegment' import { createDatePickerSegment } from '../segments/CreateDatePickerSegment' import { createDateRangePickerSegment } from '../segments/CreateDateRangePickerSegment' import { createNameSegment } from '../segments/CreateNameSegment' @@ -73,6 +74,8 @@ function createSearchItemContentSegment( return createSelectSegment(prefixCls, searchField) case 'treeSelect': return createTreeSelectSegment(prefixCls, searchField) + case 'cascader': + return createCascaderSegment(prefixCls, searchField) case 'input': return createInputSegment(prefixCls, searchField) case 'datePicker': diff --git a/packages/pro/search/src/panel/CascaderPanel.tsx b/packages/pro/search/src/panel/CascaderPanel.tsx new file mode 100644 index 000000000..0bb88b0e0 --- /dev/null +++ b/packages/pro/search/src/panel/CascaderPanel.tsx @@ -0,0 +1,110 @@ +/** + * @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, onUnmounted, watch } from 'vue' + +import { type VKey, callEmit } from '@idux/cdk/utils' +import { CascaderPanelProps, IxCascaderPanel } from '@idux/components/cascader' + +import PanelFooter from './PanelFooter' +import { proSearchContext } from '../token' +import { proSearchCascaderPanelProps } from '../types' + +export default defineComponent({ + props: proSearchCascaderPanelProps, + setup(props) { + const { mergedPrefixCls, locale } = inject(proSearchContext)! + + watch( + () => props.searchValue, + searchValue => { + callEmit(props.onSearch, searchValue ?? '') + }, + ) + onUnmounted(() => { + if (props.searchValue) { + callEmit(props.onSearch, '') + } + }) + + const changeSelected = (keys: VKey[] | VKey[] | VKey[][]) => { + callEmit(props.onChange, props.multiple ? keys : [keys]) + } + + const handleConfirm = () => { + callEmit(props.onConfirm) + } + const handleCancel = () => { + callEmit(props.onCancel) + } + + const renderFooter = () => { + if (!props.multiple) { + return + } + + return ( + + ) + } + + return () => { + const prefixCls = `${mergedPrefixCls.value}-cascader-panel` + const { + dataSource, + disableData, + expandIcon, + expandTrigger, + fullPath, + loadChildren, + multiple, + + searchValue, + searchFn, + separator, + strategy, + virtual, + + onExpand, + onLoaded, + } = props + const panelProps = { + selectedKeys: props.value, + dataSource, + disableData, + childrenKey: 'children', + getKey: 'key', + expandIcon, + expandTrigger, + fullPath, + loadChildren, + labelKey: 'label', + multiple, + searchFn, + searchValue, + separator, + strategy, + virtual, + onExpand, + onLoaded, + 'onUpdate:selectedKeys': changeSelected, + } as CascaderPanelProps + + return ( +
evt.preventDefault()}> + + {renderFooter()} +
+ ) + } + }, +}) diff --git a/packages/pro/search/src/panel/DatePickerPanel.tsx b/packages/pro/search/src/panel/DatePickerPanel.tsx index 1b0a4fed1..0cacbcbd1 100644 --- a/packages/pro/search/src/panel/DatePickerPanel.tsx +++ b/packages/pro/search/src/panel/DatePickerPanel.tsx @@ -16,6 +16,7 @@ import { IxDateRangePanel, } from '@idux/components/date-picker' +import PanelFooter from './PanelFooter' import { proSearchContext } from '../token' import { proSearchDatePanelProps } from '../types' @@ -63,17 +64,18 @@ export default defineComponent({ )}
- {props.type === 'datetime' && ( - - {visiblePanel.value === 'datePanel' ? locale.switchToTimePanel : locale.switchToDatePanel} - - )} - - {locale.ok} - - - {locale.cancel} - + + {props.type === 'datetime' && ( + + {visiblePanel.value === 'datePanel' ? locale.switchToTimePanel : locale.switchToDatePanel} + + )} +
) diff --git a/packages/pro/search/src/panel/PanelFooter.tsx b/packages/pro/search/src/panel/PanelFooter.tsx new file mode 100644 index 000000000..9b321287d --- /dev/null +++ b/packages/pro/search/src/panel/PanelFooter.tsx @@ -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 type { ProSearchPanelFooterProps } from '../types' + +import { type FunctionalComponent } from 'vue' + +import { IxButton } from '@idux/components/button' + +const PanelFooter: FunctionalComponent = (props, { slots }) => { + const { prefixCls, locale, onConfirm, onCancel } = props + + return ( +
+ {slots.default?.()} + + {locale!.ok} + + + {locale!.cancel} + +
+ ) +} + +export default PanelFooter diff --git a/packages/pro/search/src/panel/SelectPanel.tsx b/packages/pro/search/src/panel/SelectPanel.tsx index 78cfd6758..1fdcb7456 100644 --- a/packages/pro/search/src/panel/SelectPanel.tsx +++ b/packages/pro/search/src/panel/SelectPanel.tsx @@ -19,10 +19,10 @@ import { } from 'vue' import { type VKey, callEmit, useState } from '@idux/cdk/utils' -import { IxButton } from '@idux/components/button' import { IxCheckbox } from '@idux/components/checkbox' import { IxSelectPanel, type SelectData, type SelectPanelInstance } from '@idux/components/select' +import PanelFooter from './PanelFooter' import { proSearchContext } from '../token' import { type ProSearchSelectPanelProps, type SelectPanelData, proSearchSelectPanelProps } from '../types' import { filterDataSource, matchRule } from '../utils/selectData' @@ -135,14 +135,12 @@ export default defineComponent({ } return ( -
- - {locale.ok} - - - {locale.cancel} - -
+ ) } diff --git a/packages/pro/search/src/panel/TreeSelectPanel.tsx b/packages/pro/search/src/panel/TreeSelectPanel.tsx index ad39ee460..b37cb4136 100644 --- a/packages/pro/search/src/panel/TreeSelectPanel.tsx +++ b/packages/pro/search/src/panel/TreeSelectPanel.tsx @@ -10,9 +10,9 @@ import { type ComputedRef, computed, defineComponent, inject, onUnmounted, watch 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 PanelFooter from './PanelFooter' import { proSearchContext } from '../token' import { type ProSearchTreeSelectPanelProps, type TreeSelectPanelData, proSearchTreeSelectPanelProps } from '../types' @@ -76,20 +76,18 @@ export default defineComponent({ callEmit(props.onLoaded, loadedKeys, node) } - const renderFooter = (prefixCls: string) => { + const renderFooter = () => { if (!props.multiple) { return } return ( -
- - {locale.ok} - - - {locale.cancel} - -
+ ) } @@ -159,7 +157,7 @@ export default defineComponent({ return (
evt.preventDefault()}> - {renderFooter(prefixCls)} + {renderFooter()}
) } diff --git a/packages/pro/search/src/segments/CreateCascaderSegment.tsx b/packages/pro/search/src/segments/CreateCascaderSegment.tsx new file mode 100644 index 000000000..dd8a1438f --- /dev/null +++ b/packages/pro/search/src/segments/CreateCascaderSegment.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 type { CascaderPanelData, CascaderSearchField, PanelRenderContext, Segment } from '../types' + +import { ref } from 'vue' + +import { isNil, toString } from 'lodash-es' + +import { type VKey, convertArray, traverseTree } from '@idux/cdk/utils' +import { type TreeCheckStateResolver, useTreeCheckStateResolver } from '@idux/components/utils' + +import CascaderPanel from '../panel/CascaderPanel' +import { getSelectableCommonParams } from '../utils' + +const defaultSeparator = '|' +const defaultPathSeparator = '/' +const defaultCascaderStrategy = 'parent' +const defaultFullPath = false + +export function createCascaderSegment( + prefixCls: string, + searchField: CascaderSearchField, +): Segment { + const { + fieldConfig: { + dataSource, + cascaderStrategy, + expandIcon, + expandTrigger, + fullPath, + pathSeparator, + separator, + searchable, + searchFn, + multiple, + virtual, + onExpand, + onSearch, + onLoaded, + }, + defaultValue, + inputClassName, + onPanelVisibleChange, + } = searchField + + const nodeKeyMap = new Map() + const parentKeyMap = new Map() + const nodeLabelMap = new Map() + const depthMap = new Map() + + const mergedCascaderStrategy = cascaderStrategy ?? defaultCascaderStrategy + traverseTree(dataSource, 'children', (item, parents) => { + nodeKeyMap.set(item.key, item) + nodeLabelMap.set(toString(item.label).trim(), [...(nodeLabelMap.get(item.label) ?? []), item]) + parents[0] && parentKeyMap.set(item.key, parents[0].key) + depthMap.set(item.key, parents.length) + }) + const checkedKeysResolver = useTreeCheckStateResolver( + ref({ + data: dataSource, + dataMap: nodeKeyMap, + parentKeyMap, + depthMap, + }), + ref('children'), + ref((item: CascaderPanelData) => item.key), + ref(mergedCascaderStrategy), + ) + + const panelRenderer = (context: PanelRenderContext) => { + const { ok, cancel } = context + const { panelValue, searchInput, handleChange } = getSelectableCommonParams( + context, + !!multiple, + separator ?? defaultSeparator, + ) + + return ( + + ) + } + + return { + name: searchField.type, + inputClassName: [inputClassName, `${prefixCls}-cascader-segment-input`], + placeholder: searchField.placeholder, + defaultValue, + parse: input => parseInput(input, searchField, nodeLabelMap, checkedKeysResolver, parentKeyMap), + format: value => formatValue(value, searchField, nodeKeyMap), + panelRenderer, + onVisibleChange: onPanelVisibleChange, + } +} + +function parseInput( + input: string, + searchField: CascaderSearchField, + nodeLabelMap: Map, + checkedKeysResolver: TreeCheckStateResolver, + parentKeyMap: Map, +): VKey | (VKey | VKey[])[] | undefined { + const { fullPath, separator, multiple, pathSeparator } = searchField.fieldConfig + const trimedInput = input.trim() + + const keys = getKeyByLabels( + nodeLabelMap, + trimedInput.split(separator ?? defaultSeparator), + checkedKeysResolver, + parentKeyMap, + fullPath ?? defaultFullPath ? pathSeparator ?? defaultPathSeparator : undefined, + ) + + return multiple ? (keys.length > 0 ? keys : undefined) : keys[0] +} + +function formatValue( + value: VKey | (VKey | VKey[])[] | undefined, + searchField: CascaderSearchField, + nodeKeyMap: Map, +): string { + const { fullPath, multiple, separator, pathSeparator } = searchField.fieldConfig + if (isNil(value)) { + return '' + } + + return getLabelByKeys( + nodeKeyMap, + (multiple ? value : [value]) as VKey[] | VKey[][], + fullPath ?? defaultFullPath ? pathSeparator ?? defaultPathSeparator : undefined, + ).join(` ${separator ?? defaultSeparator} `) +} + +function getLabelByKeys( + nodeKeyMap: Map, + keys: VKey[] | VKey[][], + pathSeparator: string | undefined, +): string[] { + if (keys.length <= 0) { + return [] + } + + return keys + .map(_keys => + convertArray(_keys) + .map(key => nodeKeyMap.get(key)?.label) + .join(pathSeparator), + ) + .filter(Boolean) as string[] +} + +function getKeyByLabels( + nodeLabelMap: Map, + labels: string[], + checkedKeysResolver: TreeCheckStateResolver, + parentKeyMap: Map, + pathSeparator: string | undefined, +): (VKey | VKey[])[] { + if (labels.length <= 0) { + return [] + } + + const trimedLabels = labels.map(label => label.trim()) + + const keys = trimedLabels + .map(label => { + if (!pathSeparator) { + return nodeLabelMap.get(label)?.[0].key + } + + const separatedLabels = label.split(pathSeparator) + return separatedLabels.length > 0 && nodeLabelMap.get(separatedLabels.pop()!)?.[0].key + }) + .filter(Boolean) as VKey[] + + const checkedKeys = checkedKeysResolver.appendKeys([], keys) + + console.log(keys, checkedKeys) + + if (!pathSeparator) { + return checkedKeys + } + + const getKeys = (key: VKey) => { + const keys = [key] + let currentKey = key + + while (parentKeyMap.has(currentKey)) { + keys.unshift((currentKey = parentKeyMap.get(currentKey)!)) + } + + console.log('getKeys', keys) + return keys + } + + return checkedKeys.map(key => getKeys(key)) +} diff --git a/packages/pro/search/src/segments/CreateSelectSegment.tsx b/packages/pro/search/src/segments/CreateSelectSegment.tsx index fc0378ec1..d5fc4c280 100644 --- a/packages/pro/search/src/segments/CreateSelectSegment.tsx +++ b/packages/pro/search/src/segments/CreateSelectSegment.tsx @@ -12,7 +12,7 @@ import { isNil, toString } from 'lodash-es' import { type VKey, convertArray } from '@idux/cdk/utils' import SelectPanel from '../panel/SelectPanel' -import { filterDataSource, getSelectDataSourceKeys } from '../utils/selectData' +import { filterDataSource, getSelectDataSourceKeys, getSelectableCommonParams } from '../utils' const defaultSeparator = '|' @@ -28,35 +28,29 @@ export function createSelectSegment( } = searchField const panelRenderer = (context: PanelRenderContext) => { - const { input, value, setValue, ok, cancel, setOnKeyDown } = context - const panelValue = convertArray(value) + const { setValue, ok, cancel, setOnKeyDown } = context const keys = getSelectDataSourceKeys(dataSource) - const inputParts = input.trim().split(separator ?? defaultSeparator) - const lastInputPart = inputParts.length > panelValue.length ? inputParts.pop()?.trim() : '' - - const handleChange = (value: VKey[]) => { - if (!multiple) { - setValue(value[0]) - ok() - } else { - setValue(value.length > 0 ? value : undefined) - } - } + const { panelValue, searchInput, handleChange } = getSelectableCommonParams( + context, + !!multiple, + separator ?? defaultSeparator, + ) + const handleSelectAll = () => { const selectableKeys = getSelectDataSourceKeys(filterDataSource(dataSource, option => !option.disabled)) - setValue(selectableKeys.length !== panelValue.length ? selectableKeys : undefined) + setValue(selectableKeys.length !== panelValue?.length ? selectableKeys : undefined) } return ( 0 && keys.length <= panelValue.length} + allSelected={panelValue && panelValue.length > 0 && keys.length <= panelValue.length} dataSource={dataSource} multiple={multiple} virtual={virtual} setOnKeyDown={setOnKeyDown} showSelectAll={showSelectAll} - searchValue={searchable ? lastInputPart : ''} + searchValue={searchable ? searchInput : ''} searchFn={searchFn} onChange={handleChange} onSelectAllClick={handleSelectAll} diff --git a/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx b/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx index 93445d27d..a7c6ccf7a 100644 --- a/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx +++ b/packages/pro/search/src/segments/CreateTreeSelectSegment.tsx @@ -12,6 +12,7 @@ import { isNil, toString } from 'lodash-es' import { type VKey, convertArray, traverseTree } from '@idux/cdk/utils' import TreeSelectPanel from '../panel/TreeSelectPanel' +import { getSelectableCommonParams } from '../utils' const defaultSeparator = '|' @@ -57,24 +58,17 @@ export function createTreeSelectSegment( }) const panelRenderer = (context: PanelRenderContext) => { - const { input, value, setValue, ok, cancel } = context - const panelValue = convertArray(value) - const inputParts = input.trim().split(separator ?? defaultSeparator) - const lastInputPart = inputParts.length > panelValue.length ? inputParts.pop()?.trim() : '' - - const handleChange = (value: VKey[]) => { - if (!multiple) { - setValue(value[0]) - ok() - } else { - setValue(value.length > 0 ? value : undefined) - } - } + const { ok, cancel } = context + const { panelValue, searchInput, handleChange } = getSelectableCommonParams( + context, + !!multiple, + separator ?? defaultSeparator, + ) return ( > & SelectData @@ -20,6 +21,10 @@ export type TreeSelectPanelData = TreeSelectNode & Required> & { children?: TreeSelectPanelData[] } +export type CascaderPanelData = CascaderData & + Required> & { + children?: CascaderPanelData[] + } export const proSearchSelectPanelProps = { value: { type: Array as PropType, default: undefined }, @@ -80,6 +85,38 @@ export const proSearchTreeSelectPanelProps = { } as const export type ProSearchTreeSelectPanelProps = ExtractInnerPropTypes +export const proSearchCascaderPanelProps = { + value: { type: null, default: undefined }, + dataSource: { type: Array as PropType, default: () => [] }, + expandedKeys: { type: Array as PropType, default: undefined }, + disableData: { type: Function as PropType<(data: CascaderPanelData) => boolean> }, + expandIcon: { type: String, default: undefined }, + expandTrigger: { type: String as PropType, default: 'click' }, + fullPath: { type: Boolean, default: undefined }, + loadChildren: { + type: Function as PropType<(data: CascaderPanelData) => Promise>, + default: undefined, + }, + multiple: { type: Boolean, default: false }, + + searchFn: { + type: [Boolean, Function] as PropType boolean)>, + default: true, + }, + searchValue: String, + separator: { type: String, default: '/' }, + strategy: { type: String as PropType, default: 'all' }, + virtual: { type: Boolean, default: false }, + + onChange: [Function, Array] as PropType void>>, + onConfirm: [Function, Array] as PropType void>>, + onCancel: [Function, Array] as PropType void>>, + onExpand: [Function, Array] as PropType void>>, + onLoaded: [Function, Array] as PropType void>>, + onSearch: [Function, Array] as PropType void>>, +} as const +export type ProSearchCascaderPanelProps = ExtractInnerPropTypes + export const proSearchDatePanelProps = { panelType: { type: String as PropType<'datePicker' | 'dateRangePicker'>, @@ -101,3 +138,17 @@ export const proSearchDatePanelProps = { onCancel: Function as PropType<() => void>, } as const export type ProSearchDatePanelProps = ExtractInnerPropTypes + +export const proSearchPanelFooterProps = { + prefixCls: { + type: String, + required: true, + }, + locale: { + type: Object as PropType, + required: true, + }, + onConfirm: Function as PropType<() => void>, + onCancel: Function as PropType<() => void>, +} +export type ProSearchPanelFooterProps = ExtractInnerPropTypes diff --git a/packages/pro/search/src/types/searchFields.ts b/packages/pro/search/src/types/searchFields.ts index 1ab9a13be..3301123ab 100644 --- a/packages/pro/search/src/types/searchFields.ts +++ b/packages/pro/search/src/types/searchFields.ts @@ -7,12 +7,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { SelectPanelData, TreeSelectPanelData } from './panels' +import type { CascaderPanelData, SelectPanelData, TreeSelectPanelData } from './panels' import type { SearchItemError } from './searchItem' import type { SearchValue } from './searchValue' import type { InputFormater, InputParser, PanelRenderContext } from './segment' import type { MaybeArray, VKey } from '@idux/cdk/utils' -import type { CascaderStrategy } from '@idux/components/cascader' +import type { CascaderExpandTrigger, CascaderStrategy } from '@idux/components/cascader' import type { DatePanelProps, DateRangePanelProps } from '@idux/components/date-picker' import type { TreeDragDropOptions } from '@idux/components/tree' import type { VNodeChild } from 'vue' @@ -74,6 +74,27 @@ export interface TreeSelectSearchField extends SearchFieldBase { } } +export interface CascaderSearchField extends SearchFieldBase { + type: 'cascader' + fieldConfig: { + dataSource: CascaderPanelData[] + cascaderStrategy?: CascaderStrategy + multiple?: boolean + disableData?: (data: CascaderPanelData) => boolean + expandIcon?: string + expandTrigger?: CascaderExpandTrigger + fullPath?: boolean + pathSeparator?: string + searchable?: boolean + searchFn?: (node: CascaderPanelData, searchValue?: string) => boolean + separator?: string + virtual?: boolean + onExpand?: MaybeArray<(expanded: boolean, data: CascaderPanelData) => void> + onSearch?: MaybeArray<(searchValue: string) => void> + onLoaded?: MaybeArray<(loadedKeys: any[], node: TreeSelectPanelData) => void> + } +} + export interface InputSearchField extends SearchFieldBase { type: 'input' fieldConfig: { @@ -116,10 +137,19 @@ export interface CustomSearchField extends SearchFieldBase { export type SearchField = | SelectSearchField | TreeSelectSearchField + | CascaderSearchField | InputSearchField | DatePickerSearchField | DateRangePickerSearchField | CustomSearchField -export const searchDataTypes = ['select', 'treeSelect', 'input', 'datePicker', 'dateRangePicker', 'custom'] as const +export const searchDataTypes = [ + 'select', + 'treeSelect', + 'cascader', + 'input', + 'datePicker', + 'dateRangePicker', + 'custom', +] as const export type SearchDataTypes = (typeof searchDataTypes)[number] diff --git a/packages/pro/search/src/utils/getSelectableCommonParams.ts b/packages/pro/search/src/utils/getSelectableCommonParams.ts new file mode 100644 index 000000000..5d3e1b705 --- /dev/null +++ b/packages/pro/search/src/utils/getSelectableCommonParams.ts @@ -0,0 +1,59 @@ +/** + * @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 { PanelRenderContext } from '../types' + +import { convertArray } from '@idux/cdk/utils' + +export interface SearchablePanelParams { + panelValue: V[] | undefined + searchInput: string + handleChange: (v: V[] | undefined) => void +} + +export function getSelectableCommonParams( + context: PanelRenderContext>, + multiple: boolean, + separator?: string, +): SearchablePanelParams +export function getSelectableCommonParams( + context: PanelRenderContext, + multiple: boolean, + separator?: string, +): SearchablePanelParams +export function getSelectableCommonParams( + context: PanelRenderContext, + multiple: boolean, + separator?: string, +): SearchablePanelParams { + const { value, input, setValue, ok } = context + const panelValue = convertArray(value) + const trimedInput = input.trim() + let searchInput + + if (!separator) { + searchInput = trimedInput + } else { + const inputParts = trimedInput.split(separator) + searchInput = inputParts.length > panelValue.length ? inputParts.pop()?.trim() ?? '' : '' + } + + const handleChange = (v: T[] | undefined) => { + if (!multiple) { + setValue(v?.[0]) + ok() + } else { + setValue(v && v.length > 0 ? v : undefined) + } + } + + return { + panelValue, + searchInput, + handleChange, + } +} diff --git a/packages/pro/search/src/utils/index.ts b/packages/pro/search/src/utils/index.ts new file mode 100644 index 000000000..9ee91870c --- /dev/null +++ b/packages/pro/search/src/utils/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 './getSelectableCommonParams' +export * from './RenderIcon' +export * from './selectData' diff --git a/packages/pro/search/style/index.less b/packages/pro/search/style/index.less index 34d4d0e89..f40f3fb8f 100644 --- a/packages/pro/search/style/index.less +++ b/packages/pro/search/style/index.less @@ -161,7 +161,7 @@ &-invalid-tooltip { background-color: @pro-search-item-tag-invalid-tooltip-background-color; color: @pro-search-item-tag-invalid-tooltip-color; - + .@{overlay-prefix}-arrow { color: @pro-search-item-tag-invalid-tooltip-background-color; } @@ -210,38 +210,27 @@ } } - &-select-panel { - &-select-all-option { - .select-option(@select-option-font-size, @select-option-color); + &-panel-footer { + .panel-footer(); + } - border-bottom: @pro-search-border-width @pro-search-border-style @pro-search-border-color; + &-select-panel-select-all-option { + .select-option(@select-option-font-size, @select-option-color); - &-label { - margin-left: @select-option-label-margin-left; - } - } - &-footer { - .panel-footer(); + border-bottom: @pro-search-border-width @pro-search-border-style @pro-search-border-color; + + &-label { + margin-left: @select-option-label-margin-left; } } - &-tree-select-panel { - &-body { - padding: @spacing-sm 0; - .@{tree-node-prefix} { - padding: 0 @spacing-sm; - } - } - &-footer { - .panel-footer(); + &-tree-select-panel-body { + padding: @spacing-sm 0; + .@{tree-node-prefix} { + padding: 0 @spacing-sm; } } - &-date-picker-panel { - &-body { - padding: @pro-search-date-picker-panel-body-padding; - } - &-footer { - .panel-footer(); - } + &-date-picker-panel-body { + padding: @pro-search-date-picker-panel-body-padding; } &-name-segment-panel { @@ -278,6 +267,10 @@ min-width: @pro-search-tree-select-segment-input-min-width; text-align: @pro-search-tree-select-segment-input-text-align; } + &-cascader-segment-input { + min-width: @pro-search-cascader-segment-input-min-width; + text-align: @pro-search-cascader-segment-input-text-align; + } &-date-picker-segment-input { min-width: @pro-search-date-picker-segment-input-min-width; text-align: @pro-search-date-picker-segment-input-text-align; diff --git a/packages/pro/search/style/themes/default.variable.less b/packages/pro/search/style/themes/default.variable.less index 9e47ee390..7c9b12b1b 100644 --- a/packages/pro/search/style/themes/default.variable.less +++ b/packages/pro/search/style/themes/default.variable.less @@ -88,6 +88,8 @@ @pro-search-select-segment-input-text-align: start; @pro-search-tree-select-segment-input-min-width: 200px; @pro-search-tree-select-segment-input-text-align: start; +@pro-search-cascader-segment-input-min-width: 200px; +@pro-search-cascader-segment-input-text-align: start; @pro-search-date-picker-segment-input-min-width: 100px; @pro-search-date-picker-segment-input-text-align: start; @pro-search-date-range-picker-segment-input-min-width: 100px;