refactor: cascader

refactor-cascader
tangjinzhou 2022-01-20 10:56:17 +08:00
parent d46762c1d6
commit e08c6da9b5
32 changed files with 1863 additions and 1609 deletions

View File

@ -1,7 +1,7 @@
import type { FunctionalComponent } from 'vue'; import type { FunctionalComponent } from 'vue';
import type { OptionCoreData } from '../vc-select/interface'; import type { DefaultOptionType } from '../vc-select/Select';
export interface OptionProps extends Omit<OptionCoreData, 'label'> { export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
/** Save for customize data */ /** Save for customize data */
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any [prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
} }

View File

@ -20,12 +20,8 @@ Cascade selection box for selecting province/city/district.
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string; const options: CascaderProps['options'] = [
label: string;
children?: Option[];
}
const options: Option[] = [
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',

View File

@ -16,16 +16,17 @@ Allow only select parent options.
</docs> </docs>
<template> <template>
<a-cascader v-model:value="value" :options="options" change-on-select /> <a-cascader
v-model:value="value"
:options="options"
placeholder="Please select"
change-on-select
/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string; const options: CascaderProps['options'] = [
label: string;
children?: Option[];
}
const options: Option[] = [
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',

View File

@ -16,7 +16,12 @@ For instance, add an external link after the selected value.
</docs> </docs>
<template> <template>
<a-cascader v-model:value="value" :options="options" style="width: 100%"> <a-cascader
v-model:value="value"
placeholder="Please select"
:options="options"
style="width: 100%"
>
<template #displayRender="{ labels, selectedOptions }"> <template #displayRender="{ labels, selectedOptions }">
<span v-for="(label, index) in labels" :key="selectedOptions[index].value"> <span v-for="(label, index) in labels" :key="selectedOptions[index].value">
<span v-if="index === labels.length - 1"> <span v-if="index === labels.length - 1">
@ -33,14 +38,8 @@ For instance, add an external link after the selected value.
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string; const options: CascaderProps['options'] = [
label: string;
children?: Option[];
code?: number;
[key: string]: any;
}
const options: Option[] = [
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',

View File

@ -18,21 +18,20 @@ Separate trigger button and result.
<template> <template>
<span> <span>
{{ text }} &nbsp; {{ text }} &nbsp;
<a-cascader v-model:value="value" :options="options" @change="onChange"> <a-cascader
v-model:value="value"
placeholder="Please select"
:options="options"
@change="onChange"
>
<a href="#">Change city</a> <a href="#">Change city</a>
</a-cascader> </a-cascader>
</span> </span>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string; const options: CascaderProps['options'] = [
label: string;
children?: Option[];
code?: number;
[key: string]: any;
}
const options: Option[] = [
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',

View File

@ -16,19 +16,12 @@ Disable option by specifying the `disabled` property in `options`.
</docs> </docs>
<template> <template>
<a-cascader v-model:value="value" :options="options" /> <a-cascader v-model:value="value" placeholder="Please select" :options="options" />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string; const options: CascaderProps['options'] = [
label: string;
disabled?: boolean;
children?: Option[];
code?: number;
[key: string]: any;
}
const options: Option[] = [
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',

View File

@ -25,14 +25,8 @@ Custom Field Names
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
code: string; const options: CascaderProps['options'] = [
name: string;
disabled?: boolean;
items?: Option[];
[key: string]: any;
}
const options: Option[] = [
{ {
code: 'zhejiang', code: 'zhejiang',
name: 'Zhejiang', name: 'Zhejiang',

View File

@ -19,19 +19,14 @@ Hover to expand sub menu, click to select option.
<a-cascader <a-cascader
v-model:value="value" v-model:value="value"
:options="options" :options="options"
:display-render="displayRender"
expand-trigger="hover" expand-trigger="hover"
placeholder="Please select" placeholder="Please select"
/> />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string; const options: CascaderProps['options'] = [
label: string;
children?: Option[];
}
const options: Option[] = [
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',
@ -67,14 +62,9 @@ const options: Option[] = [
]; ];
export default defineComponent({ export default defineComponent({
setup() { setup() {
const displayRender = ({ labels }: { labels: string[] }) => {
return labels[labels.length - 1];
};
return { return {
value: ref<string[]>([]), value: ref<string[]>([]),
options, options,
displayRender,
}; };
}, },
}); });

View File

@ -28,16 +28,11 @@ Load options lazily with `loadData`.
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string;
label: string;
loading?: boolean;
isLeaf?: boolean;
children?: Option[];
}
export default defineComponent({ export default defineComponent({
setup() { setup() {
const options = ref<Option[]>([ const options = ref<CascaderProps['options']>([
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',

View File

@ -27,13 +27,8 @@ Search and select options directly.
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string; const options: CascaderProps['options'] = [
label: string;
disabled?: boolean;
children?: Option[];
}
const options: Option[] = [
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',

View File

@ -16,24 +16,20 @@ Cascade selection box of different sizes.
</docs> </docs>
<template> <template>
<a-cascader v-model:value="value" size="large" :options="options" /> <a-cascader v-model:value="value" placeholder="Please select" size="large" :options="options" />
<br /> <br />
<br /> <br />
<a-cascader v-model:value="value" :options="options" /> <a-cascader v-model:value="value" placeholder="Please select" :options="options" />
<br /> <br />
<br /> <br />
<a-cascader v-model:value="value" size="small" :options="options" /> <a-cascader v-model:value="value" placeholder="Please select" size="small" :options="options" />
<br /> <br />
<br /> <br />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string; const options: CascaderProps['options'] = [
label: string;
children?: Option[];
}
const options: Option[] = [
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',

View File

@ -35,12 +35,8 @@ Custom suffix icon
<script lang="ts"> <script lang="ts">
import { SmileOutlined } from '@ant-design/icons-vue'; import { SmileOutlined } from '@ant-design/icons-vue';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
interface Option { import type { CascaderProps } from 'ant-design-vue';
value: string; const options: CascaderProps['options'] = [
label: string;
children?: Option[];
}
const options: Option[] = [
{ {
value: 'zhejiang', value: 'zhejiang',
label: 'Zhejiang', label: 'Zhejiang',

View File

@ -1,646 +1,286 @@
import type { PropType, CSSProperties, ExtractPropTypes } from 'vue'; import type {
import { inject, provide, defineComponent } from 'vue'; ShowSearchType,
import PropTypes from '../_util/vue-types'; FieldNames,
import VcCascader from '../vc-cascader'; BaseOptionType,
import arrayTreeFilter from 'array-tree-filter'; DefaultOptionType,
import classNames from '../_util/classNames'; } from '../vc-cascader2';
import KeyCode from '../_util/KeyCode'; import VcCascader, { cascaderProps as vcCascaderProps } from '../vc-cascader2';
import Input from '../input';
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import RightOutlined from '@ant-design/icons-vue/RightOutlined'; import RightOutlined from '@ant-design/icons-vue/RightOutlined';
import RedoOutlined from '@ant-design/icons-vue/RedoOutlined'; import RedoOutlined from '@ant-design/icons-vue/RedoOutlined';
import { import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
hasProp, import getIcons from '../select/utils/iconUtil';
getOptionProps,
isValidElement,
getComponent,
splitAttrs,
findDOMNode,
getSlot,
} from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import { cloneElement } from '../_util/vnode';
import warning from '../_util/warning';
import { defaultConfigProvider } from '../config-provider';
import type { VueNode } from '../_util/type'; import type { VueNode } from '../_util/type';
import { tuple, withInstall } from '../_util/type'; import { withInstall } from '../_util/type';
import type { RenderEmptyHandler } from '../config-provider/renderEmpty';
import { useInjectFormItemContext } from '../form/FormItemContext';
import omit from '../_util/omit'; import omit from '../_util/omit';
import { computed, defineComponent, ref, watchEffect } from 'vue';
import type { ExtractPropTypes, PropType } from 'vue';
import PropTypes from '../_util/vue-types';
import { initDefaultProps } from '../_util/props-util';
import useConfigInject from '../_util/hooks/useConfigInject';
import classNames from '../_util/classNames';
import type { SizeType } from '../config-provider';
import devWarning from '../vc-util/devWarning';
import { getTransitionName } from '../_util/transition'; import { getTransitionName } from '../_util/transition';
import { useInjectFormItemContext } from '../form';
import type { ValueType } from '../vc-cascader2/Cascader';
export interface CascaderOptionType { // Align the design since we use `rc-select` in root. This help:
value?: string | number; // - List search content will show all content
label?: VueNode; // - Hover opacity style
disabled?: boolean; // - Search filter match case
export { BaseOptionType, DefaultOptionType };
export type FieldNamesType = FieldNames;
export type FilledFieldNamesType = Required<FieldNamesType>;
function highlightKeyword(str: string, lowerKeyword: string, prefixCls: string | undefined) {
const cells = str
.toLowerCase()
.split(lowerKeyword)
.reduce((list, cur, index) => (index === 0 ? [cur] : [...list, lowerKeyword, cur]), []);
const fillCells: VueNode[] = [];
let start = 0;
cells.forEach((cell, index) => {
const end = start + cell.length;
let originWorld: VueNode = str.slice(start, end);
start = end;
if (index % 2 === 1) {
originWorld = (
<span class={`${prefixCls}-menu-item-keyword`} key="seperator">
{originWorld}
</span>
);
}
fillCells.push(originWorld);
});
return fillCells;
}
const defaultSearchRender: ShowSearchType['render'] = ({
inputValue,
path,
prefixCls,
fieldNames,
}) => {
const optionList: VueNode[] = [];
// We do lower here to save perf
const lower = inputValue.toLowerCase();
path.forEach((node, index) => {
if (index !== 0) {
optionList.push(' / ');
}
let label = (node as any)[fieldNames.label!];
const type = typeof label;
if (type === 'string' || type === 'number') {
label = highlightKeyword(String(label), lower, prefixCls);
}
optionList.push(label);
});
return optionList;
};
export interface CascaderOptionType extends DefaultOptionType {
isLeaf?: boolean; isLeaf?: boolean;
loading?: boolean; loading?: boolean;
children?: CascaderOptionType[]; children?: CascaderOptionType[];
[key: string]: any; [key: string]: any;
} }
export function cascaderProps<DataNodeType extends CascaderOptionType = CascaderOptionType>() {
return {
...omit(vcCascaderProps(), ['customSlots', 'checkable', 'options']),
multiple: { type: Boolean, default: undefined },
size: String as PropType<SizeType>,
bordered: { type: Boolean, default: undefined },
export interface FieldNamesType { suffixIcon: PropTypes.any,
value?: string; options: Array as PropType<DataNodeType[]>,
label?: string; 'onUpdate:value': Function as PropType<(value: ValueType) => void>,
children?: string;
}
export interface FilledFieldNamesType {
value: string;
label: string;
children: string;
}
// const CascaderOptionType = PropTypes.shape({
// value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// label: PropTypes.any,
// disabled: PropTypes.looseBool,
// children: PropTypes.array,
// key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// }).loose;
// const FieldNamesType = PropTypes.shape({
// value: PropTypes.string.isRequired,
// label: PropTypes.string.isRequired,
// children: PropTypes.string,
// }).loose;
export interface ShowSearchType {
filter?: (inputValue: string, path: CascaderOptionType[], names: FilledFieldNamesType) => boolean;
render?: (
inputValue: string,
path: CascaderOptionType[],
prefixCls: string | undefined,
names: FilledFieldNamesType,
) => VueNode;
sort?: (
a: CascaderOptionType[],
b: CascaderOptionType[],
inputValue: string,
names: FilledFieldNamesType,
) => number;
matchInputWidth?: boolean;
limit?: number | false;
}
export interface EmptyFilteredOptionsType {
disabled: boolean;
[key: string]: any;
}
export interface FilteredOptionsType extends EmptyFilteredOptionsType {
__IS_FILTERED_OPTION: boolean;
path: CascaderOptionType[];
}
// const ShowSearchType = PropTypes.shape({
// filter: PropTypes.func,
// render: PropTypes.func,
// sort: PropTypes.func,
// matchInputWidth: PropTypes.looseBool,
// limit: withUndefined(PropTypes.oneOfType([Boolean, Number])),
// }).loose;
function noop() {}
const cascaderProps = {
/** 可选项数据源 */
options: { type: Array as PropType<CascaderOptionType[]>, default: [] },
/** 默认的选中项 */
defaultValue: PropTypes.array,
/** 指定选中项 */
value: PropTypes.array,
/** 选择完成后的回调 */
// onChange?: (value: string[], selectedOptions?: CascaderOptionType[]) => void;
/** 选择后展示的渲染函数 */
displayRender: PropTypes.func,
transitionName: PropTypes.string,
popupStyle: PropTypes.object.def(() => ({})),
/** 自定义浮层类名 */
popupClassName: PropTypes.string,
/** 浮层预设位置:`bottomLeft` `bottomRight` `topLeft` `topRight` */
popupPlacement: PropTypes.oneOf(tuple('bottomLeft', 'bottomRight', 'topLeft', 'topRight')).def(
'bottomLeft',
),
/** 输入框占位文本*/
placeholder: PropTypes.string.def('Please select'),
/** 输入框大小,可选 `large` `default` `small` */
size: PropTypes.oneOf(tuple('large', 'default', 'small')),
/** 禁用*/
disabled: PropTypes.looseBool.def(false),
/** 是否支持清除*/
allowClear: PropTypes.looseBool.def(true),
showSearch: {
type: [Boolean, Object] as PropType<boolean | ShowSearchType | undefined>,
default: undefined as PropType<boolean | ShowSearchType | undefined>,
},
notFoundContent: PropTypes.any,
loadData: PropTypes.func,
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
expandTrigger: PropTypes.oneOf(tuple('click', 'hover')),
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
changeOnSelect: PropTypes.looseBool,
/** 浮层可见变化时回调 */
// onPopupVisibleChange?: (popupVisible: boolean) => void;
prefixCls: PropTypes.string,
inputPrefixCls: PropTypes.string,
getPopupContainer: PropTypes.func,
popupVisible: PropTypes.looseBool,
fieldNames: { type: Object as PropType<FieldNamesType> },
autofocus: PropTypes.looseBool,
suffixIcon: PropTypes.any,
showSearchRender: PropTypes.any,
onChange: PropTypes.func,
onPopupVisibleChange: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onSearch: PropTypes.func,
'onUpdate:value': PropTypes.func,
};
export type CascaderProps = Partial<ExtractPropTypes<typeof cascaderProps>>;
// We limit the filtered item count by default
const defaultLimit = 50;
function defaultFilterOption(
inputValue: string,
path: CascaderOptionType[],
names: FilledFieldNamesType,
) {
return path.some(option => option[names.label].indexOf(inputValue) > -1);
}
function defaultSortFilteredOption(
a: CascaderOptionType[],
b: CascaderOptionType[],
inputValue: string,
names: FilledFieldNamesType,
) {
function callback(elem: CascaderOptionType) {
return elem[names.label].indexOf(inputValue) > -1;
}
return a.findIndex(callback) - b.findIndex(callback);
}
function getFilledFieldNames(props: any) {
const fieldNames = (props.fieldNames || {}) as FieldNamesType;
const names: FilledFieldNamesType = {
children: fieldNames.children || 'children',
label: fieldNames.label || 'label',
value: fieldNames.value || 'value',
}; };
return names;
} }
function flattenTree( export type CascaderProps = Partial<ExtractPropTypes<ReturnType<typeof cascaderProps>>>;
options: CascaderOptionType[],
props: any,
ancestor: CascaderOptionType[] = [],
) {
const names: FilledFieldNamesType = getFilledFieldNames(props);
let flattenOptions = [];
const childrenName = names.children;
options.forEach(option => {
const path = ancestor.concat(option);
if (props.changeOnSelect || !option[childrenName] || !option[childrenName].length) {
flattenOptions.push(path);
}
if (option[childrenName]) {
flattenOptions = flattenOptions.concat(flattenTree(option[childrenName], props, path));
}
});
return flattenOptions;
}
const defaultDisplayRender = ({ labels }) => labels.join(' / '); export interface CascaderRef {
focus: () => void;
blur: () => void;
}
const Cascader = defineComponent({ const Cascader = defineComponent({
name: 'ACascader', name: 'ACascader',
mixins: [BaseMixin],
inheritAttrs: false, inheritAttrs: false,
props: cascaderProps, props: initDefaultProps(cascaderProps(), {
setup() { bordered: true,
choiceTransitionName: '',
allowClear: true,
}),
setup(props, { attrs, expose, slots, emit }) {
const formItemContext = useInjectFormItemContext(); const formItemContext = useInjectFormItemContext();
return {
configProvider: inject('configProvider', defaultConfigProvider),
localeData: inject('localeData', {} as any),
cachedOptions: [],
popupRef: undefined,
input: undefined,
formItemContext,
};
},
data() {
const { value, defaultValue, popupVisible, showSearch, options } = this.$props;
return {
sValue: (value || defaultValue || []) as any[],
inputValue: '',
inputFocused: false,
sPopupVisible: popupVisible as boolean,
flattenOptions: showSearch
? flattenTree(options as CascaderOptionType[], this.$props)
: undefined,
};
},
watch: {
value(val) {
this.setState({ sValue: val || [] });
},
popupVisible(val) {
this.setState({ sPopupVisible: val });
},
options(val) {
if (this.showSearch) {
this.setState({ flattenOptions: flattenTree(val, this.$props as any) });
}
},
},
// model: {
// prop: 'value',
// event: 'change',
// },
created() {
provide('savePopupRef', this.savePopupRef);
},
methods: {
savePopupRef(ref: any) {
this.popupRef = ref;
},
highlightKeyword(str: string, keyword: string, prefixCls: string | undefined) {
return str
.split(keyword)
.map((node, index) =>
index === 0
? node
: [<span class={`${prefixCls}-menu-item-keyword`}>{keyword}</span>, node],
);
},
defaultRenderFilteredOption(opt: {
inputValue: string;
path: CascaderOptionType[];
prefixCls: string | undefined;
names: FilledFieldNamesType;
}) {
const { inputValue, path, prefixCls, names } = opt;
return path.map((option, index) => {
const label = option[names.label];
const node =
label.indexOf(inputValue) > -1
? this.highlightKeyword(label, inputValue, prefixCls)
: label;
return index === 0 ? node : [' / ', node];
});
},
saveInput(node: any) {
this.input = node;
},
handleChange(value: any, selectedOptions: CascaderOptionType[]) {
this.setState({ inputValue: '' });
if (selectedOptions[0].__IS_FILTERED_OPTION) {
const unwrappedValue = value[0];
const unwrappedSelectedOptions = selectedOptions[0].path;
this.setValue(unwrappedValue, unwrappedSelectedOptions);
return;
}
this.setValue(value, selectedOptions);
},
handlePopupVisibleChange(popupVisible: boolean) {
if (!hasProp(this, 'popupVisible')) {
this.setState((state: any) => ({
sPopupVisible: popupVisible,
inputFocused: popupVisible,
inputValue: popupVisible ? state.inputValue : '',
}));
}
this.$emit('popupVisibleChange', popupVisible);
},
handleInputFocus(e: InputEvent) {
this.$emit('focus', e);
},
handleInputBlur(e: InputEvent) {
this.setState({
inputFocused: false,
});
this.$emit('blur', e);
this.formItemContext.onFieldBlur();
},
handleInputClick(e: MouseEvent & { nativeEvent?: any }) {
const { inputFocused, sPopupVisible } = this;
// Prevent `Trigger` behavior.
if (inputFocused || sPopupVisible) {
e.stopPropagation();
if (e.nativeEvent && e.nativeEvent.stopImmediatePropagation) {
e.nativeEvent.stopImmediatePropagation();
}
}
},
handleKeyDown(e: KeyboardEvent) {
if (e.keyCode === KeyCode.BACKSPACE || e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
}
},
handleInputChange(e: Event) {
const inputValue = (e.target as HTMLInputElement).value;
this.setState({ inputValue });
this.$emit('search', inputValue);
},
setValue(value: string[] | number[], selectedOptions: CascaderOptionType[] = []) {
if (!hasProp(this, 'value')) {
this.setState({ sValue: value });
}
this.$emit('update:value', value);
this.$emit('change', value, selectedOptions);
this.formItemContext.onFieldChange();
},
getLabel() {
const { options } = this;
const names = getFilledFieldNames(this.$props);
const displayRender = getComponent(this, 'displayRender', {}, false) || defaultDisplayRender;
const value = this.sValue;
const unwrappedValue = Array.isArray(value[0]) ? value[0] : value;
const selectedOptions = arrayTreeFilter<CascaderOptionType>(
options as CascaderOptionType[],
(o, level) => o[names.value] === unwrappedValue[level],
{ childrenKeyName: names.children },
);
const labels = selectedOptions.map(o => o[names.label]);
return displayRender({ labels, selectedOptions });
},
clearSelection(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
if (!this.inputValue) {
this.setValue([]);
this.handlePopupVisibleChange(false);
} else {
this.setState({ inputValue: '' });
}
},
generateFilteredOptions(
prefixCls: string | undefined,
renderEmpty: RenderEmptyHandler,
): EmptyFilteredOptionsType[] | FilteredOptionsType[] {
const { showSearch, notFoundContent } = this;
const names: FilledFieldNamesType = getFilledFieldNames(this.$props);
const {
filter = defaultFilterOption,
// render = this.defaultRenderFilteredOption,
sort = defaultSortFilteredOption,
limit = defaultLimit,
} = showSearch as ShowSearchType;
const render =
(showSearch as ShowSearchType).render ||
getComponent(this, 'showSearchRender') ||
this.defaultRenderFilteredOption;
const { flattenOptions = [], inputValue } = this.$data;
// Limit the filter if needed
let filtered: Array<CascaderOptionType[]>;
if (limit > 0) {
filtered = [];
let matchCount = 0;
// Perf optimization to filter items only below the limit
flattenOptions.some(path => {
const match = filter(inputValue, path, names);
if (match) {
filtered.push(path);
matchCount += 1;
}
return matchCount >= limit;
});
} else {
warning(
typeof limit !== 'number',
'Cascader',
"'limit' of showSearch in Cascader should be positive number or false.",
);
filtered = flattenOptions.filter(path => filter(inputValue, path, names));
}
filtered.sort((a, b) => sort(a, b, inputValue, names));
if (filtered.length > 0) {
return filtered.map(path => {
return {
__IS_FILTERED_OPTION: true,
path,
[names.label]: render({ inputValue, path, prefixCls, names }),
[names.value]: path.map(o => o[names.value]),
disabled: path.some(o => !!o.disabled),
};
});
}
return [
{
[names.label]: notFoundContent || renderEmpty('Cascader'),
[names.value]: 'ANT_CASCADER_NOT_FOUND',
disabled: true,
},
];
},
focus() {
this.input && this.input.focus();
},
blur() {
this.input && this.input.blur();
},
},
render() {
const { sPopupVisible, inputValue, configProvider, localeData } = this;
const { sValue: value, inputFocused } = this.$data;
const props = getOptionProps(this);
let suffixIcon = getComponent(this, 'suffixIcon');
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon;
const { getPopupContainer: getContextPopupContainer } = configProvider;
const { const {
prefixCls: customizePrefixCls, prefixCls: cascaderPrefixCls,
inputPrefixCls: customizeInputPrefixCls, rootPrefixCls,
placeholder = localeData.placeholder, getPrefixCls,
size, direction,
disabled,
allowClear,
showSearch = false,
notFoundContent,
...otherProps
} = props as any;
const { onEvents, extraAttrs } = splitAttrs(this.$attrs);
const {
class: className,
style,
id = this.formItemContext.id.value,
...restAttrs
} = extraAttrs;
const getPrefixCls = this.configProvider.getPrefixCls;
const renderEmpty = this.configProvider.renderEmpty;
const rootPrefixCls = getPrefixCls();
const prefixCls = getPrefixCls('cascader', customizePrefixCls);
const inputPrefixCls = getPrefixCls('input', customizeInputPrefixCls);
const sizeCls = classNames({
[`${inputPrefixCls}-lg`]: size === 'large',
[`${inputPrefixCls}-sm`]: size === 'small',
});
const clearIcon =
(allowClear && !disabled && value.length > 0) || inputValue ? (
<CloseCircleFilled
class={`${prefixCls}-picker-clear`}
onClick={this.clearSelection}
key="clear-icon"
/>
) : null;
const arrowCls = classNames({
[`${prefixCls}-picker-arrow`]: true,
[`${prefixCls}-picker-arrow-expand`]: sPopupVisible,
});
const pickerCls = classNames(className, `${prefixCls}-picker`, {
[`${prefixCls}-picker-with-value`]: inputValue,
[`${prefixCls}-picker-disabled`]: disabled,
[`${prefixCls}-picker-${size}`]: !!size,
[`${prefixCls}-picker-show-search`]: !!showSearch,
[`${prefixCls}-picker-focused`]: inputFocused,
});
// Fix bug of https://github.com/facebook/react/pull/5004
// and https://fb.me/react-unknown-prop
const tempInputProps = omit(otherProps, [
'popupStyle',
'options',
'popupPlacement',
'transitionName',
'displayRender',
'changeOnSelect',
'expandTrigger',
'popupVisible',
'getPopupContainer',
'loadData',
'popupClassName',
'filterOption',
'renderFilteredOption',
'sortFilteredOption',
'notFoundContent',
'defaultValue',
'fieldNames',
'onChange',
'onPopupVisibleChange',
'onFocus',
'onBlur',
'onSearch',
'onUpdate:value',
]);
let options = props.options;
const names = getFilledFieldNames(this.$props);
if (options && options.length > 0) {
if (inputValue) {
options = this.generateFilteredOptions(prefixCls, renderEmpty);
}
} else {
options = [
{
[names.label]: notFoundContent || renderEmpty('Cascader'),
[names.value]: 'ANT_CASCADER_NOT_FOUND',
disabled: true,
},
];
}
// Dropdown menu should keep previous status until it is fully closed.
if (!sPopupVisible) {
options = this.cachedOptions;
} else {
this.cachedOptions = options;
}
const dropdownMenuColumnStyle: CSSProperties = {};
const isNotFound =
(options || []).length === 1 && options[0].value === 'ANT_CASCADER_NOT_FOUND';
if (isNotFound) {
dropdownMenuColumnStyle.height = 'auto'; // Height of one row.
}
// The default value of `matchInputWidth` is `true`
const resultListMatchInputWidth = showSearch.matchInputWidth !== false;
if (resultListMatchInputWidth && (inputValue || isNotFound) && this.input) {
dropdownMenuColumnStyle.width = findDOMNode(this.input.input).offsetWidth + 'px';
}
// showSearchfocusblurinputref='picker'
const inputProps = {
...restAttrs,
...tempInputProps,
id,
prefixCls: inputPrefixCls,
placeholder: value && value.length > 0 ? undefined : placeholder,
value: inputValue,
disabled,
readonly: !showSearch,
autocomplete: 'off',
class: `${prefixCls}-input ${sizeCls}`,
onFocus: this.handleInputFocus,
onClick: showSearch ? this.handleInputClick : noop,
onBlur: showSearch ? this.handleInputBlur : props.onBlur,
onKeydown: this.handleKeyDown,
onChange: showSearch ? this.handleInputChange : noop,
};
const children = getSlot(this);
const inputIcon = (suffixIcon &&
(isValidElement(suffixIcon) ? (
cloneElement(suffixIcon, {
class: `${prefixCls}-picker-arrow`,
})
) : (
<span class={`${prefixCls}-picker-arrow`}>{suffixIcon}</span>
))) || <DownOutlined class={arrowCls} />;
const input = children.length ? (
children
) : (
<span class={pickerCls} style={style}>
<span class={`${prefixCls}-picker-label`}>{this.getLabel()}</span>
<Input {...inputProps} ref={this.saveInput} />
{clearIcon}
{inputIcon}
</span>
);
const expandIcon = <RightOutlined />;
const loadingIcon = (
<span class={`${prefixCls}-menu-item-loading-icon`}>
<RedoOutlined spin />
</span>
);
const getPopupContainer = props.getPopupContainer || getContextPopupContainer;
const cascaderProps = {
...props,
getPopupContainer, getPopupContainer,
options, renderEmpty,
prefixCls, size,
value, } = useConfigInject('cascader', props);
popupVisible: sPopupVisible, const prefixCls = computed(() => getPrefixCls('select', props.prefixCls));
dropdownMenuColumnStyle, const isRtl = computed(() => direction.value === 'rtl');
expandIcon, // =================== Warning =====================
loadingIcon, if (process.env.NODE_ENV !== 'production') {
...onEvents, watchEffect(() => {
onPopupVisibleChange: this.handlePopupVisibleChange, devWarning(
onChange: this.handleChange, props.popupClassName === undefined,
transitionName: getTransitionName(rootPrefixCls, 'slide-up', props.transitionName), 'Cascader',
'`popupClassName` is deprecated. Please use `dropdownClassName` instead.',
);
devWarning(
!props.multiple || !props.displayRender || !slots.displayRender,
'Cascader',
'`displayRender` not work on `multiple`. Please use `tagRender` instead.',
);
});
}
// ==================== Search =====================
const mergedShowSearch = computed(() => {
if (!props.showSearch) {
return props.showSearch;
}
let searchConfig: ShowSearchType = {
render: defaultSearchRender,
};
if (typeof props.showSearch === 'object') {
searchConfig = {
...searchConfig,
...props.showSearch,
};
}
return searchConfig;
});
// =================== Dropdown ====================
const mergedDropdownClassName = computed(() =>
classNames(
props.dropdownClassName || props.popupClassName,
`${cascaderPrefixCls.value}-dropdown`,
{
[`${cascaderPrefixCls.value}-dropdown-rtl`]: isRtl.value,
},
),
);
const selectRef = ref<CascaderRef>();
expose({
focus() {
selectRef.value?.focus();
},
blur() {
selectRef.value?.blur();
},
} as CascaderRef);
const handleChange: CascaderProps['onChange'] = (...args) => {
emit('update:value', args[0]);
emit('change', ...args);
formItemContext.onFieldChange();
};
const handleBlur: CascaderProps['onBlur'] = (...args) => {
emit('blur', ...args);
formItemContext.onFieldBlur();
};
return () => {
const {
notFoundContent = slots.notFoundContent?.(),
expandIcon = slots.expandIcon?.(),
multiple,
bordered,
allowClear,
choiceTransitionName,
transitionName,
id = formItemContext.id.value,
...restProps
} = props;
// =================== No Found ====================
const mergedNotFoundContent = notFoundContent || renderEmpty.value('Cascader');
// ===================== Icon ======================
let mergedExpandIcon = expandIcon;
if (!expandIcon) {
mergedExpandIcon = isRtl.value ? <LeftOutlined /> : <RightOutlined />;
}
const loadingIcon = (
<span class={`${prefixCls.value}-menu-item-loading-icon`}>
<RedoOutlined spin />
</span>
);
// ===================== Icons =====================
const { suffixIcon, removeIcon, clearIcon } = getIcons(
{
...props,
multiple,
prefixCls: prefixCls.value,
},
slots,
);
return (
<VcCascader
{...restProps}
{...attrs}
id={id}
prefixCls={prefixCls.value}
class={[
cascaderPrefixCls.value,
{
[`${prefixCls.value}-lg`]: size.value === 'large',
[`${prefixCls.value}-sm`]: size.value === 'small',
[`${prefixCls.value}-rtl`]: isRtl.value,
[`${prefixCls.value}-borderless`]: !bordered,
},
attrs.class,
]}
direction={direction.value}
notFoundContent={mergedNotFoundContent}
allowClear={allowClear}
showSearch={mergedShowSearch.value}
expandIcon={mergedExpandIcon}
inputIcon={suffixIcon}
removeIcon={removeIcon}
clearIcon={clearIcon}
loadingIcon={loadingIcon}
checkable={!!multiple}
dropdownClassName={mergedDropdownClassName.value}
dropdownPrefixCls={cascaderPrefixCls.value}
choiceTransitionName={getTransitionName(rootPrefixCls.value, '', choiceTransitionName)}
transitionName={getTransitionName(rootPrefixCls.value, 'slide-up', transitionName)}
getPopupContainer={getPopupContainer.value}
customSlots={{
...slots,
checkable: () => <span class={`${cascaderPrefixCls.value}-checkbox-inner`} />,
}}
displayRender={props.displayRender || slots.displayRender}
onChange={handleChange}
onBlur={handleBlur}
v-slots={slots}
ref={selectRef}
/>
);
}; };
return <VcCascader {...cascaderProps}>{input}</VcCascader>;
}, },
}); });

View File

@ -0,0 +1,647 @@
import type { PropType, CSSProperties, ExtractPropTypes } from 'vue';
import { inject, provide, defineComponent } from 'vue';
import PropTypes from '../_util/vue-types';
import VcCascader from '../vc-cascader';
import arrayTreeFilter from 'array-tree-filter';
import classNames from '../_util/classNames';
import KeyCode from '../_util/KeyCode';
import Input from '../input';
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import RightOutlined from '@ant-design/icons-vue/RightOutlined';
import RedoOutlined from '@ant-design/icons-vue/RedoOutlined';
import {
hasProp,
getOptionProps,
isValidElement,
getComponent,
splitAttrs,
findDOMNode,
getSlot,
} from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import { cloneElement } from '../_util/vnode';
import warning from '../_util/warning';
import { defaultConfigProvider } from '../config-provider';
import type { VueNode } from '../_util/type';
import { tuple, withInstall } from '../_util/type';
import type { RenderEmptyHandler } from '../config-provider/renderEmpty';
import { useInjectFormItemContext } from '../form/FormItemContext';
import omit from '../_util/omit';
import { getTransitionName } from '../_util/transition';
export interface CascaderOptionType {
value?: string | number;
label?: VueNode;
disabled?: boolean;
isLeaf?: boolean;
loading?: boolean;
children?: CascaderOptionType[];
[key: string]: any;
}
export interface FieldNamesType {
value?: string;
label?: string;
children?: string;
}
export interface FilledFieldNamesType {
value: string;
label: string;
children: string;
}
// const CascaderOptionType = PropTypes.shape({
// value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// label: PropTypes.any,
// disabled: PropTypes.looseBool,
// children: PropTypes.array,
// key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// }).loose;
// const FieldNamesType = PropTypes.shape({
// value: PropTypes.string.isRequired,
// label: PropTypes.string.isRequired,
// children: PropTypes.string,
// }).loose;
export interface ShowSearchType {
filter?: (inputValue: string, path: CascaderOptionType[], names: FilledFieldNamesType) => boolean;
render?: (
inputValue: string,
path: CascaderOptionType[],
prefixCls: string | undefined,
names: FilledFieldNamesType,
) => VueNode;
sort?: (
a: CascaderOptionType[],
b: CascaderOptionType[],
inputValue: string,
names: FilledFieldNamesType,
) => number;
matchInputWidth?: boolean;
limit?: number | false;
}
export interface EmptyFilteredOptionsType {
disabled: boolean;
[key: string]: any;
}
export interface FilteredOptionsType extends EmptyFilteredOptionsType {
__IS_FILTERED_OPTION: boolean;
path: CascaderOptionType[];
}
// const ShowSearchType = PropTypes.shape({
// filter: PropTypes.func,
// render: PropTypes.func,
// sort: PropTypes.func,
// matchInputWidth: PropTypes.looseBool,
// limit: withUndefined(PropTypes.oneOfType([Boolean, Number])),
// }).loose;
function noop() {}
const cascaderProps = {
/** 可选项数据源 */
options: { type: Array as PropType<CascaderOptionType[]>, default: [] },
/** 默认的选中项 */
defaultValue: PropTypes.array,
/** 指定选中项 */
value: PropTypes.array,
/** 选择完成后的回调 */
// onChange?: (value: string[], selectedOptions?: CascaderOptionType[]) => void;
/** 选择后展示的渲染函数 */
displayRender: PropTypes.func,
transitionName: PropTypes.string,
popupStyle: PropTypes.object.def(() => ({})),
/** 自定义浮层类名 */
popupClassName: PropTypes.string,
/** 浮层预设位置:`bottomLeft` `bottomRight` `topLeft` `topRight` */
popupPlacement: PropTypes.oneOf(tuple('bottomLeft', 'bottomRight', 'topLeft', 'topRight')).def(
'bottomLeft',
),
/** 输入框占位文本*/
placeholder: PropTypes.string.def('Please select'),
/** 输入框大小,可选 `large` `default` `small` */
size: PropTypes.oneOf(tuple('large', 'default', 'small')),
/** 禁用*/
disabled: PropTypes.looseBool.def(false),
/** 是否支持清除*/
allowClear: PropTypes.looseBool.def(true),
showSearch: {
type: [Boolean, Object] as PropType<boolean | ShowSearchType | undefined>,
default: undefined as PropType<boolean | ShowSearchType | undefined>,
},
notFoundContent: PropTypes.any,
loadData: PropTypes.func,
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
expandTrigger: PropTypes.oneOf(tuple('click', 'hover')),
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
changeOnSelect: PropTypes.looseBool,
/** 浮层可见变化时回调 */
// onPopupVisibleChange?: (popupVisible: boolean) => void;
prefixCls: PropTypes.string,
inputPrefixCls: PropTypes.string,
getPopupContainer: PropTypes.func,
popupVisible: PropTypes.looseBool,
fieldNames: { type: Object as PropType<FieldNamesType> },
autofocus: PropTypes.looseBool,
suffixIcon: PropTypes.any,
showSearchRender: PropTypes.any,
onChange: PropTypes.func,
onPopupVisibleChange: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onSearch: PropTypes.func,
'onUpdate:value': PropTypes.func,
};
export type CascaderProps = Partial<ExtractPropTypes<typeof cascaderProps>>;
// We limit the filtered item count by default
const defaultLimit = 50;
function defaultFilterOption(
inputValue: string,
path: CascaderOptionType[],
names: FilledFieldNamesType,
) {
return path.some(option => option[names.label].indexOf(inputValue) > -1);
}
function defaultSortFilteredOption(
a: CascaderOptionType[],
b: CascaderOptionType[],
inputValue: string,
names: FilledFieldNamesType,
) {
function callback(elem: CascaderOptionType) {
return elem[names.label].indexOf(inputValue) > -1;
}
return a.findIndex(callback) - b.findIndex(callback);
}
function getFilledFieldNames(props: any) {
const fieldNames = (props.fieldNames || {}) as FieldNamesType;
const names: FilledFieldNamesType = {
children: fieldNames.children || 'children',
label: fieldNames.label || 'label',
value: fieldNames.value || 'value',
};
return names;
}
function flattenTree(
options: CascaderOptionType[],
props: any,
ancestor: CascaderOptionType[] = [],
) {
const names: FilledFieldNamesType = getFilledFieldNames(props);
let flattenOptions = [];
const childrenName = names.children;
options.forEach(option => {
const path = ancestor.concat(option);
if (props.changeOnSelect || !option[childrenName] || !option[childrenName].length) {
flattenOptions.push(path);
}
if (option[childrenName]) {
flattenOptions = flattenOptions.concat(flattenTree(option[childrenName], props, path));
}
});
return flattenOptions;
}
const defaultDisplayRender = ({ labels }) => labels.join(' / ');
const Cascader = defineComponent({
name: 'ACascader',
mixins: [BaseMixin],
inheritAttrs: false,
props: cascaderProps,
setup() {
const formItemContext = useInjectFormItemContext();
return {
configProvider: inject('configProvider', defaultConfigProvider),
localeData: inject('localeData', {} as any),
cachedOptions: [],
popupRef: undefined,
input: undefined,
formItemContext,
};
},
data() {
const { value, defaultValue, popupVisible, showSearch, options } = this.$props;
return {
sValue: (value || defaultValue || []) as any[],
inputValue: '',
inputFocused: false,
sPopupVisible: popupVisible as boolean,
flattenOptions: showSearch
? flattenTree(options as CascaderOptionType[], this.$props)
: undefined,
};
},
watch: {
value(val) {
this.setState({ sValue: val || [] });
},
popupVisible(val) {
this.setState({ sPopupVisible: val });
},
options(val) {
if (this.showSearch) {
this.setState({ flattenOptions: flattenTree(val, this.$props as any) });
}
},
},
// model: {
// prop: 'value',
// event: 'change',
// },
created() {
provide('savePopupRef', this.savePopupRef);
},
methods: {
savePopupRef(ref: any) {
this.popupRef = ref;
},
highlightKeyword(str: string, keyword: string, prefixCls: string | undefined) {
return str
.split(keyword)
.map((node, index) =>
index === 0
? node
: [<span class={`${prefixCls}-menu-item-keyword`}>{keyword}</span>, node],
);
},
defaultRenderFilteredOption(opt: {
inputValue: string;
path: CascaderOptionType[];
prefixCls: string | undefined;
names: FilledFieldNamesType;
}) {
const { inputValue, path, prefixCls, names } = opt;
return path.map((option, index) => {
const label = option[names.label];
const node =
label.indexOf(inputValue) > -1
? this.highlightKeyword(label, inputValue, prefixCls)
: label;
return index === 0 ? node : [' / ', node];
});
},
saveInput(node: any) {
this.input = node;
},
handleChange(value: any, selectedOptions: CascaderOptionType[]) {
this.setState({ inputValue: '' });
if (selectedOptions[0].__IS_FILTERED_OPTION) {
const unwrappedValue = value[0];
const unwrappedSelectedOptions = selectedOptions[0].path;
this.setValue(unwrappedValue, unwrappedSelectedOptions);
return;
}
this.setValue(value, selectedOptions);
},
handlePopupVisibleChange(popupVisible: boolean) {
if (!hasProp(this, 'popupVisible')) {
this.setState((state: any) => ({
sPopupVisible: popupVisible,
inputFocused: popupVisible,
inputValue: popupVisible ? state.inputValue : '',
}));
}
this.$emit('popupVisibleChange', popupVisible);
},
handleInputFocus(e: InputEvent) {
this.$emit('focus', e);
},
handleInputBlur(e: InputEvent) {
this.setState({
inputFocused: false,
});
this.$emit('blur', e);
this.formItemContext.onFieldBlur();
},
handleInputClick(e: MouseEvent & { nativeEvent?: any }) {
const { inputFocused, sPopupVisible } = this;
// Prevent `Trigger` behavior.
if (inputFocused || sPopupVisible) {
e.stopPropagation();
if (e.nativeEvent && e.nativeEvent.stopImmediatePropagation) {
e.nativeEvent.stopImmediatePropagation();
}
}
},
handleKeyDown(e: KeyboardEvent) {
if (e.keyCode === KeyCode.BACKSPACE || e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
}
},
handleInputChange(e: Event) {
const inputValue = (e.target as HTMLInputElement).value;
this.setState({ inputValue });
this.$emit('search', inputValue);
},
setValue(value: string[] | number[], selectedOptions: CascaderOptionType[] = []) {
if (!hasProp(this, 'value')) {
this.setState({ sValue: value });
}
this.$emit('update:value', value);
this.$emit('change', value, selectedOptions);
this.formItemContext.onFieldChange();
},
getLabel() {
const { options } = this;
const names = getFilledFieldNames(this.$props);
const displayRender = getComponent(this, 'displayRender', {}, false) || defaultDisplayRender;
const value = this.sValue;
const unwrappedValue = Array.isArray(value[0]) ? value[0] : value;
const selectedOptions = arrayTreeFilter<CascaderOptionType>(
options as CascaderOptionType[],
(o, level) => o[names.value] === unwrappedValue[level],
{ childrenKeyName: names.children },
);
const labels = selectedOptions.map(o => o[names.label]);
return displayRender({ labels, selectedOptions });
},
clearSelection(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
if (!this.inputValue) {
this.setValue([]);
this.handlePopupVisibleChange(false);
} else {
this.setState({ inputValue: '' });
}
},
generateFilteredOptions(
prefixCls: string | undefined,
renderEmpty: RenderEmptyHandler,
): EmptyFilteredOptionsType[] | FilteredOptionsType[] {
const { showSearch, notFoundContent } = this;
const names: FilledFieldNamesType = getFilledFieldNames(this.$props);
const {
filter = defaultFilterOption,
// render = this.defaultRenderFilteredOption,
sort = defaultSortFilteredOption,
limit = defaultLimit,
} = showSearch as ShowSearchType;
const render =
(showSearch as ShowSearchType).render ||
getComponent(this, 'showSearchRender') ||
this.defaultRenderFilteredOption;
const { flattenOptions = [], inputValue } = this.$data;
// Limit the filter if needed
let filtered: Array<CascaderOptionType[]>;
if (limit > 0) {
filtered = [];
let matchCount = 0;
// Perf optimization to filter items only below the limit
flattenOptions.some(path => {
const match = filter(inputValue, path, names);
if (match) {
filtered.push(path);
matchCount += 1;
}
return matchCount >= limit;
});
} else {
warning(
typeof limit !== 'number',
'Cascader',
"'limit' of showSearch in Cascader should be positive number or false.",
);
filtered = flattenOptions.filter(path => filter(inputValue, path, names));
}
filtered.sort((a, b) => sort(a, b, inputValue, names));
if (filtered.length > 0) {
return filtered.map(path => {
return {
__IS_FILTERED_OPTION: true,
path,
[names.label]: render({ inputValue, path, prefixCls, names }),
[names.value]: path.map(o => o[names.value]),
disabled: path.some(o => !!o.disabled),
};
});
}
return [
{
[names.label]: notFoundContent || renderEmpty('Cascader'),
[names.value]: 'ANT_CASCADER_NOT_FOUND',
disabled: true,
},
];
},
focus() {
this.input && this.input.focus();
},
blur() {
this.input && this.input.blur();
},
},
render() {
const { sPopupVisible, inputValue, configProvider, localeData } = this;
const { sValue: value, inputFocused } = this.$data;
const props = getOptionProps(this);
let suffixIcon = getComponent(this, 'suffixIcon');
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon;
const { getPopupContainer: getContextPopupContainer } = configProvider;
const {
prefixCls: customizePrefixCls,
inputPrefixCls: customizeInputPrefixCls,
placeholder = localeData.placeholder,
size,
disabled,
allowClear,
showSearch = false,
notFoundContent,
...otherProps
} = props as any;
const { onEvents, extraAttrs } = splitAttrs(this.$attrs);
const {
class: className,
style,
id = this.formItemContext.id.value,
...restAttrs
} = extraAttrs;
const getPrefixCls = this.configProvider.getPrefixCls;
const renderEmpty = this.configProvider.renderEmpty;
const rootPrefixCls = getPrefixCls();
const prefixCls = getPrefixCls('cascader', customizePrefixCls);
const inputPrefixCls = getPrefixCls('input', customizeInputPrefixCls);
const sizeCls = classNames({
[`${inputPrefixCls}-lg`]: size === 'large',
[`${inputPrefixCls}-sm`]: size === 'small',
});
const clearIcon =
(allowClear && !disabled && value.length > 0) || inputValue ? (
<CloseCircleFilled
class={`${prefixCls}-picker-clear`}
onClick={this.clearSelection}
key="clear-icon"
/>
) : null;
const arrowCls = classNames({
[`${prefixCls}-picker-arrow`]: true,
[`${prefixCls}-picker-arrow-expand`]: sPopupVisible,
});
const pickerCls = classNames(className, `${prefixCls}-picker`, {
[`${prefixCls}-picker-with-value`]: inputValue,
[`${prefixCls}-picker-disabled`]: disabled,
[`${prefixCls}-picker-${size}`]: !!size,
[`${prefixCls}-picker-show-search`]: !!showSearch,
[`${prefixCls}-picker-focused`]: inputFocused,
});
// Fix bug of https://github.com/facebook/react/pull/5004
// and https://fb.me/react-unknown-prop
const tempInputProps = omit(otherProps, [
'popupStyle',
'options',
'popupPlacement',
'transitionName',
'displayRender',
'changeOnSelect',
'expandTrigger',
'popupVisible',
'getPopupContainer',
'loadData',
'popupClassName',
'filterOption',
'renderFilteredOption',
'sortFilteredOption',
'notFoundContent',
'defaultValue',
'fieldNames',
'onChange',
'onPopupVisibleChange',
'onFocus',
'onBlur',
'onSearch',
'onUpdate:value',
]);
let options = props.options;
const names = getFilledFieldNames(this.$props);
if (options && options.length > 0) {
if (inputValue) {
options = this.generateFilteredOptions(prefixCls, renderEmpty);
}
} else {
options = [
{
[names.label]: notFoundContent || renderEmpty('Cascader'),
[names.value]: 'ANT_CASCADER_NOT_FOUND',
disabled: true,
},
];
}
// Dropdown menu should keep previous status until it is fully closed.
if (!sPopupVisible) {
options = this.cachedOptions;
} else {
this.cachedOptions = options;
}
const dropdownMenuColumnStyle: CSSProperties = {};
const isNotFound =
(options || []).length === 1 && options[0].value === 'ANT_CASCADER_NOT_FOUND';
if (isNotFound) {
dropdownMenuColumnStyle.height = 'auto'; // Height of one row.
}
// The default value of `matchInputWidth` is `true`
const resultListMatchInputWidth = showSearch.matchInputWidth !== false;
if (resultListMatchInputWidth && (inputValue || isNotFound) && this.input) {
dropdownMenuColumnStyle.width = findDOMNode(this.input.input).offsetWidth + 'px';
}
// showSearchfocusblurinputref='picker'
const inputProps = {
...restAttrs,
...tempInputProps,
id,
prefixCls: inputPrefixCls,
placeholder: value && value.length > 0 ? undefined : placeholder,
value: inputValue,
disabled,
readonly: !showSearch,
autocomplete: 'off',
class: `${prefixCls}-input ${sizeCls}`,
onFocus: this.handleInputFocus,
onClick: showSearch ? this.handleInputClick : noop,
onBlur: showSearch ? this.handleInputBlur : props.onBlur,
onKeydown: this.handleKeyDown,
onChange: showSearch ? this.handleInputChange : noop,
};
const children = getSlot(this);
const inputIcon = (suffixIcon &&
(isValidElement(suffixIcon) ? (
cloneElement(suffixIcon, {
class: `${prefixCls}-picker-arrow`,
})
) : (
<span class={`${prefixCls}-picker-arrow`}>{suffixIcon}</span>
))) || <DownOutlined class={arrowCls} />;
const input = children.length ? (
children
) : (
<span class={pickerCls} style={style}>
<span class={`${prefixCls}-picker-label`}>{this.getLabel()}</span>
<Input {...inputProps} ref={this.saveInput} />
{clearIcon}
{inputIcon}
</span>
);
const expandIcon = <RightOutlined />;
const loadingIcon = (
<span class={`${prefixCls}-menu-item-loading-icon`}>
<RedoOutlined spin />
</span>
);
const getPopupContainer = props.getPopupContainer || getContextPopupContainer;
const cascaderProps = {
...props,
getPopupContainer,
options,
prefixCls,
value,
popupVisible: sPopupVisible,
dropdownMenuColumnStyle,
expandIcon,
loadingIcon,
...onEvents,
onPopupVisibleChange: this.handlePopupVisibleChange,
onChange: this.handleChange,
transitionName: getTransitionName(rootPrefixCls, 'slide-up', props.transitionName),
};
return <VcCascader {...cascaderProps}>{input}</VcCascader>;
},
});
export default withInstall(Cascader);

View File

@ -1,169 +1,38 @@
@import '../../style/themes/index'; @import '../../style/themes/index';
@import '../../style/mixins/index'; @import '../../style/mixins/index';
@import '../../input/style/mixin'; @import '../../input/style/mixin';
@import '../../checkbox/style/mixin';
@cascader-prefix-cls: ~'@{ant-prefix}-cascader'; @cascader-prefix-cls: ~'@{ant-prefix}-cascader';
.antCheckboxFn(@checkbox-prefix-cls: ~'@{cascader-prefix-cls}-checkbox');
.@{cascader-prefix-cls} { .@{cascader-prefix-cls} {
.reset-component(); width: 184px;
&-input.@{ant-prefix}-input { &-checkbox {
// Keep it static for https://github.com/ant-design/ant-design/issues/16738 top: 0;
position: static; margin-right: @padding-xs;
width: 100%;
// https://github.com/ant-design/ant-design/issues/17582
padding-right: 24px;
// Add important to fix https://github.com/ant-design/ant-design/issues/5078
// because input.less will compile after cascader.less
background-color: transparent !important;
cursor: pointer;
}
&-picker-show-search &-input.@{ant-prefix}-input {
position: relative;
}
&-picker {
.reset-component();
position: relative;
display: inline-block;
background-color: @cascader-bg;
border-radius: @border-radius-base;
outline: 0;
cursor: pointer;
transition: color 0.3s;
&-with-value &-label {
color: transparent;
}
&-disabled {
color: @disabled-color;
background: @input-disabled-bg;
cursor: not-allowed;
.@{cascader-prefix-cls}-input {
cursor: not-allowed;
}
}
&:focus .@{cascader-prefix-cls}-input {
.active();
}
&-show-search&-focused {
color: @disabled-color;
}
&-label {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 20px;
margin-top: -10px;
padding: 0 20px 0 @control-padding-horizontal;
overflow: hidden;
line-height: 20px;
white-space: nowrap;
text-overflow: ellipsis;
}
&-clear {
position: absolute;
top: 50%;
right: @control-padding-horizontal;
z-index: 2;
width: 12px;
height: 12px;
margin-top: -6px;
color: @disabled-color;
font-size: @font-size-sm;
line-height: 12px;
background: @component-background;
cursor: pointer;
opacity: 0;
transition: color 0.3s ease, opacity 0.15s ease;
&:hover {
color: @text-color-secondary;
}
}
&:hover &-clear {
opacity: 1;
}
// arrow
&-arrow {
position: absolute;
top: 50%;
right: @control-padding-horizontal;
z-index: 1;
width: 12px;
height: 12px;
margin-top: -6px;
color: @disabled-color;
font-size: 12px;
line-height: 12px;
transition: transform 0.2s;
&&-expand {
transform: rotate(180deg);
}
}
}
// https://github.com/ant-design/ant-design/pull/12407#issuecomment-424657810
&-picker-label:hover + &-input {
.hover();
}
&-picker-small &-picker-clear,
&-picker-small &-picker-arrow {
right: @control-padding-horizontal-sm;
} }
&-menus { &-menus {
position: absolute; display: flex;
z-index: @zindex-dropdown; flex-wrap: nowrap;
font-size: @cascader-dropdown-font-size; align-items: flex-start;
white-space: nowrap;
background: @cascader-menu-bg;
border-radius: @border-radius-base;
box-shadow: @box-shadow-base;
ul, &.@{cascader-prefix-cls}-menu-empty {
ol { .@{cascader-prefix-cls}-menu {
margin: 0; width: 100%;
list-style: none; height: auto;
} }
&-empty,
&-hidden {
display: none;
}
&.slide-up-enter.slide-up-enter-active&-placement-bottomLeft,
&.slide-up-appear.slide-up-appear-active&-placement-bottomLeft {
animation-name: antSlideUpIn;
}
&.slide-up-enter.slide-up-enter-active&-placement-topLeft,
&.slide-up-appear.slide-up-appear-active&-placement-topLeft {
animation-name: antSlideDownIn;
}
&.slide-up-leave.slide-up-leave-active&-placement-bottomLeft {
animation-name: antSlideUpOut;
}
&.slide-up-leave.slide-up-leave-active&-placement-topLeft {
animation-name: antSlideDownOut;
} }
} }
&-menu { &-menu {
display: inline-block;
min-width: 111px; min-width: 111px;
height: 180px; height: 180px;
margin: 0; margin: 0;
margin: -@dropdown-edge-child-vertical-padding 0;
padding: @cascader-dropdown-edge-child-vertical-padding 0; padding: @cascader-dropdown-edge-child-vertical-padding 0;
overflow: auto; overflow: auto;
vertical-align: top; vertical-align: top;
@ -171,60 +40,65 @@
border-right: @border-width-base @border-style-base @cascader-menu-border-color-split; border-right: @border-width-base @border-style-base @cascader-menu-border-color-split;
-ms-overflow-style: -ms-autohiding-scrollbar; // https://github.com/ant-design/ant-design/issues/11857 -ms-overflow-style: -ms-autohiding-scrollbar; // https://github.com/ant-design/ant-design/issues/11857
&:first-child { &-item {
border-radius: @border-radius-base 0 0 @border-radius-base; display: flex;
} flex-wrap: nowrap;
&:last-child { align-items: center;
margin-right: -1px; padding: @cascader-dropdown-vertical-padding @control-padding-horizontal;
border-right-color: transparent; overflow: hidden;
border-radius: 0 @border-radius-base @border-radius-base 0; line-height: @cascader-dropdown-line-height;
} white-space: nowrap;
&:only-child { text-overflow: ellipsis;
border-radius: @border-radius-base; cursor: pointer;
} transition: all 0.3s;
}
&-menu-item {
padding: @cascader-dropdown-vertical-padding @control-padding-horizontal;
line-height: @cascader-dropdown-line-height;
white-space: nowrap;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: @item-hover-bg;
}
&-disabled {
color: @disabled-color;
cursor: not-allowed;
&:hover {
background: transparent;
}
}
&-active:not(&-disabled) {
&,
&:hover {
font-weight: @select-item-selected-font-weight;
background-color: @cascader-item-selected-bg;
}
}
&-expand {
position: relative;
padding-right: 24px;
}
&-expand &-expand-icon, &:hover {
&-loading-icon { background: @item-hover-bg;
.iconfont-size-under-12px(10px); }
position: absolute; &-disabled {
right: @control-padding-horizontal;
color: @text-color-secondary;
.@{cascader-prefix-cls}-menu-item-disabled& {
color: @disabled-color; color: @disabled-color;
} cursor: not-allowed;
}
& &-keyword { &:hover {
color: @highlight-color; background: transparent;
}
}
.@{cascader-prefix-cls}-menu-empty & {
color: @disabled-color;
cursor: default;
pointer-events: none;
}
&-active:not(&-disabled) {
&,
&:hover {
font-weight: @select-item-selected-font-weight;
background-color: @cascader-item-selected-bg;
}
}
&-content {
flex: auto;
}
&-expand &-expand-icon,
&-loading-icon {
margin-left: @padding-xss;
color: @text-color-secondary;
font-size: 10px;
.@{cascader-prefix-cls}-menu-item-disabled& {
color: @disabled-color;
}
}
&-keyword {
color: @highlight-color;
}
} }
} }
} }
@import './rtl';

View File

@ -3,4 +3,4 @@ import './index.less';
// style dependencies // style dependencies
import '../../empty/style'; import '../../empty/style';
import '../../input/style'; import '../../select/style';

View File

@ -0,0 +1,19 @@
// We can not import reference of `./index` directly since it will make dead loop in less
@import (reference) '../../style/themes/index';
@cascader-prefix-cls: ~'@{ant-prefix}-cascader';
.@{cascader-prefix-cls}-rtl {
.@{cascader-prefix-cls}-menu-item {
&-expand-icon,
&-loading-icon {
margin-right: @padding-xss;
margin-left: 0;
}
}
.@{cascader-prefix-cls}-checkbox {
top: 0;
margin-right: 0;
margin-left: @padding-xs;
}
}

View File

@ -1,3 +1,27 @@
import { computed, defineComponent, ref, toRef, toRefs, watchEffect } from 'vue';
import type { CSSProperties, ExtractPropTypes, PropType, Ref } from 'vue';
import type { BaseSelectRef, BaseSelectProps } from '../vc-select';
import type { DisplayValueType, Placement } from '../vc-select/BaseSelect';
import { baseSelectPropsWithoutPrivate } from '../vc-select/BaseSelect';
import omit from '../_util/omit';
import type { Key, VueNode } from '../_util/type';
import PropTypes from '../_util/vue-types';
import { initDefaultProps } from '../_util/props-util';
import useId from '../vc-select/hooks/useId';
import useMergedState from '../_util/hooks/useMergedState';
import { fillFieldNames, toPathKey, toPathKeys } from './utils/commonUtil';
import useEntities from './hooks/useEntities';
import useSearchConfig from './hooks/useSearchConfig';
import useSearchOptions from './hooks/useSearchOptions';
import useMissingValues from './hooks/useMissingValues';
import { formatStrategyValues, toPathOptions } from './utils/treeUtil';
import { conductCheck } from '../vc-tree/utils/conductUtil';
import useDisplayValues from './hooks/useDisplayValues';
import { warning } from '../vc-util/warning';
import { useProvideCascader } from './context';
import OptionList from './OptionList';
import { BaseSelect } from '../vc-select';
export interface ShowSearchType<OptionType extends BaseOptionType = DefaultOptionType> { export interface ShowSearchType<OptionType extends BaseOptionType = DefaultOptionType> {
filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean; filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean;
render?: (arg?: { render?: (arg?: {
@ -30,100 +54,110 @@ export interface BaseOptionType {
[name: string]: any; [name: string]: any;
} }
export interface DefaultOptionType extends BaseOptionType { export interface DefaultOptionType extends BaseOptionType {
label: React.ReactNode; label?: any;
value?: string | number | null; value?: string | number | null;
children?: DefaultOptionType[]; children?: DefaultOptionType[];
} }
interface BaseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType> function baseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
extends Omit< return {
BaseSelectPropsWithoutPrivate, ...omit(baseSelectPropsWithoutPrivate(), ['tokenSeparators', 'mode', 'showSearch']),
'tokenSeparators' | 'labelInValue' | 'mode' | 'showSearch' // MISC
> { id: String,
// MISC prefixCls: String,
id?: string; fieldNames: Object as PropType<FieldNames>,
prefixCls?: string; children: Array as PropType<VueNode[]>,
fieldNames?: FieldNames;
children?: React.ReactElement;
// Value // Value
value?: ValueType; value: { type: [String, Number, Array] as PropType<ValueType> },
defaultValue?: ValueType; defaultValue: { type: [String, Number, Array] as PropType<ValueType> },
changeOnSelect?: boolean; changeOnSelect: { type: Boolean, default: undefined },
onChange?: (value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void; onChange: Function as PropType<
displayRender?: (label: string[], selectedOptions?: OptionType[]) => React.ReactNode; (value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void
checkable?: boolean | React.ReactNode; >,
displayRender: Function as PropType<
(opt: { labels: string[]; selectedOptions?: OptionType[] }) => any
>,
checkable: { type: Boolean, default: undefined },
// Search // Search
showSearch?: boolean | ShowSearchType<OptionType>; showSearch: { type: [Boolean, Object] as PropType<boolean | ShowSearchType<OptionType>> },
searchValue?: string; searchValue: String,
onSearch?: (value: string) => void; onSearch: Function as PropType<(value: string) => void>,
// Trigger // Trigger
expandTrigger?: 'hover' | 'click'; expandTrigger: String as PropType<'hover' | 'click'>,
// Options // Options
options?: OptionType[]; options: Array as PropType<OptionType[]>,
/** @private Internal usage. Do not use in your production. */ /** @private Internal usage. Do not use in your production. */
dropdownPrefixCls?: string; dropdownPrefixCls: String,
loadData?: (selectOptions: OptionType[]) => void; loadData: Function as PropType<(selectOptions: OptionType[]) => void>,
// Open // Open
/** @deprecated Use `open` instead */ /** @deprecated Use `open` instead */
popupVisible?: boolean; popupVisible: { type: Boolean, default: undefined },
/** @deprecated Use `dropdownClassName` instead */ /** @deprecated Use `dropdownClassName` instead */
popupClassName?: string; popupClassName: String,
dropdownClassName?: string; dropdownClassName: String,
dropdownMenuColumnStyle?: React.CSSProperties; dropdownMenuColumnStyle: Object as PropType<CSSProperties>,
/** @deprecated Use `placement` instead */ /** @deprecated Use `placement` instead */
popupPlacement?: Placement; popupPlacement: String as PropType<Placement>,
placement?: Placement; placement: String as PropType<Placement>,
/** @deprecated Use `onDropdownVisibleChange` instead */ /** @deprecated Use `onDropdownVisibleChange` instead */
onPopupVisibleChange?: (open: boolean) => void; onPopupVisibleChange: Function as PropType<(open: boolean) => void>,
onDropdownVisibleChange?: (open: boolean) => void; onDropdownVisibleChange: Function as PropType<(open: boolean) => void>,
// Icon // Icon
expandIcon?: React.ReactNode; expandIcon: PropTypes.any,
loadingIcon?: React.ReactNode; loadingIcon: PropTypes.any,
};
} }
export type BaseCascaderProps = Partial<ExtractPropTypes<ReturnType<typeof baseCascaderProps>>>;
type OnSingleChange<OptionType> = (value: SingleValueType, selectOptions: OptionType[]) => void; type OnSingleChange<OptionType> = (value: SingleValueType, selectOptions: OptionType[]) => void;
type OnMultipleChange<OptionType> = ( type OnMultipleChange<OptionType> = (
value: SingleValueType[], value: SingleValueType[],
selectOptions: OptionType[][], selectOptions: OptionType[][],
) => void; ) => void;
export interface SingleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType> export function singleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
extends BaseCascaderProps<OptionType> { return {
checkable?: false; ...baseCascaderProps(),
checkable: Boolean as PropType<false>,
onChange?: OnSingleChange<OptionType>; onChange: Function as PropType<OnSingleChange<OptionType>>,
};
} }
export interface MultipleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType> export type SingleCascaderProps = Partial<ExtractPropTypes<ReturnType<typeof singleCascaderProps>>>;
extends BaseCascaderProps<OptionType> {
checkable: true | React.ReactNode;
onChange?: OnMultipleChange<OptionType>; export function multipleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
return {
...baseCascaderProps(),
checkable: Boolean as PropType<true>,
onChange: Function as PropType<OnMultipleChange<OptionType>>,
};
} }
export type CascaderProps<OptionType extends BaseOptionType = DefaultOptionType> = export type MultipleCascaderProps = Partial<
| SingleCascaderProps<OptionType> ExtractPropTypes<ReturnType<typeof singleCascaderProps>>
| MultipleCascaderProps<OptionType>; >;
type InternalCascaderProps<OptionType extends BaseOptionType = DefaultOptionType> = Omit< export function internalCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
SingleCascaderProps<OptionType> | MultipleCascaderProps<OptionType>, return {
'onChange' ...baseCascaderProps(),
> & { onChange: Function as PropType<
onChange?: ( (value: ValueType, selectOptions: OptionType[] | OptionType[][]) => void
value: SingleValueType | SingleValueType[], >,
selectOptions: OptionType[] | OptionType[][], customSlots: Object as PropType<Record<string, Function>>,
) => void; };
}; }
export type CascaderProps = Partial<ExtractPropTypes<ReturnType<typeof internalCascaderProps>>>;
export type CascaderRef = Omit<BaseSelectRef, 'scrollTo'>; export type CascaderRef = Omit<BaseSelectRef, 'scrollTo'>;
function isMultipleValue(value: ValueType): value is SingleValueType[] { function isMultipleValue(value: ValueType): value is SingleValueType[] {
@ -142,270 +176,242 @@ function toRawValues(value: ValueType): SingleValueType[] {
return value.length === 0 ? [] : [value]; return value.length === 0 ? [] : [value];
} }
const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, ref) => { export default defineComponent({
const { name: 'Cascader',
// MISC inheritAttrs: false,
id, props: initDefaultProps(internalCascaderProps(), {}),
prefixCls = 'rc-cascader', setup(props, { attrs, expose, slots }) {
fieldNames, const mergedId = useId(toRef(props, 'id'));
const multiple = computed(() => !!props.checkable);
// Value // =========================== Values ===========================
defaultValue, const [rawValues, setRawValues] = useMergedState<ValueType, Ref<SingleValueType[]>>(
value, props.defaultValue,
changeOnSelect, {
onChange, value: computed(() => props.value),
displayRender, postState: toRawValues,
checkable, },
);
// Search // ========================= FieldNames =========================
searchValue, const mergedFieldNames = computed(() => fillFieldNames(props.fieldNames));
onSearch,
showSearch,
// Trigger // =========================== Option ===========================
expandTrigger, const mergedOptions = computed(() => props.options || []);
// Options // Only used in multiple mode, this fn will not call in single mode
options, const pathKeyEntities = useEntities(mergedOptions, mergedFieldNames);
dropdownPrefixCls,
loadData,
// Open /** Convert path key back to value format */
popupVisible, const getValueByKeyPath = (pathKeys: Key[]): SingleValueType[] => {
open, const ketPathEntities = pathKeyEntities.value;
popupClassName,
dropdownClassName,
dropdownMenuColumnStyle,
popupPlacement,
placement,
onDropdownVisibleChange,
onPopupVisibleChange,
// Icon
expandIcon = '>',
loadingIcon,
// Children
children,
...restProps
} = props;
const mergedId = useId(id);
const multiple = !!checkable;
// =========================== Values ===========================
const [rawValues, setRawValues] = useMergedState<ValueType, SingleValueType[]>(defaultValue, {
value,
postState: toRawValues,
});
// ========================= FieldNames =========================
const mergedFieldNames = React.useMemo(
() => fillFieldNames(fieldNames),
/* eslint-disable react-hooks/exhaustive-deps */
[JSON.stringify(fieldNames)],
/* eslint-enable react-hooks/exhaustive-deps */
);
// =========================== Option ===========================
const mergedOptions = React.useMemo(() => options || [], [options]);
// Only used in multiple mode, this fn will not call in single mode
const getPathKeyEntities = useEntities(mergedOptions, mergedFieldNames);
/** Convert path key back to value format */
const getValueByKeyPath = React.useCallback(
(pathKeys: React.Key[]): SingleValueType[] => {
const ketPathEntities = getPathKeyEntities();
return pathKeys.map(pathKey => { return pathKeys.map(pathKey => {
const { nodes } = ketPathEntities[pathKey]; const { nodes } = ketPathEntities[pathKey];
return nodes.map(node => node[mergedFieldNames.value]); return nodes.map(node => node[mergedFieldNames.value.value]);
}); });
}, };
[getPathKeyEntities, mergedFieldNames],
);
// =========================== Search =========================== // =========================== Search ===========================
const [mergedSearchValue, setSearchValue] = useMergedState('', { const [mergedSearchValue, setSearchValue] = useMergedState('', {
value: searchValue, value: computed(() => props.searchValue),
postState: search => search || '', postState: search => search || '',
}); });
const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => { const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => {
setSearchValue(searchText); setSearchValue(searchText);
if (info.source !== 'blur' && onSearch) { if (info.source !== 'blur' && props.onSearch) {
onSearch(searchText); props.onSearch(searchText);
} }
}; };
const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch); const { showSearch: mergedShowSearch, searchConfig: mergedSearchConfig } = useSearchConfig(
toRef(props, 'showSearch'),
);
const searchOptions = useSearchOptions( const searchOptions = useSearchOptions(
mergedSearchValue, mergedSearchValue,
mergedOptions, mergedOptions,
mergedFieldNames, mergedFieldNames,
dropdownPrefixCls || prefixCls, computed(() => props.dropdownPrefixCls || props.prefixCls),
searchConfig, mergedSearchConfig,
changeOnSelect, toRef(props, 'changeOnSelect'),
); );
// =========================== Values =========================== // =========================== Values ===========================
const getMissingValues = useMissingValues(mergedOptions, mergedFieldNames); const missingValuesInfo = useMissingValues(mergedOptions, mergedFieldNames, rawValues);
// Fill `rawValues` with checked conduction values // Fill `rawValues` with checked conduction values
const [checkedValues, halfCheckedValues, missingCheckedValues] = React.useMemo(() => { const [checkedValues, halfCheckedValues, missingCheckedValues] = [
const [existValues, missingValues] = getMissingValues(rawValues); ref<SingleValueType[]>([]),
ref<SingleValueType[]>([]),
ref<SingleValueType[]>([]),
];
watchEffect(() => {
const [existValues, missingValues] = missingValuesInfo.value;
if (!multiple || !rawValues.length) { if (!multiple.value || !rawValues.value.length) {
return [existValues, [], missingValues]; [checkedValues.value, halfCheckedValues.value, missingCheckedValues.value] = [
} existValues,
[],
const keyPathValues = toPathKeys(existValues); missingValues,
const ketPathEntities = getPathKeyEntities(); ];
const { checkedKeys, halfCheckedKeys } = conductCheck(keyPathValues, true, ketPathEntities);
// Convert key back to value cells
return [getValueByKeyPath(checkedKeys), getValueByKeyPath(halfCheckedKeys), missingValues];
}, [multiple, rawValues, getPathKeyEntities, getValueByKeyPath, getMissingValues]);
const deDuplicatedValues = React.useMemo(() => {
const checkedKeys = toPathKeys(checkedValues);
const deduplicateKeys = formatStrategyValues(checkedKeys, getPathKeyEntities);
return [...missingCheckedValues, ...getValueByKeyPath(deduplicateKeys)];
}, [checkedValues, getPathKeyEntities, getValueByKeyPath, missingCheckedValues]);
const displayValues = useDisplayValues(
deDuplicatedValues,
mergedOptions,
mergedFieldNames,
multiple,
displayRender,
);
// =========================== Change ===========================
const triggerChange = useRefFunc((nextValues: ValueType) => {
setRawValues(nextValues);
// Save perf if no need trigger event
if (onChange) {
const nextRawValues = toRawValues(nextValues);
const valueOptions = nextRawValues.map(valueCells =>
toPathOptions(valueCells, mergedOptions, mergedFieldNames).map(valueOpt => valueOpt.option),
);
const triggerValues = multiple ? nextRawValues : nextRawValues[0];
const triggerOptions = multiple ? valueOptions : valueOptions[0];
onChange(triggerValues, triggerOptions);
}
});
// =========================== Select ===========================
const onInternalSelect = useRefFunc((valuePath: SingleValueType) => {
if (!multiple) {
triggerChange(valuePath);
} else {
// Prepare conduct required info
const pathKey = toPathKey(valuePath);
const checkedPathKeys = toPathKeys(checkedValues);
const halfCheckedPathKeys = toPathKeys(halfCheckedValues);
const existInChecked = checkedPathKeys.includes(pathKey);
const existInMissing = missingCheckedValues.some(
valueCells => toPathKey(valueCells) === pathKey,
);
// Do update
let nextCheckedValues = checkedValues;
let nextMissingValues = missingCheckedValues;
if (existInMissing && !existInChecked) {
// Missing value only do filter
nextMissingValues = missingCheckedValues.filter(
valueCells => toPathKey(valueCells) !== pathKey,
);
} else {
// Update checked key first
const nextRawCheckedKeys = existInChecked
? checkedPathKeys.filter(key => key !== pathKey)
: [...checkedPathKeys, pathKey];
const pathKeyEntities = getPathKeyEntities();
// Conduction by selected or not
let checkedKeys: React.Key[];
if (existInChecked) {
({ checkedKeys } = conductCheck(
nextRawCheckedKeys,
{ checked: false, halfCheckedKeys: halfCheckedPathKeys },
pathKeyEntities,
));
} else {
({ checkedKeys } = conductCheck(nextRawCheckedKeys, true, pathKeyEntities));
}
// Roll up to parent level keys
const deDuplicatedKeys = formatStrategyValues(checkedKeys, getPathKeyEntities);
nextCheckedValues = getValueByKeyPath(deDuplicatedKeys);
} }
triggerChange([...nextMissingValues, ...nextCheckedValues]); const keyPathValues = toPathKeys(existValues);
} const ketPathEntities = pathKeyEntities.value;
});
// Display Value change logic const { checkedKeys, halfCheckedKeys } = conductCheck(keyPathValues, true, ketPathEntities);
const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (_, info) => {
if (info.type === 'clear') { // Convert key back to value cells
triggerChange([]); return [getValueByKeyPath(checkedKeys), getValueByKeyPath(halfCheckedKeys), missingValues];
return; });
const deDuplicatedValues = computed(() => {
const checkedKeys = toPathKeys(checkedValues.value);
const deduplicateKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value);
return [...missingCheckedValues.value, ...getValueByKeyPath(deduplicateKeys)];
});
const displayValues = useDisplayValues(
deDuplicatedValues,
mergedOptions,
mergedFieldNames,
multiple,
toRef(props, 'displayRender'),
);
// =========================== Change ===========================
const triggerChange = (nextValues: ValueType) => {
setRawValues(nextValues);
// Save perf if no need trigger event
if (props.onChange) {
const nextRawValues = toRawValues(nextValues);
const valueOptions = nextRawValues.map(valueCells =>
toPathOptions(valueCells, mergedOptions.value, mergedFieldNames.value).map(
valueOpt => valueOpt.option,
),
);
const triggerValues = multiple.value ? nextRawValues : nextRawValues[0];
const triggerOptions = multiple.value ? valueOptions : valueOptions[0];
props.onChange(triggerValues, triggerOptions);
}
};
// =========================== Select ===========================
const onInternalSelect = (valuePath: SingleValueType) => {
if (!multiple.value) {
triggerChange(valuePath);
} else {
// Prepare conduct required info
const pathKey = toPathKey(valuePath);
const checkedPathKeys = toPathKeys(checkedValues.value);
const halfCheckedPathKeys = toPathKeys(halfCheckedValues.value);
const existInChecked = checkedPathKeys.includes(pathKey);
const existInMissing = missingCheckedValues.value.some(
valueCells => toPathKey(valueCells) === pathKey,
);
// Do update
let nextCheckedValues = checkedValues.value;
let nextMissingValues = missingCheckedValues.value;
if (existInMissing && !existInChecked) {
// Missing value only do filter
nextMissingValues = missingCheckedValues.value.filter(
valueCells => toPathKey(valueCells) !== pathKey,
);
} else {
// Update checked key first
const nextRawCheckedKeys = existInChecked
? checkedPathKeys.filter(key => key !== pathKey)
: [...checkedPathKeys, pathKey];
// Conduction by selected or not
let checkedKeys: Key[];
if (existInChecked) {
({ checkedKeys } = conductCheck(
nextRawCheckedKeys,
{ checked: false, halfCheckedKeys: halfCheckedPathKeys },
pathKeyEntities.value,
));
} else {
({ checkedKeys } = conductCheck(nextRawCheckedKeys, true, pathKeyEntities.value));
}
// Roll up to parent level keys
const deDuplicatedKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value);
nextCheckedValues = getValueByKeyPath(deDuplicatedKeys);
}
triggerChange([...nextMissingValues, ...nextCheckedValues]);
}
};
// Display Value change logic
const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (_, info) => {
if (info.type === 'clear') {
triggerChange([]);
return;
}
// Cascader do not support `add` type. Only support `remove`
const { valueCells } = info.values[0] as DisplayValueType & { valueCells: SingleValueType };
onInternalSelect(valueCells);
};
// ============================ Open ============================
if (process.env.NODE_ENV !== 'production') {
watchEffect(() => {
warning(
!props.onPopupVisibleChange,
'`popupVisibleChange` is deprecated. Please use `dropdownVisibleChange` instead.',
);
warning(
props.popupVisible === undefined,
'`popupVisible` is deprecated. Please use `open` instead.',
);
warning(
props.popupClassName === undefined,
'`popupClassName` is deprecated. Please use `dropdownClassName` instead.',
);
warning(
props.popupPlacement === undefined,
'`popupPlacement` is deprecated. Please use `placement` instead.',
);
});
} }
// Cascader do not support `add` type. Only support `remove` const mergedOpen = computed(() => (props.open !== undefined ? props.open : props.popupVisible));
const { valueCells } = info.values[0] as DisplayValueType & { valueCells: SingleValueType };
onInternalSelect(valueCells);
};
// ============================ Open ============================ const mergedDropdownClassName = computed(() => props.dropdownClassName || props.popupClassName);
if (process.env.NODE_ENV !== 'production') {
warning(
!onPopupVisibleChange,
'`onPopupVisibleChange` is deprecated. Please use `onDropdownVisibleChange` instead.',
);
warning(popupVisible === undefined, '`popupVisible` is deprecated. Please use `open` instead.');
warning(
popupClassName === undefined,
'`popupClassName` is deprecated. Please use `dropdownClassName` instead.',
);
warning(
popupPlacement === undefined,
'`popupPlacement` is deprecated. Please use `placement` instead.',
);
}
const mergedOpen = open !== undefined ? open : popupVisible; const mergedPlacement = computed(() => props.placement || props.popupPlacement);
const mergedDropdownClassName = dropdownClassName || popupClassName; const onInternalDropdownVisibleChange = (nextVisible: boolean) => {
props.onDropdownVisibleChange?.(nextVisible);
const mergedPlacement = placement || popupPlacement; props.onPopupVisibleChange?.(nextVisible);
};
const onInternalDropdownVisibleChange = (nextVisible: boolean) => { const {
onDropdownVisibleChange?.(nextVisible); changeOnSelect,
onPopupVisibleChange?.(nextVisible); checkable,
}; dropdownPrefixCls,
loadData,
// ========================== Context =========================== expandTrigger,
const cascaderContext = React.useMemo( expandIcon,
() => ({ loadingIcon,
dropdownMenuColumnStyle,
customSlots,
} = toRefs(props);
useProvideCascader({
options: mergedOptions, options: mergedOptions,
fieldNames: mergedFieldNames, fieldNames: mergedFieldNames,
values: checkedValues, values: checkedValues,
@ -420,81 +426,116 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
expandIcon, expandIcon,
loadingIcon, loadingIcon,
dropdownMenuColumnStyle, dropdownMenuColumnStyle,
}), customSlots,
[ });
mergedOptions, const selectRef = ref<BaseSelectRef>();
mergedFieldNames,
checkedValues,
halfCheckedValues,
changeOnSelect,
onInternalSelect,
checkable,
searchOptions,
dropdownPrefixCls,
loadData,
expandTrigger,
expandIcon,
loadingIcon,
dropdownMenuColumnStyle,
],
);
// ============================================================== expose({
// == Render == focus() {
// ============================================================== selectRef.value?.focus();
const emptyOptions = !(mergedSearchValue ? searchOptions : mergedOptions).length; },
blur() {
selectRef.value?.blur();
},
scrollTo(arg) {
selectRef.value?.scrollTo(arg);
},
} as BaseSelectRef);
const dropdownStyle: React.CSSProperties = const pickProps = computed(() => {
// Search to match width return omit(props, [
(mergedSearchValue && searchConfig.matchInputWidth) || 'id',
// Empty keep the width 'prefixCls',
emptyOptions 'fieldNames',
? {}
: {
minWidth: 'auto',
};
return (
<CascaderContext.Provider value={cascaderContext}>
<BaseSelect
{...restProps}
// MISC
ref={ref as any}
id={mergedId}
prefixCls={prefixCls}
dropdownMatchSelectWidth={false}
dropdownStyle={dropdownStyle}
// Value // Value
displayValues={displayValues} 'defaultValue',
onDisplayValuesChange={onDisplayValuesChange} 'value',
mode={multiple ? 'multiple' : undefined} 'changeOnSelect',
'onChange',
'displayRender',
'checkable',
// Search // Search
searchValue={mergedSearchValue} 'searchValue',
onSearch={onInternalSearch} 'onSearch',
showSearch={mergedShowSearch} 'showSearch',
// Trigger
'expandTrigger',
// Options // Options
OptionList={OptionList} 'options',
emptyOptions={emptyOptions} 'dropdownPrefixCls',
'loadData',
// Open // Open
open={mergedOpen} 'popupVisible',
dropdownClassName={mergedDropdownClassName} 'open',
placement={mergedPlacement}
onDropdownVisibleChange={onInternalDropdownVisibleChange} 'popupClassName',
'dropdownClassName',
'dropdownMenuColumnStyle',
'popupPlacement',
'placement',
'onDropdownVisibleChange',
'onPopupVisibleChange',
// Icon
'expandIcon',
'loadingIcon',
'customSlots',
// Children // Children
getRawInputElement={() => children} 'children',
/> ]);
</CascaderContext.Provider> });
); return () => {
}) as (<OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType>( const emptyOptions = !(mergedSearchValue.value ? searchOptions.value : mergedOptions.value)
props: React.PropsWithChildren<CascaderProps<OptionType>> & { .length;
ref?: React.Ref<BaseSelectRef>;
const dropdownStyle: CSSProperties =
// Search to match width
(mergedSearchValue.value && mergedSearchConfig.value.matchInputWidth) ||
// Empty keep the width
emptyOptions
? {}
: {
minWidth: 'auto',
};
return (
<BaseSelect
{...pickProps.value}
{...attrs}
// MISC
ref={selectRef}
id={mergedId}
prefixCls={props.prefixCls}
dropdownMatchSelectWidth={false}
dropdownStyle={dropdownStyle}
// Value
displayValues={displayValues.value}
onDisplayValuesChange={onDisplayValuesChange}
mode={multiple.value ? 'multiple' : undefined}
// Search
searchValue={mergedSearchValue.value}
onSearch={onInternalSearch}
showSearch={mergedShowSearch.value}
// Options
OptionList={OptionList}
emptyOptions={emptyOptions}
// Open
open={mergedOpen.value}
dropdownClassName={mergedDropdownClassName.value}
placement={mergedPlacement.value}
onDropdownVisibleChange={onInternalDropdownVisibleChange}
// Children
getRawInputElement={() => slots.default?.()}
v-slots={slots}
/>
);
};
}, },
) => React.ReactElement) & { });
displayName?: string;
};
if (process.env.NODE_ENV !== 'production') {
Cascader.displayName = 'Cascader';
}
export default Cascader;

View File

@ -16,9 +16,9 @@ export default function Checkbox({
disabled, disabled,
onClick, onClick,
}: CheckboxProps) { }: CheckboxProps) {
const { slotsContext, checkable } = useInjectCascader(); const { customSlots, checkable } = useInjectCascader();
const mergedCheckable = checkable.value === undefined ? slotsContext.value.checkable : checkable; const mergedCheckable = checkable.value === undefined ? customSlots.value.checkable : checkable;
const customCheckbox = const customCheckbox =
typeof mergedCheckable === 'function' typeof mergedCheckable === 'function'
? mergedCheckable() ? mergedCheckable()

View File

@ -1,25 +1,24 @@
import * as React from 'react';
import classNames from 'classnames';
import { isLeaf, toPathKey } from '../utils/commonUtil'; import { isLeaf, toPathKey } from '../utils/commonUtil';
import CascaderContext from '../context';
import Checkbox from './Checkbox'; import Checkbox from './Checkbox';
import type { DefaultOptionType, SingleValueType } from '../Cascader'; import type { DefaultOptionType, SingleValueType } from '../Cascader';
import { SEARCH_MARK } from '../hooks/useSearchOptions'; import { SEARCH_MARK } from '../hooks/useSearchOptions';
import type { Key } from '../../_util/type';
import { useInjectCascader } from '../context';
export interface ColumnProps { export interface ColumnProps {
prefixCls: string; prefixCls: string;
multiple?: boolean; multiple?: boolean;
options: DefaultOptionType[]; options: DefaultOptionType[];
/** Current Column opened item key */ /** Current Column opened item key */
activeValue?: React.Key; activeValue?: Key;
/** The value path before current column */ /** The value path before current column */
prevValuePath: React.Key[]; prevValuePath: Key[];
onToggleOpen: (open: boolean) => void; onToggleOpen: (open: boolean) => void;
onSelect: (valuePath: SingleValueType, leaf: boolean) => void; onSelect: (valuePath: SingleValueType, leaf: boolean) => void;
onActive: (valuePath: SingleValueType) => void; onActive: (valuePath: SingleValueType) => void;
checkedSet: Set<React.Key>; checkedSet: Set<Key>;
halfCheckedSet: Set<React.Key>; halfCheckedSet: Set<Key>;
loadingKeys: React.Key[]; loadingKeys: Key[];
isSelectable: (option: DefaultOptionType) => boolean; isSelectable: (option: DefaultOptionType) => boolean;
} }
@ -44,27 +43,29 @@ export default function Column({
fieldNames, fieldNames,
changeOnSelect, changeOnSelect,
expandTrigger, expandTrigger,
expandIcon, expandIcon: expandIconRef,
loadingIcon, loadingIcon: loadingIconRef,
dropdownMenuColumnStyle, dropdownMenuColumnStyle,
} = React.useContext(CascaderContext); customSlots,
} = useInjectCascader();
const hoverOpen = expandTrigger === 'hover'; const expandIcon = expandIconRef.value ?? customSlots.value.expandIcon?.();
const loadingIcon = loadingIconRef.value ?? customSlots.value.loadingIcon?.();
const hoverOpen = expandTrigger.value === 'hover';
// ============================ Render ============================ // ============================ Render ============================
return ( return (
<ul className={menuPrefixCls} role="menu"> <ul class={menuPrefixCls} role="menu">
{options.map(option => { {options.map(option => {
const { disabled } = option; const { disabled } = option;
const searchOptions = option[SEARCH_MARK]; const searchOptions = option[SEARCH_MARK];
const label = option[fieldNames.label]; const label = option[fieldNames.value.label];
const value = option[fieldNames.value]; const value = option[fieldNames.value.value];
const isMergedLeaf = isLeaf(option, fieldNames); const isMergedLeaf = isLeaf(option, fieldNames.value);
// Get real value of option. Search option is different way. // Get real value of option. Search option is different way.
const fullPath = searchOptions const fullPath = searchOptions
? searchOptions.map(opt => opt[fieldNames.value]) ? searchOptions.map(opt => opt[fieldNames.value.value])
: [...prevValuePath, value]; : [...prevValuePath, value];
const fullPathKey = toPathKey(fullPath); const fullPathKey = toPathKey(fullPath);
@ -75,7 +76,6 @@ export default function Column({
// >>>>> halfChecked // >>>>> halfChecked
const halfChecked = halfCheckedSet.has(fullPathKey); const halfChecked = halfCheckedSet.has(fullPathKey);
// >>>>> Open // >>>>> Open
const triggerOpenPath = () => { const triggerOpenPath = () => {
if (!disabled && (!hoverOpen || !isMergedLeaf)) { if (!disabled && (!hoverOpen || !isMergedLeaf)) {
@ -102,13 +102,16 @@ export default function Column({
return ( return (
<li <li
key={fullPathKey} key={fullPathKey}
className={classNames(menuItemPrefixCls, { class={[
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf, menuItemPrefixCls,
[`${menuItemPrefixCls}-active`]: activeValue === value, {
[`${menuItemPrefixCls}-disabled`]: disabled, [`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
[`${menuItemPrefixCls}-loading`]: isLoading, [`${menuItemPrefixCls}-active`]: activeValue === value,
})} [`${menuItemPrefixCls}-disabled`]: disabled,
style={dropdownMenuColumnStyle} [`${menuItemPrefixCls}-loading`]: isLoading,
},
]}
style={dropdownMenuColumnStyle.value}
role="menuitemcheckbox" role="menuitemcheckbox"
title={title} title={title}
aria-checked={checked} aria-checked={checked}
@ -119,12 +122,12 @@ export default function Column({
triggerSelect(); triggerSelect();
} }
}} }}
onDoubleClick={() => { onDblclick={() => {
if (changeOnSelect) { if (changeOnSelect.value) {
onToggleOpen(false); onToggleOpen(false);
} }
}} }}
onMouseEnter={() => { onMouseenter={() => {
if (hoverOpen) { if (hoverOpen) {
triggerOpenPath(); triggerOpenPath();
} }
@ -136,18 +139,18 @@ export default function Column({
checked={checked} checked={checked}
halfChecked={halfChecked} halfChecked={halfChecked}
disabled={disabled} disabled={disabled}
onClick={(e: React.MouseEvent<HTMLSpanElement>) => { onClick={(e: MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
triggerSelect(); triggerSelect();
}} }}
/> />
)} )}
<div className={`${menuItemPrefixCls}-content`}>{option[fieldNames.label]}</div> <div class={`${menuItemPrefixCls}-content`}>{option[fieldNames.value.label]}</div>
{!isLoading && expandIcon && !isMergedLeaf && ( {!isLoading && expandIcon && !isMergedLeaf && (
<div className={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div> <div class={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
)} )}
{isLoading && loadingIcon && ( {isLoading && loadingIcon && (
<div className={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div> <div class={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
)} )}
</li> </li>
); );
@ -155,3 +158,19 @@ export default function Column({
</ul> </ul>
); );
} }
Column.props = [
'prefixCls',
'multiple',
'options',
'activeValue',
'prevValuePath',
'onToggleOpen',
'onSelect',
'onActive',
'checkedSet',
'halfCheckedSet',
'loadingKeys',
'isSelectable',
];
Column.displayName = 'Column';
Column.inheritAttrs = false;

View File

@ -1,213 +1,228 @@
/* eslint-disable default-case */ /* eslint-disable default-case */
import * as React from 'react';
import classNames from 'classnames';
import { useBaseProps } from 'rc-select';
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
import Column from './Column'; import Column from './Column';
import CascaderContext from '../context';
import type { DefaultOptionType, SingleValueType } from '../Cascader'; import type { DefaultOptionType, SingleValueType } from '../Cascader';
import { isLeaf, toPathKey, toPathKeys, toPathValueStr } from '../utils/commonUtil'; import { isLeaf, toPathKey, toPathKeys, toPathValueStr } from '../utils/commonUtil';
import useActive from './useActive'; import useActive from './useActive';
import useKeyboard from './useKeyboard'; import useKeyboard from './useKeyboard';
import { toPathOptions } from '../utils/treeUtil'; import { toPathOptions } from '../utils/treeUtil';
import { computed, defineComponent, ref, shallowRef, watchEffect } from 'vue';
import { useBaseProps } from '../../vc-select';
import { useInjectCascader } from '../context';
import type { Key } from '../../_util/type';
import type { EventHandler } from '../../_util/EventInterface';
const RefOptionList = React.forwardRef<RefOptionListProps>((props, ref) => { export default defineComponent({
const { prefixCls, multiple, searchValue, toggleOpen, notFoundContent, direction } = name: 'OptionList',
useBaseProps(); inheritAttrs: false,
setup(_props, context) {
const { attrs, slots } = context;
const baseProps = useBaseProps();
const containerRef = ref<HTMLDivElement>();
const rtl = computed(() => baseProps.direction === 'rtl');
const {
options,
values,
halfValues,
fieldNames,
changeOnSelect,
onSelect,
searchOptions,
dropdownPrefixCls,
loadData,
expandTrigger,
customSlots,
} = useInjectCascader();
const containerRef = React.useRef<HTMLDivElement>(); const mergedPrefixCls = computed(() => dropdownPrefixCls.value || baseProps.prefixCls);
const rtl = direction === 'rtl';
const { // ========================= loadData =========================
options, const loadingKeys = shallowRef<string[]>([]);
values, const internalLoadData = (valueCells: Key[]) => {
halfValues, // Do not load when search
fieldNames, if (!loadData.value || baseProps.searchValue) {
changeOnSelect, return;
onSelect,
searchOptions,
dropdownPrefixCls,
loadData,
expandTrigger,
} = React.useContext(CascaderContext);
const mergedPrefixCls = dropdownPrefixCls || prefixCls;
// ========================= loadData =========================
const [loadingKeys, setLoadingKeys] = React.useState([]);
const internalLoadData = (valueCells: React.Key[]) => {
// Do not load when search
if (!loadData || searchValue) {
return;
}
const optionList = toPathOptions(valueCells, options, fieldNames);
const rawOptions = optionList.map(({ option }) => option);
const lastOption = rawOptions[rawOptions.length - 1];
if (lastOption && !isLeaf(lastOption, fieldNames)) {
const pathKey = toPathKey(valueCells);
setLoadingKeys(keys => [...keys, pathKey]);
loadData(rawOptions);
}
};
// zombieJ: This is bad. We should make this same as `rc-tree` to use Promise instead.
React.useEffect(() => {
if (loadingKeys.length) {
loadingKeys.forEach(loadingKey => {
const valueStrCells = toPathValueStr(loadingKey);
const optionList = toPathOptions(valueStrCells, options, fieldNames, true).map(
({ option }) => option,
);
const lastOption = optionList[optionList.length - 1];
if (!lastOption || lastOption[fieldNames.children] || isLeaf(lastOption, fieldNames)) {
setLoadingKeys(keys => keys.filter(key => key !== loadingKey));
}
});
}
}, [options, loadingKeys, fieldNames]);
// ========================== Values ==========================
const checkedSet = React.useMemo(() => new Set(toPathKeys(values)), [values]);
const halfCheckedSet = React.useMemo(() => new Set(toPathKeys(halfValues)), [halfValues]);
// ====================== Accessibility =======================
const [activeValueCells, setActiveValueCells] = useActive();
// =========================== Path ===========================
const onPathOpen = (nextValueCells: React.Key[]) => {
setActiveValueCells(nextValueCells);
// Trigger loadData
internalLoadData(nextValueCells);
};
const isSelectable = (option: DefaultOptionType) => {
const { disabled } = option;
const isMergedLeaf = isLeaf(option, fieldNames);
return !disabled && (isMergedLeaf || changeOnSelect || multiple);
};
const onPathSelect = (valuePath: SingleValueType, leaf: boolean, fromKeyboard = false) => {
onSelect(valuePath);
if (!multiple && (leaf || (changeOnSelect && (expandTrigger === 'hover' || fromKeyboard)))) {
toggleOpen(false);
}
};
// ========================== Option ==========================
const mergedOptions = React.useMemo(() => {
if (searchValue) {
return searchOptions;
}
return options;
}, [searchValue, searchOptions, options]);
// ========================== Column ==========================
const optionColumns = React.useMemo(() => {
const optionList = [{ options: mergedOptions }];
let currentList = mergedOptions;
for (let i = 0; i < activeValueCells.length; i += 1) {
const activeValueCell = activeValueCells[i];
const currentOption = currentList.find(
option => option[fieldNames.value] === activeValueCell,
);
const subOptions = currentOption?.[fieldNames.children];
if (!subOptions?.length) {
break;
} }
currentList = subOptions; const optionList = toPathOptions(valueCells, options.value, fieldNames.value);
optionList.push({ options: subOptions }); const rawOptions = optionList.map(({ option }) => option);
} const lastOption = rawOptions[rawOptions.length - 1];
return optionList; if (lastOption && !isLeaf(lastOption, fieldNames.value)) {
}, [mergedOptions, activeValueCells, fieldNames]); const pathKey = toPathKey(valueCells);
// ========================= Keyboard ========================= loadingKeys.value = [...loadingKeys.value, pathKey];
const onKeyboardSelect = (selectValueCells: SingleValueType, option: DefaultOptionType) => { loadData.value(rawOptions);
if (isSelectable(option)) { }
onPathSelect(selectValueCells, isLeaf(option, fieldNames), true); };
}
};
useKeyboard( watchEffect(() => {
ref, if (loadingKeys.value.length) {
mergedOptions, loadingKeys.value.forEach(loadingKey => {
fieldNames, const valueStrCells = toPathValueStr(loadingKey);
activeValueCells, const optionList = toPathOptions(
onPathOpen, valueStrCells,
containerRef, options.value,
onKeyboardSelect, fieldNames.value,
); true,
).map(({ option }) => option);
const lastOption = optionList[optionList.length - 1];
// ========================== Render ========================== if (
// >>>>> Empty !lastOption ||
const isEmpty = !optionColumns[0]?.options?.length; lastOption[fieldNames.value.children] ||
isLeaf(lastOption, fieldNames.value)
) {
loadingKeys.value = loadingKeys.value.filter(key => key !== loadingKey);
}
});
}
});
const emptyList: DefaultOptionType[] = [ // ========================== Values ==========================
{ const checkedSet = computed(() => new Set(toPathKeys(values.value)));
[fieldNames.label as 'label']: notFoundContent, const halfCheckedSet = computed(() => new Set(toPathKeys(halfValues.value)));
[fieldNames.value as 'value']: '__EMPTY__',
disabled: true,
},
];
const columnProps = { // ====================== Accessibility =======================
...props, const [activeValueCells, setActiveValueCells] = useActive();
multiple: !isEmpty && multiple,
onSelect: onPathSelect,
onActive: onPathOpen,
onToggleOpen: toggleOpen,
checkedSet,
halfCheckedSet,
loadingKeys,
isSelectable,
};
// >>>>> Columns // =========================== Path ===========================
const mergedOptionColumns = isEmpty ? [{ options: emptyList }] : optionColumns; const onPathOpen = (nextValueCells: Key[]) => {
setActiveValueCells(nextValueCells);
const columnNodes: React.ReactElement[] = mergedOptionColumns.map((col, index) => { // Trigger loadData
const prevValuePath = activeValueCells.slice(0, index); internalLoadData(nextValueCells);
const activeValue = activeValueCells[index]; };
return ( const isSelectable = (option: DefaultOptionType) => {
<Column const { disabled } = option;
key={index}
{...columnProps} const isMergedLeaf = isLeaf(option, fieldNames.value);
prefixCls={mergedPrefixCls} return !disabled && (isMergedLeaf || changeOnSelect.value || baseProps.multiple);
options={col.options} };
prevValuePath={prevValuePath}
activeValue={activeValue} const onPathSelect = (valuePath: SingleValueType, leaf: boolean, fromKeyboard = false) => {
/> onSelect(valuePath);
if (
!baseProps.multiple &&
(leaf || (changeOnSelect.value && (expandTrigger.value === 'hover' || fromKeyboard)))
) {
baseProps.toggleOpen(false);
}
};
// ========================== Option ==========================
const mergedOptions = computed(() => {
if (baseProps.searchValue) {
return searchOptions.value;
}
return options.value;
});
// ========================== Column ==========================
const optionColumns = computed(() => {
const optionList = [{ options: mergedOptions.value }];
let currentList = mergedOptions.value;
for (let i = 0; i < activeValueCells.value.length; i += 1) {
const activeValueCell = activeValueCells.value[i];
const currentOption = currentList.find(
option => option[fieldNames.value.value] === activeValueCell,
);
const subOptions = currentOption?.[fieldNames.value.children];
if (!subOptions?.length) {
break;
}
currentList = subOptions;
optionList.push({ options: subOptions });
}
return optionList;
});
// ========================= Keyboard =========================
const onKeyboardSelect = (selectValueCells: SingleValueType, option: DefaultOptionType) => {
if (isSelectable(option)) {
onPathSelect(selectValueCells, isLeaf(option, fieldNames.value), true);
}
};
useKeyboard(
context,
mergedOptions,
fieldNames,
activeValueCells,
onPathOpen,
containerRef,
onKeyboardSelect,
); );
}); const onListMouseDown: EventHandler = event => {
event.preventDefault();
};
return () => {
// ========================== Render ==========================
const {
notFoundContent = slots.notFoundContent?.() || customSlots.value.notFoundContent?.(),
multiple,
toggleOpen,
} = baseProps;
// >>>>> Empty
const isEmpty = !optionColumns.value[0]?.options?.length;
// >>>>> Render const emptyList: DefaultOptionType[] = [
return ( {
<> [fieldNames.value.label as 'label']: notFoundContent,
<div [fieldNames.value.value as 'value']: '__EMPTY__',
className={classNames(`${mergedPrefixCls}-menus`, { disabled: true,
[`${mergedPrefixCls}-menu-empty`]: isEmpty, },
[`${mergedPrefixCls}-rtl`]: rtl, ];
})} const columnProps = {
ref={containerRef} ...attrs,
> multiple: !isEmpty && multiple,
{columnNodes} onSelect: onPathSelect,
</div> onActive: onPathOpen,
</> onToggleOpen: toggleOpen,
); checkedSet: checkedSet.value,
halfCheckedSet: halfCheckedSet.value,
loadingKeys: loadingKeys.value,
isSelectable,
};
// >>>>> Columns
const mergedOptionColumns = isEmpty ? [{ options: emptyList }] : optionColumns.value;
const columnNodes = mergedOptionColumns.map((col, index) => {
const prevValuePath = activeValueCells.value.slice(0, index);
const activeValue = activeValueCells.value[index];
return (
<Column
key={index}
{...columnProps}
prefixCls={mergedPrefixCls.value}
options={col.options}
prevValuePath={prevValuePath}
activeValue={activeValue}
/>
);
});
return (
<div
class={[
`${mergedPrefixCls.value}-menus`,
{
[`${mergedPrefixCls.value}-menu-empty`]: isEmpty,
[`${mergedPrefixCls.value}-rtl`]: rtl.value,
},
]}
onMousedown={onListMouseDown}
ref={containerRef}
>
{columnNodes}
</div>
);
};
},
}); });
export default RefOptionList;

View File

@ -1,28 +1,30 @@
import * as React from 'react'; import { useInjectCascader } from '../context';
import CascaderContext from '../context'; import type { Ref } from 'vue';
import { useBaseProps } from 'rc-select'; import { watch } from 'vue';
import { useBaseProps } from '../../vc-select';
import type { Key } from '../../_util/type';
import useState from '../../_util/hooks/useState';
/** /**
* Control the active open options path. * Control the active open options path.
*/ */
export default (): [React.Key[], (activeValueCells: React.Key[]) => void] => { export default (): [Ref<Key[]>, (activeValueCells: Key[]) => void] => {
const { multiple, open } = useBaseProps(); const baseProps = useBaseProps();
const { values } = React.useContext(CascaderContext); const { values } = useInjectCascader();
// Record current dropdown active options // Record current dropdown active options
// This also control the open status // This also control the open status
const [activeValueCells, setActiveValueCells] = React.useState<React.Key[]>([]); const [activeValueCells, setActiveValueCells] = useState<Key[]>([]);
React.useEffect( watch(
() => baseProps.open,
() => { () => {
if (open && !multiple) { if (baseProps.open && !baseProps.multiple) {
const firstValueCells = values[0]; const firstValueCells = values.value[0];
setActiveValueCells(firstValueCells || []); setActiveValueCells(firstValueCells || []);
} }
}, },
/* eslint-disable react-hooks/exhaustive-deps */ { immediate: true },
[open],
/* eslint-enable react-hooks/exhaustive-deps */
); );
return [activeValueCells, setActiveValueCells]; return [activeValueCells, setActiveValueCells];

View File

@ -1,36 +1,42 @@
import * as React from 'react'; import type { RefOptionListProps } from '../../vc-select/OptionList';
import type { RefOptionListProps } from 'rc-select/lib/OptionList'; import type { Key } from 'ant-design-vue/es/_util/type';
import KeyCode from 'rc-util/lib/KeyCode'; import type { Ref, SetupContext } from 'vue';
import { ref, watchEffect } from 'vue';
import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader'; import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader';
import { toPathKey } from '../utils/commonUtil'; import { toPathKey } from '../utils/commonUtil';
import { useBaseProps } from 'rc-select'; import { useBaseProps } from '../../vc-select';
import KeyCode from '../../_util/KeyCode';
export default ( export default (
ref: React.Ref<RefOptionListProps>, context: SetupContext,
options: DefaultOptionType[], options: Ref<DefaultOptionType[]>,
fieldNames: InternalFieldNames, fieldNames: Ref<InternalFieldNames>,
activeValueCells: React.Key[], activeValueCells: Ref<Key[]>,
setActiveValueCells: (activeValueCells: React.Key[]) => void, setActiveValueCells: (activeValueCells: Key[]) => void,
containerRef: React.RefObject<HTMLElement>, containerRef: Ref<HTMLElement>,
onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void, onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void,
) => { ) => {
const { direction, searchValue, toggleOpen, open } = useBaseProps(); const { direction, searchValue, toggleOpen, open } = useBaseProps();
const rtl = direction === 'rtl'; const rtl = direction === 'rtl';
const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = [
const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = React.useMemo(() => { ref<Key[]>([]),
ref<number>(),
ref<DefaultOptionType[]>([]),
];
watchEffect(() => {
let activeIndex = -1; let activeIndex = -1;
let currentOptions = options; let currentOptions = options.value;
const mergedActiveIndexes: number[] = []; const mergedActiveIndexes: number[] = [];
const mergedActiveValueCells: React.Key[] = []; const mergedActiveValueCells: Key[] = [];
const len = activeValueCells.length; const len = activeValueCells.value.length;
// Fill validate active value cells and index // Fill validate active value cells and index
for (let i = 0; i < len; i += 1) { for (let i = 0; i < len; i += 1) {
// Mark the active index for current options // Mark the active index for current options
const nextActiveIndex = currentOptions.findIndex( const nextActiveIndex = currentOptions.findIndex(
option => option[fieldNames.value] === activeValueCells[i], option => option[fieldNames.value.value] === activeValueCells.value[i],
); );
if (nextActiveIndex === -1) { if (nextActiveIndex === -1) {
@ -39,44 +45,48 @@ export default (
activeIndex = nextActiveIndex; activeIndex = nextActiveIndex;
mergedActiveIndexes.push(activeIndex); mergedActiveIndexes.push(activeIndex);
mergedActiveValueCells.push(activeValueCells[i]); mergedActiveValueCells.push(activeValueCells.value[i]);
currentOptions = currentOptions[activeIndex][fieldNames.children]; currentOptions = currentOptions[activeIndex][fieldNames.value.children];
} }
// Fill last active options // Fill last active options
let activeOptions = options; let activeOptions = options.value;
for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) { for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) {
activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.children]; activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.value.children];
} }
return [mergedActiveValueCells, activeIndex, activeOptions]; [validActiveValueCells.value, lastActiveIndex.value, lastActiveOptions.value] = [
}, [activeValueCells, fieldNames, options]); mergedActiveValueCells,
activeIndex,
activeOptions,
];
});
// Update active value cells and scroll to target element // Update active value cells and scroll to target element
const internalSetActiveValueCells = (next: React.Key[]) => { const internalSetActiveValueCells = (next: Key[]) => {
setActiveValueCells(next); setActiveValueCells(next);
const ele = containerRef.current?.querySelector(`li[data-path-key="${toPathKey(next)}"]`); const ele = containerRef.value?.querySelector(`li[data-path-key="${toPathKey(next)}"]`);
ele?.scrollIntoView?.({ block: 'nearest' }); ele?.scrollIntoView?.({ block: 'nearest' });
}; };
// Same options offset // Same options offset
const offsetActiveOption = (offset: number) => { const offsetActiveOption = (offset: number) => {
const len = lastActiveOptions.length; const len = lastActiveOptions.value.length;
let currentIndex = lastActiveIndex; let currentIndex = lastActiveIndex.value;
if (currentIndex === -1 && offset < 0) { if (currentIndex === -1 && offset < 0) {
currentIndex = len; currentIndex = len;
} }
for (let i = 0; i < len; i += 1) { for (let i = 0; i < len; i += 1) {
currentIndex = (currentIndex + offset + len) % len; currentIndex = (currentIndex + offset + len) % len;
const option = lastActiveOptions[currentIndex]; const option = lastActiveOptions.value[currentIndex];
if (option && !option.disabled) { if (option && !option.disabled) {
const value = option[fieldNames.value]; const value = option[fieldNames.value.value];
const nextActiveCells = validActiveValueCells.slice(0, -1).concat(value); const nextActiveCells = validActiveValueCells.value.slice(0, -1).concat(value);
internalSetActiveValueCells(nextActiveCells); internalSetActiveValueCells(nextActiveCells);
return; return;
} }
@ -85,8 +95,8 @@ export default (
// Different options offset // Different options offset
const prevColumn = () => { const prevColumn = () => {
if (validActiveValueCells.length > 1) { if (validActiveValueCells.value.length > 1) {
const nextActiveCells = validActiveValueCells.slice(0, -1); const nextActiveCells = validActiveValueCells.value.slice(0, -1);
internalSetActiveValueCells(nextActiveCells); internalSetActiveValueCells(nextActiveCells);
} else { } else {
toggleOpen(false); toggleOpen(false);
@ -95,19 +105,19 @@ export default (
const nextColumn = () => { const nextColumn = () => {
const nextOptions: DefaultOptionType[] = const nextOptions: DefaultOptionType[] =
lastActiveOptions[lastActiveIndex]?.[fieldNames.children] || []; lastActiveOptions.value[lastActiveIndex.value]?.[fieldNames.value.children] || [];
const nextOption = nextOptions.find(option => !option.disabled); const nextOption = nextOptions.find(option => !option.disabled);
if (nextOption) { if (nextOption) {
const nextActiveCells = [...validActiveValueCells, nextOption[fieldNames.value]]; const nextActiveCells = [...validActiveValueCells.value, nextOption[fieldNames.value.value]];
internalSetActiveValueCells(nextActiveCells); internalSetActiveValueCells(nextActiveCells);
} }
}; };
React.useImperativeHandle(ref, () => ({ context.expose({
// scrollTo: treeRef.current?.scrollTo, // scrollTo: treeRef.current?.scrollTo,
onKeyDown: event => { onKeydown: event => {
const { which } = event; const { which } = event;
switch (which) { switch (which) {
@ -155,8 +165,11 @@ export default (
// >>> Select // >>> Select
case KeyCode.ENTER: { case KeyCode.ENTER: {
if (validActiveValueCells.length) { if (validActiveValueCells.value.length) {
onKeyBoardSelect(validActiveValueCells, lastActiveOptions[lastActiveIndex]); onKeyBoardSelect(
validActiveValueCells.value,
lastActiveOptions.value[lastActiveIndex.value],
);
} }
break; break;
} }
@ -171,6 +184,6 @@ export default (
} }
} }
}, },
onKeyUp: () => {}, onKeyup: () => {},
})); } as RefOptionListProps);
}; };

View File

@ -2,14 +2,14 @@ import type { CSSProperties, InjectionKey, Ref } from 'vue';
import { inject, provide } from 'vue'; import { inject, provide } from 'vue';
import type { VueNode } from '../_util/type'; import type { VueNode } from '../_util/type';
import type { import type {
CascaderProps, BaseCascaderProps,
InternalFieldNames, InternalFieldNames,
DefaultOptionType, DefaultOptionType,
SingleValueType, SingleValueType,
} from './Cascader'; } from './Cascader';
export interface CascaderContextProps { export interface CascaderContextProps {
options: Ref<CascaderProps['options']>; options: Ref<BaseCascaderProps['options']>;
fieldNames: Ref<InternalFieldNames>; fieldNames: Ref<InternalFieldNames>;
values: Ref<SingleValueType[]>; values: Ref<SingleValueType[]>;
halfValues: Ref<SingleValueType[]>; halfValues: Ref<SingleValueType[]>;
@ -23,7 +23,7 @@ export interface CascaderContextProps {
expandIcon: Ref<VueNode>; expandIcon: Ref<VueNode>;
loadingIcon: Ref<VueNode>; loadingIcon: Ref<VueNode>;
dropdownMenuColumnStyle: Ref<CSSProperties>; dropdownMenuColumnStyle: Ref<CSSProperties>;
slotsContext: Ref<Record<string, Function>>; customSlots: Ref<Record<string, Function>>;
} }
const CascaderContextKey: InjectionKey<CascaderContextProps> = Symbol('CascaderContextKey'); const CascaderContextKey: InjectionKey<CascaderContextProps> = Symbol('CascaderContextKey');

View File

@ -1,37 +1,38 @@
import { toPathOptions } from '../utils/treeUtil'; import { toPathOptions } from '../utils/treeUtil';
import * as React from 'react';
import type { import type {
DefaultOptionType, DefaultOptionType,
SingleValueType, SingleValueType,
CascaderProps, BaseCascaderProps,
InternalFieldNames, InternalFieldNames,
} from '../Cascader'; } from '../Cascader';
import { toPathKey } from '../utils/commonUtil'; import { toPathKey } from '../utils/commonUtil';
import type { Ref } from 'vue';
import { computed } from 'vue';
import { isValidElement } from '../../_util/props-util';
import { cloneElement } from '../../_util/vnode';
export default ( export default (
rawValues: SingleValueType[], rawValues: Ref<SingleValueType[]>,
options: DefaultOptionType[], options: Ref<DefaultOptionType[]>,
fieldNames: InternalFieldNames, fieldNames: Ref<InternalFieldNames>,
multiple: boolean, multiple: Ref<boolean>,
displayRender: CascaderProps['displayRender'], displayRender: Ref<BaseCascaderProps['displayRender']>,
) => { ) => {
return React.useMemo(() => { return computed(() => {
const mergedDisplayRender = const mergedDisplayRender =
displayRender || displayRender.value ||
// Default displayRender // Default displayRender
(labels => { (({ labels }) => {
const mergedLabels = multiple ? labels.slice(-1) : labels; const mergedLabels = multiple.value ? labels.slice(-1) : labels;
const SPLIT = ' / '; const SPLIT = ' / ';
if (mergedLabels.every(label => ['string', 'number'].includes(typeof label))) { if (mergedLabels.every(label => ['string', 'number'].includes(typeof label))) {
return mergedLabels.join(SPLIT); return mergedLabels.join(SPLIT);
} }
// If exist non-string value, use ReactNode instead // If exist non-string value, use VueNode instead
return mergedLabels.reduce((list, label, index) => { return mergedLabels.reduce((list, label, index) => {
const keyedLabel = React.isValidElement(label) const keyedLabel = isValidElement(label) ? cloneElement(label, { key: index }) : label;
? React.cloneElement(label, { key: index })
: label;
if (index === 0) { if (index === 0) {
return [keyedLabel]; return [keyedLabel];
@ -41,13 +42,13 @@ export default (
}, []); }, []);
}); });
return rawValues.map(valueCells => { return rawValues.value.map(valueCells => {
const valueOptions = toPathOptions(valueCells, options, fieldNames); const valueOptions = toPathOptions(valueCells, options.value, fieldNames.value);
const label = mergedDisplayRender( const label = mergedDisplayRender({
valueOptions.map(({ option, value }) => option?.[fieldNames.label] ?? value), labels: valueOptions.map(({ option, value }) => option?.[fieldNames.value.label] ?? value),
valueOptions.map(({ option }) => option), selectedOptions: valueOptions.map(({ option }) => option),
); });
return { return {
label, label,
@ -55,5 +56,5 @@ export default (
valueCells, valueCells,
}; };
}); });
}, [rawValues, options, fieldNames, displayRender, multiple]); });
}; };

View File

@ -10,13 +10,11 @@ export interface OptionsInfo {
pathKeyEntities: Record<string, DataEntity>; pathKeyEntities: Record<string, DataEntity>;
} }
export type GetEntities = () => OptionsInfo['pathKeyEntities'];
/** Lazy parse options data into conduct-able info to avoid perf issue in single mode */ /** Lazy parse options data into conduct-able info to avoid perf issue in single mode */
export default (options: Ref<DefaultOptionType[]>, fieldNames: Ref<InternalFieldNames>) => { export default (options: Ref<DefaultOptionType[]>, fieldNames: Ref<InternalFieldNames>) => {
const entities = computed(() => { const entities = computed(() => {
return ( return (
convertDataToEntities(options as any, { convertDataToEntities(options.value as any, {
fieldNames: fieldNames.value, fieldNames: fieldNames.value,
initWrapper: wrapper => ({ initWrapper: wrapper => ({
...wrapper, ...wrapper,

View File

@ -1,13 +1,17 @@
import type { CascaderProps, ShowSearchType } from '../Cascader'; import type { BaseCascaderProps, ShowSearchType } from '../Cascader';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed } from 'vue'; import { ref, watchEffect } from 'vue';
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
// Convert `showSearch` to unique config // Convert `showSearch` to unique config
export default function useSearchConfig(showSearch?: Ref<CascaderProps['showSearch']>) { export default function useSearchConfig(showSearch?: Ref<BaseCascaderProps['showSearch']>) {
return computed(() => { const mergedShowSearch = ref(false);
const mergedSearchConfig = ref<ShowSearchType>({});
watchEffect(() => {
if (!showSearch.value) { if (!showSearch.value) {
return [false, {}]; mergedShowSearch.value = false;
mergedSearchConfig.value = {};
return;
} }
let searchConfig: ShowSearchType = { let searchConfig: ShowSearchType = {
@ -29,7 +33,9 @@ export default function useSearchConfig(showSearch?: Ref<CascaderProps['showSear
warning(false, "'limit' of showSearch should be positive number or false."); warning(false, "'limit' of showSearch should be positive number or false.");
} }
} }
mergedShowSearch.value = true;
return [true, searchConfig]; mergedSearchConfig.value = searchConfig;
return;
}); });
return { showSearch: mergedShowSearch, searchConfig: mergedSearchConfig };
} }

View File

@ -0,0 +1,12 @@
// rc-cascader@3.0.0-alpha.6
import Cascader, { internalCascaderProps as cascaderProps } from './Cascader';
export type {
CascaderProps,
FieldNames,
ShowSearchType,
DefaultOptionType,
BaseOptionType,
} from './Cascader';
export { cascaderProps };
export default Cascader;

View File

@ -1,10 +1,12 @@
import type { Key } from '../../_util/type'; import type { Key } from '../../_util/type';
import type { SingleValueType, DefaultOptionType, InternalFieldNames } from '../Cascader'; import type { SingleValueType, DefaultOptionType, InternalFieldNames } from '../Cascader';
import type { GetEntities } from '../hooks/useEntities'; import type { OptionsInfo } from '../hooks/useEntities';
export function formatStrategyValues(pathKeys: Key[], getKeyPathEntities: GetEntities) { export function formatStrategyValues(
pathKeys: Key[],
keyPathEntities: OptionsInfo['pathKeyEntities'],
) {
const valueSet = new Set(pathKeys); const valueSet = new Set(pathKeys);
const keyPathEntities = getKeyPathEntities();
return pathKeys.filter(key => { return pathKeys.filter(key => {
const entity = keyPathEntities[key]; const entity = keyPathEntities[key];

View File

@ -30,7 +30,7 @@ import {
} from 'vue'; } from 'vue';
import type { CSSProperties, ExtractPropTypes, PropType, VNode } from 'vue'; import type { CSSProperties, ExtractPropTypes, PropType, VNode } from 'vue';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { initDefaultProps } from '../_util/props-util'; import { initDefaultProps, isValidElement } from '../_util/props-util';
import isMobile from '../vc-util/isMobile'; import isMobile from '../vc-util/isMobile';
import KeyCode from '../_util/KeyCode'; import KeyCode from '../_util/KeyCode';
import { toReactive } from '../_util/toReactive'; import { toReactive } from '../_util/toReactive';
@ -38,6 +38,7 @@ import classNames from '../_util/classNames';
import createRef from '../_util/createRef'; import createRef from '../_util/createRef';
import type { BaseOptionType } from './Select'; import type { BaseOptionType } from './Select';
import useInjectLegacySelectContext from '../vc-tree-select/LegacyContext'; import useInjectLegacySelectContext from '../vc-tree-select/LegacyContext';
import { cloneElement } from '../_util/vnode';
const DEFAULT_OMIT_PROPS = [ const DEFAULT_OMIT_PROPS = [
'value', 'value',
@ -543,6 +544,8 @@ export default defineComponent({
focus: onContainerFocus, focus: onContainerFocus,
blur: onContainerBlur, blur: onContainerBlur,
}); });
// Give focus back of Select
const activeTimeoutIds: any[] = []; const activeTimeoutIds: any[] = [];
onMounted(() => { onMounted(() => {
@ -591,7 +594,7 @@ export default defineComponent({
triggerOpen, triggerOpen,
() => { () => {
if (triggerOpen.value) { if (triggerOpen.value) {
const newWidth = Math.ceil(containerRef.value.offsetWidth); const newWidth = Math.ceil(containerRef.value?.offsetWidth);
if (containerWidth.value !== newWidth && !Number.isNaN(newWidth)) { if (containerWidth.value !== newWidth && !Number.isNaN(newWidth)) {
containerWidth.value = newWidth; containerWidth.value = newWidth;
} }
@ -740,7 +743,7 @@ export default defineComponent({
onInternalSearch('', false, false); onInternalSearch('', false, false);
}; };
if (!disabled && allowClear && (displayValues.length || mergedSearchValue)) { if (!disabled && allowClear && (displayValues.length || mergedSearchValue.value)) {
clearNode = ( clearNode = (
<TransBtn <TransBtn
class={`${prefixCls}-clear`} class={`${prefixCls}-clear`}
@ -800,7 +803,15 @@ export default defineComponent({
v-slots={{ v-slots={{
default: () => { default: () => {
return customizeRawInputElement ? ( return customizeRawInputElement ? (
customizeRawInputElement isValidElement(customizeRawInputElement) &&
cloneElement(
customizeRawInputElement,
{
ref: selectorDomRef,
},
false,
true,
)
) : ( ) : (
<Selector <Selector
{...props} {...props}

View File

@ -161,8 +161,8 @@ const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
return ( return (
<Trigger <Trigger
{...props} {...props}
showAction={[]} showAction={onPopupVisibleChange ? ['click'] : []}
hideAction={[]} hideAction={onPopupVisibleChange ? ['click'] : []}
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')} popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
builtinPlacements={builtInPlacements.value} builtinPlacements={builtInPlacements.value}
prefixCls={dropdownPrefixCls} prefixCls={dropdownPrefixCls}

View File

@ -174,11 +174,11 @@ const Input = defineComponent({
this.VCSelectContainerEvent?.focus(args[0]); this.VCSelectContainerEvent?.focus(args[0]);
}, },
onBlur: (...args: any[]) => { onBlur: (...args: any[]) => {
this.blurTimeout = setTimeout(() => { // this.blurTimeout = setTimeout(() => {
onOriginBlur && onOriginBlur(args[0]); onOriginBlur && onOriginBlur(args[0]);
onBlur && onBlur(args[0]); onBlur && onBlur(args[0]);
this.VCSelectContainerEvent?.blur(args[0]); this.VCSelectContainerEvent?.blur(args[0]);
}, 200); // }, 200);
}, },
}, },
inputNode.type === 'textarea' ? {} : { type: 'search' }, inputNode.type === 'textarea' ? {} : { type: 'search' },