Skip to content

Commit b092d17

Browse files
uyarntdesign-bot
andauthored
feat(Cascader): support select parent nodes when filtering (#6102)
* feat(cascader): support select parentnode when filtering * feat(cascader): support select parentnode when filtering * feat(cascader): support select parentnode when filtering * fix(cascader): fix custom option node change * fix(cascader): fix custom option node change * chore: fix lint * chore: stash changelog [ci skip] * chore: optimize * feat(cascader): add more params for option * fix(cascader): fix reservekeyword api * chore: stash changelog [ci skip] --------- Co-authored-by: tdesign-bot <tdesign@tencent.com>
1 parent 0947ba7 commit b092d17

File tree

15 files changed

+124
-83
lines changed

15 files changed

+124
-83
lines changed

packages/components/cascader/_example-ts/custom-options.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,27 @@
3131
:option="optionRender"
3232
>
3333
</t-cascader>
34+
<t-cascader
35+
v-model="value4"
36+
:popup-props="{ overlayClassName: 'tdesign-demo-select__overlay-option' }"
37+
:options="options"
38+
multiple
39+
>
40+
<template #option="{ item, onChange }">
41+
<div class="tdesign-demo__user-option" @click="(e) => handleClick(item, onChange)">
42+
<img src="https://tdesign.gtimg.com/site/avatar.jpg" />
43+
<div class="tdesign-demo__user-option-info">
44+
<div>{{ item.label }}</div>
45+
<div>{{ item.value }}</div>
46+
</div>
47+
</div>
48+
</template>
49+
</t-cascader>
3450
</t-space>
3551
</template>
3652
<script lang="tsx" setup>
3753
import { ref, computed } from 'vue';
38-
import { CascaderProps } from 'tdesign-vue-next';
54+
import type { CascaderProps, TreeOptionData } from 'tdesign-vue-next';
3955
const options: CascaderProps['options'] = [
4056
{
4157
label: '选项一',
@@ -96,6 +112,10 @@ const getDeepOptions = (options: CascaderProps['options']): CascaderProps['optio
96112
}));
97113
};
98114
const optionsData = computed<CascaderProps['options']>(() => getDeepOptions(options));
115+
116+
const handleClick = (item: TreeOptionData, changeCallback: () => void) => {
117+
if (Array.isArray(item.children) && !item.children?.length) changeCallback();
118+
};
99119
</script>
100120
<style>
101121
.tdesign-demo__user-option {

packages/components/cascader/_example-ts/filterable.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<t-cascader v-model="value" :options="options" filterable clearable />
44
<t-cascader v-model="value2" :options="options" filterable clearable multiple :min-collapsed-num="2" />
55
<t-cascader v-model="value3" :filter="filterMethod" :options="options" clearable :min-collapsed-num="2" />
6+
<t-cascader v-model="value4" filterable :options="options" clearable multiple check-strictly />
67
</t-space>
78
</template>
89

@@ -46,6 +47,8 @@ const options: CascaderProps['options'] = [
4647
const value = ref('');
4748
const value2 = ref(['1.1']);
4849
const value3 = ref('');
50+
const value4 = ref([]);
51+
4952
const filterMethod: CascaderProps['filter'] = (search, node) => {
5053
console.log('filter:', search, node.label);
5154
return node.label.indexOf(search) !== -1;

packages/components/cascader/_example/custom-options.vue

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@
3131
:option="optionRender"
3232
>
3333
</t-cascader>
34+
<t-cascader
35+
v-model="value4"
36+
:popup-props="{ overlayClassName: 'tdesign-demo-select__overlay-option' }"
37+
:options="options"
38+
multiple
39+
>
40+
<template #option="{ item, onChange }">
41+
<div class="tdesign-demo__user-option" @click="(e) => handleClick(item, onChange)">
42+
<img src="https://tdesign.gtimg.com/site/avatar.jpg" />
43+
<div class="tdesign-demo__user-option-info">
44+
<div>{{ item.label }}</div>
45+
<div>{{ item.value }}</div>
46+
</div>
47+
</div>
48+
</template>
49+
</t-cascader>
3450
</t-space>
3551
</template>
3652
<script setup lang="jsx">
@@ -74,6 +90,7 @@ const options = [
7490
const value1 = ref('');
7591
const value2 = ref('');
7692
const value3 = ref('');
93+
const value4 = ref([]);
7794
7895
const optionRender = (h, { item }) => (
7996
<div class="tdesign-demo__user-option">
@@ -96,6 +113,10 @@ const getDeepOptions = (options) => {
96113
};
97114
98115
const optionsData = computed(() => getDeepOptions(options));
116+
117+
const handleClick = (item, changeCallback) => {
118+
if (Array.isArray(item.children) && !item.children?.length) changeCallback();
119+
};
99120
</script>
100121
<style>
101122
.tdesign-demo__user-option {

packages/components/cascader/_example/filterable.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<t-cascader v-model="value" :options="options" filterable clearable />
44
<t-cascader v-model="value2" :options="options" filterable clearable multiple :min-collapsed-num="2" />
55
<t-cascader v-model="value3" :filter="filterMethod" :options="options" clearable :min-collapsed-num="2" />
6+
<t-cascader v-model="value4" filterable :options="options" clearable multiple check-strictly />
67
</t-space>
78
</template>
89

@@ -47,6 +48,7 @@ const options = [
4748
const value = ref('');
4849
const value2 = ref(['1.1']);
4950
const value3 = ref('');
51+
const value4 = ref([]);
5052
5153
const filterMethod = (search, node) => {
5254
console.log('filter:', search, node.label);

packages/components/cascader/cascader.en-US.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ loadingText | String / Slot / Function | - | Typescript:`string \| TNode`。[s
2626
max | Number | 0 | \- | N
2727
minCollapsedNum | Number | 0 | \- | N
2828
multiple | Boolean | false | \- | N
29-
option | Slot / Function | - | customize one option。Typescript:`TNode<{ item: CascaderOption; index: number }>`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
29+
option | Slot / Function | - | customize one option。Typescript:`TNode<{ item: CascaderOption; index: number; onChange: ()=> void; onExpand: ()=> void }>`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
3030
options | Array | [] | Typescript:`Array<CascaderOption>` | N
3131
panelBottomContent | String / Slot / Function | - | bottom content of the cascader panel。Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
3232
panelTopContent | String / Slot / Function | - | top content of the cascader panel。Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
@@ -36,7 +36,7 @@ popupVisible | Boolean | - | \- | N
3636
defaultPopupVisible | Boolean | - | uncontrolled property | N
3737
prefixIcon | Slot / Function | - | Typescript:`TNode`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
3838
readonly | Boolean | undefined | \- | N
39-
reserveKeyword | Boolean | false | \- | N
39+
reserveKeyword | Boolean | true | \- | N
4040
selectInputProps | Object | - | Typescript:`SelectInputProps`[SelectInput API Documents](./select-input?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/cascader/type.ts) | N
4141
showAllLevels | Boolean | true | \- | N
4242
size | String | medium | options: large/medium/small。Typescript:`SizeEnum`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N

packages/components/cascader/cascader.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ loadingText | String / Slot / Function | - | 远程加载时显示的文字,
2626
max | Number | 0 | 用于控制多选数量,值为 0 则不限制 | N
2727
minCollapsedNum | Number | 0 | 最小折叠数量,用于多选情况下折叠选中项,超出该数值的选中项折叠。值为 0 则表示不折叠 | N
2828
multiple | Boolean | false | 是否允许多选 | N
29-
option | Slot / Function | - | 自定义单个级联选项。TS 类型:`TNode<{ item: CascaderOption; index: number }>`[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
29+
option | Slot / Function | - | 自定义单个级联选项, item 是选项本身的值,index 是下标,onChange 用于触发当前节点选中,onExpand 用于触发当前节点展开。TS 类型:`TNode<{ item: CascaderOption; index: number; onChange: ()=> void; onExpand: ()=> void }>`[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
3030
options | Array | [] | 可选项数据源。TS 类型:`Array<CascaderOption>` | N
3131
panelBottomContent | String / Slot / Function | - | 面板内的底部内容。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
3232
panelTopContent | String / Slot / Function | - | 面板内的顶部内容。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
@@ -36,7 +36,7 @@ popupVisible | Boolean | - | 是否显示下拉框 | N
3636
defaultPopupVisible | Boolean | - | 是否显示下拉框。非受控属性 | N
3737
prefixIcon | Slot / Function | - | 组件前置图标。TS 类型:`TNode`[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
3838
readonly | Boolean | undefined | 只读状态,值为真会隐藏输入框,且无法打开下拉框 | N
39-
reserveKeyword | Boolean | false | 多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词 | N
39+
reserveKeyword | Boolean | true | 多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词 | N
4040
selectInputProps | Object | - | 透传 SelectInput 筛选器输入框组件的全部属性。TS 类型:`SelectInputProps`[SelectInput API Documents](./select-input?tab=api)[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/cascader/type.ts) | N
4141
showAllLevels | Boolean | true | 选中值使用完整路径,输入框在单选时也显示完整路径 | N
4242
size | String | medium | 组件尺寸。可选项:large/medium/small。TS 类型:`SizeEnum`[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N

packages/components/cascader/components/Item.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export default defineComponent({
9999
disabled={node.isDisabled() || ((value as TreeNodeValue[]).length >= max && max !== 0)}
100100
// node.value maybe string or number
101101
name={String(node.value)}
102-
stopLabelTrigger={!!node.children}
102+
stopLabelTrigger={!!node.children && !props.cascaderContext.isParentFilterable}
103103
title={inputVal ? getFullPathLabel(node) : renderTitle(node)}
104104
onChange={() => {
105105
props.onChange();
@@ -113,19 +113,14 @@ export default defineComponent({
113113

114114
return () => {
115115
const { cascaderContext, node, optionChild } = props;
116-
const isOptionChildAndMultiple = optionChild && cascaderContext.multiple;
117116
return (
118-
<li
119-
ref={liRef}
120-
class={itemClass.value}
121-
onClick={() => (isOptionChildAndMultiple ? props.onChange() : props.onClick())}
122-
onMouseenter={props.onMouseenter}
123-
>
117+
<li ref={liRef} class={itemClass.value} onClick={props.onClick} onMouseenter={props.onMouseenter}>
124118
{optionChild ||
125119
(cascaderContext.multiple
126120
? RenderCheckBox(node, cascaderContext)
127121
: RenderLabelContent(node, cascaderContext))}
128122
{node.children &&
123+
!props.cascaderContext.isParentFilterable &&
129124
(node.loading ? (
130125
<TLoading class={iconClass.value} size="small" />
131126
) : (

packages/components/cascader/components/Panel.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import CascaderProps from '../props';
66
import { useConfig, usePrefixClass, useTNodeDefault } from '@tdesign/shared-hooks';
77

88
import { getDefaultNode } from '@tdesign/shared-utils';
9-
import { getPanels, expendClickEffect, valueChangeEffect } from '../utils';
9+
import { getPanels, expandClickEffect, valueChangeEffect } from '../utils';
1010

1111
export default defineComponent({
1212
name: 'TCascaderSubPanel',
@@ -32,14 +32,19 @@ export default defineComponent({
3232

3333
const handleExpand = (node: TreeNode, trigger: 'hover' | 'click') => {
3434
const { trigger: propsTrigger, cascaderContext } = props;
35-
expendClickEffect(propsTrigger, trigger, node, cascaderContext, props.options);
35+
expandClickEffect(propsTrigger, trigger, node, cascaderContext);
3636
};
3737

3838
const renderItem = (node: TreeNode, index: number) => {
3939
const optionChild = node.data.content
4040
? getDefaultNode(node.data.content(h))
4141
: renderTNodeJSXDefault('option', {
42-
params: { item: node.data, index },
42+
params: {
43+
item: node.data,
44+
index,
45+
onExpand: () => handleExpand(node, 'click'),
46+
onChange: () => valueChangeEffect(node, props.cascaderContext),
47+
},
4348
});
4449
return (
4550
<Item

packages/components/cascader/hooks/index.ts

Lines changed: 17 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
isValueInvalid,
1212
treeNodesEffect,
1313
treeStoreExpendEffect,
14-
calculateExpand,
1514
} from '../utils';
1615

1716
import {
@@ -24,34 +23,6 @@ import {
2423
TreeOptionData,
2524
} from '../types';
2625

27-
/**
28-
* @description 扁平化树形数据,在 filterable 和 checkStrictly 时使用
29-
*/
30-
function flattenOptions(options: TdCascaderProps['options']) {
31-
const result: TdCascaderProps['options'] = [];
32-
33-
function processNodes(nodes: any[], parentLabel = '', isParentDisabled = false) {
34-
nodes.forEach((node) => {
35-
const currentDisabled = isParentDisabled || node.disabled || false;
36-
const currentLabel = parentLabel ? `${parentLabel}/${node.label}` : node.label;
37-
38-
const newNode = {
39-
label: currentLabel,
40-
value: node.value,
41-
disabled: currentDisabled,
42-
};
43-
result.push(newNode);
44-
45-
if (node.children) {
46-
processNodes(node.children, currentLabel, currentDisabled);
47-
}
48-
});
49-
}
50-
51-
processNodes(options);
52-
return result;
53-
}
54-
5526
// 全局状态
5627
export const useContext = (
5728
props: TdCascaderProps,
@@ -67,6 +38,11 @@ export const useContext = (
6738
expend: [],
6839
});
6940

41+
// 部分模式下需要允许父节点被搜索选择 valueMode = 'parentFirst' 和 checkStrictly
42+
const isParentFilterable = computed(
43+
() => (props.valueMode === 'parentFirst' || props.checkStrictly) && statusContext.inputVal,
44+
);
45+
7046
return {
7147
statusContext,
7248
cascaderContext: computed(() => {
@@ -84,6 +60,8 @@ export const useContext = (
8460
minCollapsedNum,
8561
valueType,
8662
modelValue,
63+
valueMode,
64+
reserveKeyword,
8765
} = props;
8866
return {
8967
value: statusContext.scopeVal,
@@ -99,7 +77,10 @@ export const useContext = (
9977
showAllLevels,
10078
minCollapsedNum,
10179
valueType,
80+
valueMode,
81+
reserveKeyword,
10282
visible: innerPopupVisible.value,
83+
isParentFilterable: isParentFilterable.value,
10384
...statusContext,
10485
setTreeNodes: (nodes: TreeNode[]) => {
10586
statusContext.treeNodes = nodes;
@@ -112,7 +93,7 @@ export const useContext = (
11293
setInputVal: (val: string) => {
11394
statusContext.inputVal = val;
11495
},
115-
setExpend: (val: TreeNodeValue[]) => {
96+
setExpand: (val: TreeNodeValue[]) => {
11697
statusContext.expend = val;
11798
},
11899
};
@@ -139,12 +120,12 @@ export const useCascaderContext = (props: TdCascaderProps) => {
139120

140121
// 更新treeNodes
141122
const updatedTreeNodes = () => {
142-
const { inputVal, treeStore, setTreeNodes } = cascaderContext.value;
143-
treeNodesEffect(inputVal, treeStore, setTreeNodes, props.filter);
123+
const { inputVal, treeStore, setTreeNodes, isParentFilterable } = cascaderContext.value;
124+
treeNodesEffect(inputVal, treeStore, setTreeNodes, props.filter, isParentFilterable);
144125
};
145126

146127
// 更新节点展开状态
147-
const updateExpend = () => {
128+
const updateExpand = () => {
148129
const { value, treeStore } = cascaderContext.value;
149130
const { expend } = statusContext;
150131
treeStoreExpendEffect(treeStore, value, expend);
@@ -185,7 +166,7 @@ export const useCascaderContext = (props: TdCascaderProps) => {
185166
treeStore.reload(options);
186167
treeStore.refreshNodes();
187168
}
188-
updateExpend();
169+
updateExpand();
189170
updatedTreeNodes();
190171
},
191172
{ immediate: true, deep: true },
@@ -235,7 +216,7 @@ export const useCascaderContext = (props: TdCascaderProps) => {
235216
}
236217

237218
if (!statusContext.treeStore) return;
238-
updateExpend();
219+
updateExpand();
239220
updatedTreeNodes();
240221
},
241222
{ immediate: true },
@@ -253,19 +234,7 @@ export const useCascaderContext = (props: TdCascaderProps) => {
253234

254235
watch(
255236
() => statusContext.inputVal,
256-
(val) => {
257-
if (props.checkStrictly && props.filterable) {
258-
if (val) {
259-
const flattenedOptions = flattenOptions(props.options);
260-
statusContext.treeStore.reload(flattenedOptions);
261-
statusContext.treeStore.refreshNodes();
262-
} else {
263-
statusContext.treeStore.reload(props.options);
264-
}
265-
const expand = calculateExpand(statusContext.treeStore, cascaderContext.value.value);
266-
statusContext.treeStore.replaceExpanded(expand);
267-
updateExpend();
268-
}
237+
() => {
269238
updatedTreeNodes();
270239
},
271240
);

packages/components/cascader/props.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default {
7878
},
7979
/** 是否允许多选 */
8080
multiple: Boolean,
81-
/** 自定义单个级联选项 */
81+
/** 自定义单个级联选项, item 是选项本身的值,index 是下标,onChange 用于触发当前节点选中,onExpand 用于触发当前节点展开 */
8282
option: {
8383
type: Function as PropType<TdCascaderProps['option']>,
8484
},
@@ -116,7 +116,10 @@ export default {
116116
default: undefined,
117117
},
118118
/** 多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词 */
119-
reserveKeyword: Boolean,
119+
reserveKeyword: {
120+
type: Boolean,
121+
default: true,
122+
},
120123
/** 透传 SelectInput 筛选器输入框组件的全部属性 */
121124
selectInputProps: {
122125
type: Object as PropType<TdCascaderProps['selectInputProps']>,

0 commit comments

Comments
 (0)