diff --git a/packages/components/cascader/__tests__/cascader.spec.ts b/packages/components/cascader/__tests__/cascader.spec.ts index 338da4e4b..70b1202a1 100644 --- a/packages/components/cascader/__tests__/cascader.spec.ts +++ b/packages/components/cascader/__tests__/cascader.spec.ts @@ -3,7 +3,7 @@ import { MountingOptions, VueWrapper, mount } from '@vue/test-utils' import { renderWork } from '@tests' import Cascader from '../src/Cascader' -import OverlayContent from '../src/contents/OverlayContent' +import Panel from '../src/panel/Panel' import { CascaderData, CascaderProps } from '../src/types' const defaultDataSource: CascaderData[] = [ @@ -125,8 +125,7 @@ const defaultMultipleValue = [ ] const defaultExpandedKeys = ['components', 'general'] -const getAllOptionGroup = (wrapper: VueWrapper) => - wrapper.findComponent(OverlayContent).findAll('.ix-cascader-option-group') +const getAllOptionGroup = (wrapper: VueWrapper) => wrapper.findComponent(Panel).findAll('.ix-cascader-option-group') describe('Cascader', () => { describe('single work', () => { @@ -200,7 +199,7 @@ describe('Cascader', () => { expect(getAllOptionGroup(wrapper).length).toBe(2) await wrapper - .findComponent(OverlayContent) + .findComponent(Panel) .findAll('.ix-cascader-option-group')[1] .find('.ix-cascader-option') .trigger('click') diff --git a/packages/components/cascader/demo/Panel.md b/packages/components/cascader/demo/Panel.md new file mode 100644 index 000000000..e5db52925 --- /dev/null +++ b/packages/components/cascader/demo/Panel.md @@ -0,0 +1,14 @@ +--- +order: 80 +title: + zh: 级联选择面板 + en: Cascader Panel +--- + +## zh + +单独使用级联选择面板。 + +## en + +use `IxCascaderPanel` only. diff --git a/packages/components/cascader/demo/Panel.vue b/packages/components/cascader/demo/Panel.vue new file mode 100644 index 000000000..7a19d041d --- /dev/null +++ b/packages/components/cascader/demo/Panel.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/packages/components/cascader/docs/Api.zh.md b/packages/components/cascader/docs/Api.zh.md index ccb17e4bb..516206afb 100644 --- a/packages/components/cascader/docs/Api.zh.md +++ b/packages/components/cascader/docs/Api.zh.md @@ -89,3 +89,47 @@ interface SelectedItemProps { | --- | --- | --- | --- | | `blur` | 失去焦点 | - | - | | `focus` | 获取焦点 | - | - | + +### IxCascaderPanel + +级联选择面板 + +#### CascaderPanelProps + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `v-model:selectedKeys` | 当前选中的的值 | `any \| any[] \| any[][]` | - | - | - | +| `v-model:expandedKeys` | 展开节点的 `key` 数组 | `VKey[]` | - | - | - | +| `v-model:loadedKeys` | 已经加载完毕的节点的 `key` | `VKey[]` | - | - | - | +| `childrenKey` | 替代[CascaderData](#CascaderData)中的`children`字段 | `string` | `children` | ✅ | - | +| `customAdditional` | 自定义下拉选项的额外属性 | `CascaderCustomAdditional` | - | - | 例如 `class`, 或者原生事件 | +| `dataSource` | 树型数据数组,参见[CascaderData](#CascaderData) | `CascaderData[]` | `[]` | - | - | +| `disabled` | 禁用选择器 | `boolean` | - | - | - | +| `disableData` | 动态禁用某些项 | `(data: CascaderData) => boolean` | - | - | - | +| `empty` | 空数据时的内容 | `'default' \| 'simple' \| EmptyProps` | `'simple'` | - | - | +| `expandIcon` | 展开图标 | `string \| #expandIcon="{key: VKey, expanded: boolean, data: CascaderData}"` | `right` | ✅ | - | +| `expandTrigger` | 触发展开的方式 | `'click' \| 'hover'` | `click` | - | - | +| `fullPath` | 选中后的值是否包含全部路径 | `boolean` | `true` | ✅ | 会影响值的类型,参见 [基本使用](#components-cascader-demo-Basic) 和 [多选模式](#components-cascader-demo-Multiple) | +| `getKey` | 获取数据的唯一标识 | `string \| (data: CascaderData) => VKey` | `key` | ✅ | - | +| `labelKey` | 替代[CascaderData](#CascaderData)中的`label`字段 | `string` | `label` | ✅ | - +| `loadChildren` | 加载子节点数据 | `(data: CascaderData) => Promise` | - | - | - | +| `maxLabel` | 最多显示多少个标签 | `number \| 'responsive'` | - | - | 响应式模式会对性能产生损耗 | +| `multiple` | 多选模式 | `boolean` | `false` | - | - | +| `multipleLimit` | 最多选中多少项 | `number` | - | - | - | +| `searchable` | 是否可搜索 | `boolean \| 'overlay'` | `false` | - | 当为 `true` 时搜索功能集成在选择器上,当为 `overlay` 时,搜索功能集成在悬浮层上 | +| `searchFn` | 根据搜索的文本进行筛选 | `boolean \| SelectSearchFn` | `true` | - | 为 `true` 时使用默认的搜索规则, 如果使用远程搜索,应该设置为 `false` | +| `separator` | 设置分割符 | `string` | `/` | - | - | +| `strategy` | 设置级联策略 | `'all' \| 'parent' \| 'child' \| 'off'` | `'all'` | - | 具体用法参见 [级联策略](#components-cascader-demo-Strategy) | +| `virtual` | 是否开启虚拟滚动 | `boolean` | `false` | - | 需要设置 `height` | +| `onSelect` | 选中值触发 | `(option: CascaderData, oldValue: any) => void` | - | - | - | +| `onExpand` | 点击展开图标时触发 | `(expanded: boolean, isSelected: boolean) => void` | - | - | - | +| `onExpandedChange` | 展开状态发生变化时触发 | `(expendedKeys: VKey[], expendedData: CascaderData[]) => void` | - | - | - | +| `onLoaded` | 子节点加载完毕时触发 | `(loadedKeys: VKey[], data: CascaderData) => void` | - | - | - | +| `onSearch` | 开启搜索功能后,输入后的回调 | `(searchValue: string) => void` | - | - | 通常用于服务端搜索 | + +#### CascaderPanelSlots + +| 名称 | 说明 | 参数类型 | 备注 | +| --- | --- | --- | --- | +| `empty` | 自定义空状态 | - | - | +| `optionLabel` | 自定义选项的文本 | `data: SelectOption` | - | diff --git a/packages/components/cascader/index.ts b/packages/components/cascader/index.ts index 1a10be50f..2f6601b06 100644 --- a/packages/components/cascader/index.ts +++ b/packages/components/cascader/index.ts @@ -5,18 +5,23 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { CascaderComponent } from './src/types' +import type { CascaderComponent, CascaderPanelComponent } from './src/types' import Cascader from './src/Cascader' +import CascaderPanel from './src/panel/Panel' const IxCascader = Cascader as unknown as CascaderComponent +const IxCascaderPanel = CascaderPanel as unknown as CascaderPanelComponent -export { IxCascader } +export { IxCascader, IxCascaderPanel } export type { CascaderInstance, CascaderComponent, CascaderPublicProps as CascaderProps, + CascaderPanelInstance, + CascaderPanelComponent, + CascaderPanelPublicProps as CascaderPanelProps, CascaderData, CascaderExpandTrigger, CascaderSearchFn, diff --git a/packages/components/cascader/src/Cascader.tsx b/packages/components/cascader/src/Cascader.tsx index 849e3a19a..15073d340 100644 --- a/packages/components/cascader/src/Cascader.tsx +++ b/packages/components/cascader/src/Cascader.tsx @@ -5,10 +5,11 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, normalizeClass, provide, ref, watch } from 'vue' +import { computed, defineComponent, normalizeClass, provide, ref, toRef, watch } from 'vue' import { useAccessorAndControl } from '@idux/cdk/forms' -import { type VKey, useState } from '@idux/cdk/utils' +import { type VKey, callEmit, useState } from '@idux/cdk/utils' +import { ɵInput } from '@idux/components/_private/input' import { ɵOverlay } from '@idux/components/_private/overlay' import { ɵSelector, type ɵSelectorInstance } from '@idux/components/_private/selector' import { useGlobalConfig } from '@idux/components/config' @@ -16,13 +17,12 @@ import { useFormItemRegister, useFormSize, useFormStatus } from '@idux/component import { ɵUseOverlayState } from '@idux/components/select' import { useGetDisabled, useGetKey } from '@idux/components/utils' -import { useActiveState } from './composables/useActiveState' import { useDataSource } from './composables/useDataSource' -import { useExpandable } from './composables/useExpandable' -import { useSearchable } from './composables/useSearchable' +import { usePanelProps } from './composables/usePanelProps' +import { useSelectedData } from './composables/useSelectedData' import { useSelectedState } from './composables/useSelectedState' -import OverlayContent from './contents/OverlayContent' -import { cascaderToken } from './token' +import Panel from './panel/Panel' +import { CASCADER_PANEL_DATA_TOKEN } from './token' import { cascaderProps } from './types' const defaultOffset: [number, number] = [0, 4] @@ -38,7 +38,6 @@ export default defineComponent({ const mergedChildrenKey = computed(() => props.childrenKey ?? config.childrenKey) const mergedClearIcon = computed(() => props.clearIcon ?? config.clearIcon) - const mergedExpandIcon = computed(() => props.expandIcon ?? config.expandIcon) const mergedFullPath = computed(() => props.fullPath ?? config.fullPath) const mergedGetKey = useGetKey(props, config, 'components/cascader') const mergedGetDisabled = useGetDisabled(props) @@ -61,33 +60,19 @@ export default defineComponent({ const mergedSize = useFormSize(props, config) const mergedStatus = useFormStatus(props, control) - const { mergedData, mergedDataMap } = useDataSource( - props, - mergedGetKey, - mergedChildrenKey, - mergedLabelKey, - mergedFullPath, - ) - const activeStateContext = useActiveState(props, mergedDataMap) - const selectedStateContext = useSelectedState(props, accessor, mergedDataMap, mergedFullPath, mergedGetDisabled) - const { searchedData } = useSearchable( - props, - mergedData, + const dataSourceContext = useDataSource(props, mergedGetKey, mergedChildrenKey, mergedLabelKey, mergedFullPath) + const { mergedDataMap } = dataSourceContext + const selectedStateContext = useSelectedState( mergedDataMap, - mergedLabelKey, - inputValue, - mergedGetDisabled, - ) - const expandableContext = useExpandable( - props, - mergedGetKey, - mergedGetDisabled, - mergedChildrenKey, - mergedLabelKey, mergedFullPath, - mergedDataMap, - selectedStateContext.selectedKeys, + mergedGetDisabled, + toRef(props, 'multiple'), + toRef(props, 'strategy'), + toRef(accessor, 'value'), + keys => accessor.setValue(keys), ) + const { resolvedSelectedKeys, setValue } = selectedStateContext + const selectedData = useSelectedData(resolvedSelectedKeys, mergedDataMap) watch(overlayOpened, opened => { opened && focus() @@ -105,31 +90,15 @@ export default defineComponent({ focus() selectedStateContext.handleSelect(key) } + const handleClear = (evt: MouseEvent) => { + evt.stopPropagation() + setValue([]) + callEmit(props.onClear, evt) + } - provide(cascaderToken, { - props, - slots, - config, - mergedPrefixCls, - mergedChildrenKey, - mergedClearIcon, - mergedExpandIcon, - mergedFullPath, - mergedGetKey, - mergedGetDisabled, - mergedLabelKey, - accessor, - inputValue, - setInputValue, - overlayOpened, - setOverlayOpened, - updateOverlay, - mergedData, - mergedDataMap, - ...activeStateContext, + provide(CASCADER_PANEL_DATA_TOKEN, { + ...dataSourceContext, ...selectedStateContext, - searchedData, - ...expandableContext, }) const overlayClasses = computed(() => { @@ -152,9 +121,9 @@ export default defineComponent({ autofocus={props.autofocus} borderless={props.borderless} clearable={props.clearable} - clearIcon={props.clearIcon} + clearIcon={mergedClearIcon.value} config={config} - dataSource={selectedStateContext.selectedData.value} + dataSource={selectedData.value} disabled={accessor.disabled} maxLabel={props.maxLabel} multiple={props.multiple} @@ -165,12 +134,11 @@ export default defineComponent({ size={mergedSize.value} status={mergedStatus.value} suffix={props.suffix} - value={selectedStateContext.selectedKeys.value} + value={resolvedSelectedKeys.value} onBlur={handleBlur} - onClear={selectedStateContext.handleClear} + onClear={handleClear} onInputValueChange={setInputValue} onItemRemove={handleItemRemove} - //onKeydown={handleKeyDown} onOpenedChange={setOverlayOpened} onResize={updateOverlay} onSearch={props.onSearch} @@ -178,7 +146,44 @@ export default defineComponent({ /> ) - const renderContent = () => + const panelProps = usePanelProps(props, setOverlayOpened) + const handleSearchInput = (evt: Event) => { + const { value } = evt.target as HTMLInputElement + setInputValue(value) + props.searchable && callEmit(props.onSearch, value) + } + const handleSearchClear = () => setInputValue('') + const renderContent = () => { + const { searchable, overlayRender } = props + const searchValue = inputValue.value + const prefixCls = mergedPrefixCls.value + const panelSlots = { empty: slots.empty, optionLabel: slots.optionLabel } + + const children = [ +
+ +
, + ] + + if (searchable === 'overlay') { + children.unshift( +
+ <ɵInput + clearable + clearIcon={mergedClearIcon.value} + clearVisible={!!searchValue} + size="sm" + suffix="search" + value={searchValue} + onClear={handleSearchClear} + onInput={handleSearchInput} + /> +
, + ) + } + + return
{overlayRender ? overlayRender(children) : children}
+ } return () => { const overlayProps = { diff --git a/packages/components/cascader/src/composables/useActiveState.ts b/packages/components/cascader/src/composables/useActiveState.ts index f0fc6048d..4efc6ef2b 100644 --- a/packages/components/cascader/src/composables/useActiveState.ts +++ b/packages/components/cascader/src/composables/useActiveState.ts @@ -10,7 +10,6 @@ import { type ComputedRef, type Ref, computed } from 'vue' import { type VKey, useState } from '@idux/cdk/utils' import { type MergedData } from './useDataSource' -import { type CascaderProps } from '../types' export interface ActiveStateContext { activeKey: Ref @@ -18,10 +17,7 @@ export interface ActiveStateContext { setActiveKey: (key: VKey) => void } -export function useActiveState( - props: CascaderProps, - mergedDataMap: ComputedRef>, -): ActiveStateContext { +export function useActiveState(mergedDataMap: ComputedRef>): ActiveStateContext { const [activeKey, setActiveKey] = useState(undefined) const activeData = computed(() => { diff --git a/packages/components/cascader/src/composables/useDataSource.ts b/packages/components/cascader/src/composables/useDataSource.ts index dc56e4508..0ed967ee7 100644 --- a/packages/components/cascader/src/composables/useDataSource.ts +++ b/packages/components/cascader/src/composables/useDataSource.ts @@ -12,7 +12,7 @@ import { isNil } from 'lodash-es' import { type VKey } from '@idux/cdk/utils' import { type GetKeyFn } from '@idux/components/utils' -import { type CascaderData, type CascaderProps } from '../types' +import { type CascaderData, type CascaderPanelProps, type CascaderProps } from '../types' export interface MergedData { children?: MergedData[] @@ -29,7 +29,7 @@ export interface DataSourceContext { } export function useDataSource( - props: CascaderProps, + props: CascaderProps | CascaderPanelProps, mergedGetKey: ComputedRef, mergedChildrenKey: ComputedRef, mergedLabelKey: ComputedRef, @@ -56,7 +56,7 @@ export function useDataSource( } export function convertMergedData( - props: CascaderProps, + props: CascaderProps | CascaderPanelProps, getKey: GetKeyFn, childrenKey: string, labelKey: string, diff --git a/packages/components/cascader/src/composables/useExpandable.ts b/packages/components/cascader/src/composables/useExpandable.ts index 094ebef01..9614611bf 100644 --- a/packages/components/cascader/src/composables/useExpandable.ts +++ b/packages/components/cascader/src/composables/useExpandable.ts @@ -13,7 +13,7 @@ import { type VKey, callEmit, useControlledProp } from '@idux/cdk/utils' import { type GetDisabledFn, type GetKeyFn } from '@idux/components/utils' import { type MergedData, convertMergedData, convertMergedDataMap } from './useDataSource' -import { type CascaderData, type CascaderProps } from '../types' +import { type CascaderData, type CascaderPanelProps, type CascaderProps } from '../types' import { callChange, getParentKeys } from '../utils' export interface ExpandableContext { @@ -24,7 +24,7 @@ export interface ExpandableContext { } export function useExpandable( - props: CascaderProps, + props: CascaderProps | CascaderPanelProps, mergedGetKey: ComputedRef, mergedGetDisabled: ComputedRef, mergedChildrenKey: ComputedRef, diff --git a/packages/components/cascader/src/composables/useIndeterminateKeys.ts b/packages/components/cascader/src/composables/useIndeterminateKeys.ts new file mode 100644 index 000000000..65c8a8445 --- /dev/null +++ b/packages/components/cascader/src/composables/useIndeterminateKeys.ts @@ -0,0 +1,44 @@ +/** + * @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 } from 'vue' + +import { isNil } from 'lodash-es' + +import { NoopArray, type VKey } from '@idux/cdk/utils' +import { type GetDisabledFn } from '@idux/components/utils' + +import { type MergedData } from './useDataSource' + +export function useIndeterminateKeys( + mergedDataMap: ComputedRef>, + selectedWithStrategyKeys: ComputedRef, + mergedGetDisabled: ComputedRef, + strategyEnabled: ComputedRef, +): ComputedRef { + return computed(() => { + if (!strategyEnabled.value) { + return NoopArray as unknown as VKey[] + } + const indeterminateKeySet = new Set() + const cascadedKeys = selectedWithStrategyKeys.value + const dataMap = mergedDataMap.value + const getDisabledFn = mergedGetDisabled.value + cascadedKeys.forEach(key => { + let currData = dataMap.get(key) + while (currData && !isNil(currData.parentKey)) { + const parentKey = currData.parentKey + const parent = dataMap.get(parentKey) + if (parent && !getDisabledFn(parent.rawData) && !cascadedKeys.includes(parentKey)) { + indeterminateKeySet.add(parentKey) + } + currData = parent + } + }) + return [...indeterminateKeySet] + }) +} diff --git a/packages/components/cascader/src/composables/usePanelProps.ts b/packages/components/cascader/src/composables/usePanelProps.ts new file mode 100644 index 000000000..6cf24968d --- /dev/null +++ b/packages/components/cascader/src/composables/usePanelProps.ts @@ -0,0 +1,53 @@ +/** + * @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 { CascaderPanelProps, CascaderProps } from '../types' + +import { type ComputedRef, computed } from 'vue' + +import { useControlledProp } from '@idux/cdk/utils' + +export function usePanelProps( + props: CascaderProps, + setOverlayOpened: (opened: boolean) => void, +): ComputedRef> { + const [expandedKeys, setExpandedKeys] = useControlledProp(props, 'expandedKeys') + const [loadedKeys, setLoadedKeys] = useControlledProp(props, 'loadedKeys') + const onSelect = () => { + if (!props.multiple) { + setOverlayOpened(false) + } + } + return computed(() => ({ + expandedKeys: expandedKeys.value, + loadedKeys: loadedKeys.value, + childrenKey: props.childrenKey, + customAdditional: props.customAdditional, + disableData: props.disableData, + empty: props.empty, + expandIcon: props.expandIcon, + expandTrigger: props.expandTrigger, + + fullPath: props.fullPath, + getKey: props.getKey, + labelKey: props.labelKey, + loadChildren: props.loadChildren, + + maxLabel: props.maxLabel, + multiple: props.multiple, + multipleLimit: props.multipleLimit, + + searchable: props.searchable, + searchFn: props.searchFn, + strategy: props.strategy, + virtual: props.virtual, + + 'onUpdate:expandedKeys': setExpandedKeys, + 'onUpdate:loadedKeys': setLoadedKeys, + onSelect, + })) +} diff --git a/packages/components/cascader/src/composables/useSearchable.ts b/packages/components/cascader/src/composables/useSearchable.ts index 51e1e78e9..4b80aaea2 100644 --- a/packages/components/cascader/src/composables/useSearchable.ts +++ b/packages/components/cascader/src/composables/useSearchable.ts @@ -13,25 +13,24 @@ import { NoopArray, type VKey } from '@idux/cdk/utils' import { type GetDisabledFn } from '@idux/components/utils' import { type MergedData } from './useDataSource' -import { type CascaderData, type CascaderProps, type CascaderSearchFn } from '../types' +import { type CascaderData, type CascaderPanelProps, type CascaderSearchFn } from '../types' export interface SearchableContext { searchedData: ComputedRef } export function useSearchable( - props: CascaderProps, + props: CascaderPanelProps, mergedData: ComputedRef, mergedDataMap: ComputedRef>, mergedLabelKey: ComputedRef, - inputValue: ComputedRef, mergedGetDisabled: ComputedRef, ): SearchableContext { const mergedSearchFn = useSearchFn(props, mergedLabelKey) const parentEnabled = computed(() => props.multiple || props.strategy === 'off') const searchedKeys = computed(() => { - const searchValue = inputValue.value + const searchValue = props.searchValue const searchFn = mergedSearchFn.value if (!searchValue || !searchFn) { return NoopArray as unknown as VKey[] @@ -51,7 +50,7 @@ export function useSearchable( return { searchedData } } -function useSearchFn(props: CascaderProps, mergedLabelKey: ComputedRef) { +function useSearchFn(props: CascaderPanelProps, mergedLabelKey: ComputedRef) { return computed(() => { const searchFn = props.searchFn if (isFunction(searchFn)) { diff --git a/packages/components/cascader/src/composables/useSelectedData.ts b/packages/components/cascader/src/composables/useSelectedData.ts new file mode 100644 index 000000000..54970d370 --- /dev/null +++ b/packages/components/cascader/src/composables/useSelectedData.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 { MergedData } from './useDataSource' +import type { VKey } from '@idux/cdk/utils' + +import { type ComputedRef, computed } from 'vue' + +export function useSelectedData( + selectedKeys: ComputedRef, + mergedDataMap: ComputedRef>, +): ComputedRef<(MergedData & { label: string })[]> { + return computed(() => { + const dataMap = mergedDataMap.value + return selectedKeys.value.map(key => dataMap.get(key)!).filter(Boolean) + }) +} diff --git a/packages/components/cascader/src/composables/useSelectedLimit.ts b/packages/components/cascader/src/composables/useSelectedLimit.ts new file mode 100644 index 000000000..3c9c2444d --- /dev/null +++ b/packages/components/cascader/src/composables/useSelectedLimit.ts @@ -0,0 +1,34 @@ +/** + * @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 { VKey } from '@idux/cdk/utils' + +import { type ComputedRef, type Ref, computed } from 'vue' + +import { useGlobalConfig } from '@idux/components/config' + +export interface SelectedLimitContext { + selectedLimit: ComputedRef + selectedLimitTitle: ComputedRef +} + +export function useSelectedLimit(selectedKeys: ComputedRef, multipleLimit: Ref): SelectedLimitContext { + const locale = useGlobalConfig('locale') + + const selectedLimit = computed(() => selectedKeys.value.length >= multipleLimit.value) + const selectedLimitTitle = computed(() => { + if (!selectedLimit.value) { + return '' + } + return locale.select.limitMessage.replace('${0}', `${multipleLimit.value}`) + }) + + return { + selectedLimit, + selectedLimitTitle, + } +} diff --git a/packages/components/cascader/src/composables/useSelectedState.ts b/packages/components/cascader/src/composables/useSelectedState.ts index 4fcbab016..2a9a56609 100644 --- a/packages/components/cascader/src/composables/useSelectedState.ts +++ b/packages/components/cascader/src/composables/useSelectedState.ts @@ -5,68 +5,53 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { type ComputedRef, computed, toRaw } from 'vue' +import { type ComputedRef, type Ref, computed, toRaw } from 'vue' import { isNil } from 'lodash-es' -import { type FormAccessor } from '@idux/cdk/forms' -import { NoopArray, type VKey, callEmit, convertArray } from '@idux/cdk/utils' -import { useGlobalConfig } from '@idux/components/config' +import { NoopArray, type VKey, convertArray } from '@idux/cdk/utils' import { type GetDisabledFn } from '@idux/components/utils' import { type MergedData } from './useDataSource' -import { type CascaderProps, type CascaderStrategy } from '../types' +import { type CascaderStrategy } from '../types' import { getChildrenKeys, getParentKeys } from '../utils' export interface SelectedStateContext { - selectedKeys: ComputedRef - selectedLimit: ComputedRef - selectedLimitTitle: ComputedRef - selectedData: ComputedRef + resolvedSelectedKeys: ComputedRef selectedWithStrategyKeys: ComputedRef - indeterminateKeys: ComputedRef + strategyEnabled: ComputedRef handleSelect: (key: VKey) => void - handleClear: (evt: MouseEvent) => void + setValue: (keys: VKey[]) => void } export function useSelectedState( - props: CascaderProps, - accessor: FormAccessor, mergedDataMap: ComputedRef>, mergedFullPath: ComputedRef, mergedGetDisabled: ComputedRef, + multiple: Ref, + strategy: Ref, + selectedKeys: Ref, + setSelectedKeys: (keys: VKey | VKey[] | VKey[][]) => void, ): SelectedStateContext { - const locale = useGlobalConfig('locale') - const selectedKeys = computed(() => { - const tempKeys = convertArray(accessor.value) + const resolvedSelectedKeys = computed(() => { + const tempKeys = convertArray(selectedKeys.value) if (!mergedFullPath.value) { - return tempKeys + return tempKeys as VKey[] } // 单选直接拿最后一个值最为选中的 key - if (!props.multiple) { - const lastKey = tempKeys[tempKeys.length - 1] + if (!multiple.value) { + const lastKey = tempKeys[tempKeys.length - 1] as VKey return isNil(lastKey) ? [] : [lastKey] } // 多选时 tempKeys 应该是一个二维数组,然后拿第二层的最后一个元素作为 key - return tempKeys.map(keys => keys[keys.length - 1]).filter(key => !isNil(key)) - }) - const selectedLimit = computed(() => selectedKeys.value.length >= props.multipleLimit) - const selectedLimitTitle = computed(() => { - if (!selectedLimit.value) { - return '' - } - return locale.select.limitMessage.replace('${0}', `${props.multipleLimit}`) + return (tempKeys as VKey[][]).map(keys => keys[keys.length - 1]).filter(key => !isNil(key)) as VKey[] }) - const selectedData = computed(() => { - const dataMap = mergedDataMap.value - return selectedKeys.value.map(key => dataMap.get(key)!).filter(Boolean) - }) - const strategyEnabled = computed(() => props.multiple && props.strategy !== 'off') + const strategyEnabled = computed(() => multiple.value && strategy.value !== 'off') const selectedWithStrategyKeys = computed(() => { return strategyEnabled.value - ? getCascadedKeys(mergedDataMap.value, selectedKeys.value, mergedGetDisabled.value) - : selectedKeys.value + ? getCascadedKeys(mergedDataMap.value, resolvedSelectedKeys.value, mergedGetDisabled.value) + : resolvedSelectedKeys.value }) const indeterminateKeys = computed(() => { @@ -95,10 +80,10 @@ export function useSelectedState( let currValue: VKey | VKey[] | VKey[][] if (!mergedFullPath.value) { - currValue = props.multiple ? keys : keys[0] + currValue = multiple.value ? keys : keys[0] } else { const getDisabledFn = mergedGetDisabled.value - if (!props.multiple) { + if (!multiple.value) { const currKey = keys[0] const dataMap = mergedDataMap.value currValue = getParentKeys(dataMap, dataMap.get(currKey), true, getDisabledFn) @@ -113,20 +98,18 @@ export function useSelectedState( } } - const oldValue = toRaw(accessor.value) + const oldValue = toRaw(selectedKeys.value) if (currValue !== oldValue) { - accessor.setValue(currValue) - callEmit(props.onChange, currValue, oldValue) + setSelectedKeys(currValue) } } const handleSelect = (key: VKey) => { - const { multiple } = props const cascadedKeys = selectedWithStrategyKeys.value const currIndex = cascadedKeys.indexOf(key) const isSelected = currIndex > -1 - if (!multiple) { + if (!multiple.value) { !isSelected && setValue([key]) return } @@ -154,25 +137,15 @@ export function useSelectedState( childrenKeys.forEach(key => keySet.add(key)) } } - setValue([...getCascadedKeysByStrategy(dataMap, keySet, props.strategy)]) - } - - const handleClear = (evt: MouseEvent) => { - evt.stopPropagation() - setValue([]) - callEmit(props.onClear, evt) + setValue([...getCascadedKeysByStrategy(dataMap, keySet, strategy.value)]) } return { - selectedKeys, - selectedLimit, - selectedLimitTitle, - selectedData, + resolvedSelectedKeys, selectedWithStrategyKeys, - indeterminateKeys, + strategyEnabled, handleSelect, - - handleClear, + setValue, } } diff --git a/packages/components/cascader/src/contents/OverlayContent.tsx b/packages/components/cascader/src/contents/OverlayContent.tsx deleted file mode 100644 index 2884a0d89..000000000 --- a/packages/components/cascader/src/contents/OverlayContent.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @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 VNode, computed, defineComponent, inject, watch } from 'vue' - -import { type VKey, callEmit } from '@idux/cdk/utils' -import { ɵInput } from '@idux/components/_private/input' - -import OverlayOptionGroup from './OverlayOptionGroup' -import { cascaderToken } from '../token' - -export default defineComponent({ - setup() { - const { - props, - mergedPrefixCls, - mergedClearIcon, - mergedData, - mergedDataMap, - searchedData, - expandedKeys, - inputValue, - setInputValue, - updateOverlay, - } = inject(cascaderToken)! - - const defaultKey = Symbol() as VKey - const contentData = computed(() => { - const dataSource = [{ key: defaultKey, dataSource: mergedData.value }] - const dataMap = mergedDataMap.value - expandedKeys.value.forEach(key => { - const currData = dataMap.get(key) - if (currData && currData.children) { - dataSource.push({ key, dataSource: currData.children }) - } - }) - return dataSource - }) - - watch([() => contentData.value.length, inputValue], () => { - updateOverlay() - }) - - const handleInput = (evt: Event) => { - const { value } = evt.target as HTMLInputElement - setInputValue(value) - props.searchable && callEmit(props.onSearch, value) - } - const handleClear = () => setInputValue('') - - return () => { - const { overlayRender } = props - const searchValue = inputValue.value - const prefixCls = mergedPrefixCls.value - const children: VNode[] = [] - - if (props.searchable === 'overlay') { - children.push( -
- <ɵInput - clearable - clearIcon={mergedClearIcon.value} - clearVisible={!!searchValue} - size="sm" - suffix="search" - value={searchValue} - onClear={handleClear} - onInput={handleInput} - /> -
, - ) - } - - const contentNode = searchValue ? ( - - ) : ( - contentData.value.map(item => ) - ) - children.push( -
- {contentNode} -
, - ) - - return
{overlayRender ? overlayRender(children) : children}
- } - }, -}) diff --git a/packages/components/cascader/src/contents/OverlayOption.tsx b/packages/components/cascader/src/panel/Option.tsx similarity index 86% rename from packages/components/cascader/src/contents/OverlayOption.tsx rename to packages/components/cascader/src/panel/Option.tsx index e9c7c9ea7..95b3546c5 100644 --- a/packages/components/cascader/src/contents/OverlayOption.tsx +++ b/packages/components/cascader/src/panel/Option.tsx @@ -9,11 +9,12 @@ import { type PropType, Slot, computed, defineComponent, inject, normalizeClass import { isNil } from 'lodash-es' +import { callEmit } from '@idux/cdk/utils' import { IxCheckbox } from '@idux/components/checkbox' import { convertIconVNode, useKey } from '@idux/components/utils' import { type MergedData } from '../composables/useDataSource' -import { cascaderToken } from '../token' +import { cascaderPanelToken } from '../token' import { type CascaderData } from '../types' export default defineComponent({ @@ -28,18 +29,16 @@ export default defineComponent({ setup(props) { const key = useKey() const { - props: cascaderProps, + props: cascaderPanelProps, slots, mergedPrefixCls, mergedExpandIcon, mergedGetDisabled, mergedLabelKey, - inputValue, activeKey, setActiveKey, expandedKeys, setExpandedKeys, - setOverlayOpened, loadingKeys, selectedWithStrategyKeys, selectedLimit, @@ -47,7 +46,7 @@ export default defineComponent({ indeterminateKeys, handleSelect, handleExpand, - } = inject(cascaderToken)! + } = inject(cascaderPanelToken)! const isActive = computed(() => key === activeKey.value) const isDisabled = computed(() => mergedGetDisabled.value(props.rawData)) const isExpanded = computed(() => expandedKeys.value.includes(key)) @@ -68,44 +67,48 @@ export default defineComponent({ }) }) + const _handleSelect = () => { + handleSelect(key) + callEmit(cascaderPanelProps.onSelect, props.rawData, isSelected.value) + } + const handleClick = () => { if (props.isLeaf) { if (!isSelected.value && selectedLimit.value) { return } - handleSelect(key) - setOverlayOpened(false) + _handleSelect() // 如果一级节点是叶子节点,被点击后关闭所有展开的节点。 isNil(props.parentKey) && setExpandedKeys([]) } else { - cascaderProps.strategy === 'off' && handleSelect(key) - cascaderProps.expandTrigger === 'click' && handleExpand(key) + cascaderPanelProps.strategy === 'off' && _handleSelect() + cascaderPanelProps.expandTrigger === 'click' && handleExpand(key) } } const handleCheckboxClick = (evt: Event) => { evt.stopPropagation() - handleSelect(key) + _handleSelect() } const handleMouseEnter = () => { setActiveKey(key) - !props.isLeaf && cascaderProps.expandTrigger === 'hover' && handleExpand(key) + !props.isLeaf && cascaderPanelProps.expandTrigger === 'hover' && handleExpand(key) } return () => { const { rawData, label } = props - const { multiple } = cascaderProps + const { multiple } = cascaderPanelProps const disabled = isDisabled.value const selected = isSelected.value const prefixCls = `${mergedPrefixCls.value}-option` + const searchValue = cascaderPanelProps.searchValue - const searchValue = inputValue.value const mergedLabel = searchValue ? label : (rawData[mergedLabelKey.value] as string) // 优先显示 selectedLimitTitle const title = (!(disabled || selected) && selectedLimitTitle.value) || mergedLabel - const customAdditional = cascaderProps.customAdditional - ? cascaderProps.customAdditional({ data: rawData, index: props.index }) + const customAdditional = cascaderPanelProps.customAdditional + ? cascaderPanelProps.customAdditional({ data: rawData, index: props.index }) : undefined return ( diff --git a/packages/components/cascader/src/contents/OverlayOptionGroup.tsx b/packages/components/cascader/src/panel/OptionGroup.tsx similarity index 73% rename from packages/components/cascader/src/contents/OverlayOptionGroup.tsx rename to packages/components/cascader/src/panel/OptionGroup.tsx index 0fa7d8e5c..8beea2438 100644 --- a/packages/components/cascader/src/contents/OverlayOptionGroup.tsx +++ b/packages/components/cascader/src/panel/OptionGroup.tsx @@ -10,16 +10,16 @@ import { type PropType, type VNode, computed, defineComponent, inject, normalize import { CdkVirtualScroll, type VirtualItemRenderFn } from '@idux/cdk/scroll' import { ɵEmpty } from '@idux/components/_private/empty' -import OverlayOption from './OverlayOption' +import Option from './Option' import { type MergedData } from '../composables/useDataSource' -import { cascaderToken } from '../token' +import { cascaderPanelToken } from '../token' export default defineComponent({ props: { dataSource: { type: Array as PropType, required: true }, }, setup(props) { - const { props: cascaderProps, slots, mergedPrefixCls } = inject(cascaderToken)! + const { props: cascaderPanelProps, slots, mergedPrefixCls } = inject(cascaderPanelToken)! const classes = computed(() => { const prefixCls = `${mergedPrefixCls.value}-option-group` @@ -33,22 +33,20 @@ export default defineComponent({ const { dataSource } = props let children: VNode if (dataSource.length > 0) { - const { overlayHeight, overlayItemHeight, virtual } = cascaderProps - const itemRender: VirtualItemRenderFn = ({ item, index }) => ( - - ) + const { _virtualScrollHeight, _virtualScrollItemHeight, virtual } = cascaderPanelProps + const itemRender: VirtualItemRenderFn = ({ item, index }) =>