refactor: cascader
parent
d46762c1d6
commit
e08c6da9b5
|
@ -1,7 +1,7 @@
|
|||
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 */
|
||||
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
|
|
|
@ -20,12 +20,8 @@ Cascade selection box for selecting province/city/district.
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
|
|
@ -16,16 +16,17 @@ Allow only select parent options.
|
|||
|
||||
</docs>
|
||||
<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>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
|
|
@ -16,7 +16,12 @@ For instance, add an external link after the selected value.
|
|||
|
||||
</docs>
|
||||
<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 }">
|
||||
<span v-for="(label, index) in labels" :key="selectedOptions[index].value">
|
||||
<span v-if="index === labels.length - 1">
|
||||
|
@ -33,14 +38,8 @@ For instance, add an external link after the selected value.
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
code?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
|
|
@ -18,21 +18,20 @@ Separate trigger button and result.
|
|||
<template>
|
||||
<span>
|
||||
{{ text }}
|
||||
<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-cascader>
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
code?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
|
|
@ -16,19 +16,12 @@ Disable option by specifying the `disabled` property in `options`.
|
|||
|
||||
</docs>
|
||||
<template>
|
||||
<a-cascader v-model:value="value" :options="options" />
|
||||
<a-cascader v-model:value="value" placeholder="Please select" :options="options" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
children?: Option[];
|
||||
code?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
|
|
@ -25,14 +25,8 @@ Custom Field Names
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
code: string;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
items?: Option[];
|
||||
[key: string]: any;
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
code: 'zhejiang',
|
||||
name: 'Zhejiang',
|
||||
|
|
|
@ -19,19 +19,14 @@ Hover to expand sub menu, click to select option.
|
|||
<a-cascader
|
||||
v-model:value="value"
|
||||
:options="options"
|
||||
:display-render="displayRender"
|
||||
expand-trigger="hover"
|
||||
placeholder="Please select"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
@ -67,14 +62,9 @@ const options: Option[] = [
|
|||
];
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const displayRender = ({ labels }: { labels: string[] }) => {
|
||||
return labels[labels.length - 1];
|
||||
};
|
||||
|
||||
return {
|
||||
value: ref<string[]>([]),
|
||||
options,
|
||||
displayRender,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -28,16 +28,11 @@ Load options lazily with `loadData`.
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
loading?: boolean;
|
||||
isLeaf?: boolean;
|
||||
children?: Option[];
|
||||
}
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const options = ref<Option[]>([
|
||||
const options = ref<CascaderProps['options']>([
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
|
|
@ -27,13 +27,8 @@ Search and select options directly.
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
children?: Option[];
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
|
|
@ -16,24 +16,20 @@ Cascade selection box of different sizes.
|
|||
|
||||
</docs>
|
||||
<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 />
|
||||
<a-cascader v-model:value="value" :options="options" />
|
||||
<a-cascader v-model:value="value" placeholder="Please select" :options="options" />
|
||||
<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 />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
|
|
@ -35,12 +35,8 @@ Custom suffix icon
|
|||
<script lang="ts">
|
||||
import { SmileOutlined } from '@ant-design/icons-vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
}
|
||||
const options: Option[] = [
|
||||
import type { CascaderProps } from 'ant-design-vue';
|
||||
const options: CascaderProps['options'] = [
|
||||
{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
|
|
|
@ -1,646 +1,286 @@
|
|||
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 type {
|
||||
ShowSearchType,
|
||||
FieldNames,
|
||||
BaseOptionType,
|
||||
DefaultOptionType,
|
||||
} from '../vc-cascader2';
|
||||
import VcCascader, { cascaderProps as vcCascaderProps } from '../vc-cascader2';
|
||||
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 LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
|
||||
import getIcons from '../select/utils/iconUtil';
|
||||
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 { withInstall } from '../_util/type';
|
||||
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 { useInjectFormItemContext } from '../form';
|
||||
import type { ValueType } from '../vc-cascader2/Cascader';
|
||||
|
||||
export interface CascaderOptionType {
|
||||
value?: string | number;
|
||||
label?: VueNode;
|
||||
disabled?: boolean;
|
||||
// Align the design since we use `rc-select` in root. This help:
|
||||
// - List search content will show all content
|
||||
// - Hover opacity style
|
||||
// - 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;
|
||||
loading?: boolean;
|
||||
children?: CascaderOptionType[];
|
||||
[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 {
|
||||
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,
|
||||
options: Array as PropType<DataNodeType[]>,
|
||||
'onUpdate:value': Function as PropType<(value: ValueType) => void>,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
export type CascaderProps = Partial<ExtractPropTypes<ReturnType<typeof cascaderProps>>>;
|
||||
|
||||
return a.findIndex(callback) - b.findIndex(callback);
|
||||
export interface CascaderRef {
|
||||
focus: () => void;
|
||||
blur: () => void;
|
||||
}
|
||||
|
||||
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() {
|
||||
props: initDefaultProps(cascaderProps(), {
|
||||
bordered: true,
|
||||
choiceTransitionName: '',
|
||||
allowClear: true,
|
||||
}),
|
||||
setup(props, { attrs, expose, slots, emit }) {
|
||||
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,
|
||||
prefixCls: cascaderPrefixCls,
|
||||
rootPrefixCls,
|
||||
getPrefixCls,
|
||||
direction,
|
||||
getPopupContainer,
|
||||
renderEmpty,
|
||||
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',
|
||||
} = useConfigInject('cascader', props);
|
||||
const prefixCls = computed(() => getPrefixCls('select', props.prefixCls));
|
||||
const isRtl = computed(() => direction.value === 'rtl');
|
||||
// =================== Warning =====================
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
watchEffect(() => {
|
||||
devWarning(
|
||||
props.popupClassName === undefined,
|
||||
'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.',
|
||||
);
|
||||
});
|
||||
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,
|
||||
},
|
||||
];
|
||||
// ==================== Search =====================
|
||||
const mergedShowSearch = computed(() => {
|
||||
if (!props.showSearch) {
|
||||
return props.showSearch;
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
// showSearch时,focus、blur在input上触发,反之在ref='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,
|
||||
let searchConfig: ShowSearchType = {
|
||||
render: defaultSearchRender,
|
||||
};
|
||||
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>
|
||||
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 expandIcon = <RightOutlined />;
|
||||
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}-menu-item-loading-icon`}>
|
||||
<span class={`${prefixCls.value}-menu-item-loading-icon`}>
|
||||
<RedoOutlined spin />
|
||||
</span>
|
||||
);
|
||||
const getPopupContainer = props.getPopupContainer || getContextPopupContainer;
|
||||
const cascaderProps = {
|
||||
|
||||
// ===================== Icons =====================
|
||||
const { suffixIcon, removeIcon, clearIcon } = getIcons(
|
||||
{
|
||||
...props,
|
||||
getPopupContainer,
|
||||
options,
|
||||
prefixCls,
|
||||
value,
|
||||
popupVisible: sPopupVisible,
|
||||
dropdownMenuColumnStyle,
|
||||
expandIcon,
|
||||
loadingIcon,
|
||||
...onEvents,
|
||||
onPopupVisibleChange: this.handlePopupVisibleChange,
|
||||
onChange: this.handleChange,
|
||||
transitionName: getTransitionName(rootPrefixCls, 'slide-up', props.transitionName),
|
||||
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>;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
// showSearch时,focus、blur在input上触发,反之在ref='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);
|
|
@ -1,169 +1,38 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
@import '../../input/style/mixin';
|
||||
@import '../../checkbox/style/mixin';
|
||||
|
||||
@cascader-prefix-cls: ~'@{ant-prefix}-cascader';
|
||||
|
||||
.antCheckboxFn(@checkbox-prefix-cls: ~'@{cascader-prefix-cls}-checkbox');
|
||||
|
||||
.@{cascader-prefix-cls} {
|
||||
.reset-component();
|
||||
width: 184px;
|
||||
|
||||
&-input.@{ant-prefix}-input {
|
||||
// Keep it static for https://github.com/ant-design/ant-design/issues/16738
|
||||
position: static;
|
||||
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;
|
||||
&-checkbox {
|
||||
top: 0;
|
||||
margin-right: @padding-xs;
|
||||
}
|
||||
|
||||
&-menus {
|
||||
position: absolute;
|
||||
z-index: @zindex-dropdown;
|
||||
font-size: @cascader-dropdown-font-size;
|
||||
white-space: nowrap;
|
||||
background: @cascader-menu-bg;
|
||||
border-radius: @border-radius-base;
|
||||
box-shadow: @box-shadow-base;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-start;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
&.@{cascader-prefix-cls}-menu-empty {
|
||||
.@{cascader-prefix-cls}-menu {
|
||||
width: 100%;
|
||||
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 {
|
||||
display: inline-block;
|
||||
min-width: 111px;
|
||||
height: 180px;
|
||||
margin: 0;
|
||||
margin: -@dropdown-edge-child-vertical-padding 0;
|
||||
padding: @cascader-dropdown-edge-child-vertical-padding 0;
|
||||
overflow: auto;
|
||||
vertical-align: top;
|
||||
|
@ -171,34 +40,37 @@
|
|||
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
|
||||
|
||||
&:first-child {
|
||||
border-radius: @border-radius-base 0 0 @border-radius-base;
|
||||
}
|
||||
&:last-child {
|
||||
margin-right: -1px;
|
||||
border-right-color: transparent;
|
||||
border-radius: 0 @border-radius-base @border-radius-base 0;
|
||||
}
|
||||
&:only-child {
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
}
|
||||
&-menu-item {
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
padding: @cascader-dropdown-vertical-padding @control-padding-horizontal;
|
||||
overflow: hidden;
|
||||
line-height: @cascader-dropdown-line-height;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: @item-hover-bg;
|
||||
}
|
||||
|
||||
&-disabled {
|
||||
color: @disabled-color;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.@{cascader-prefix-cls}-menu-empty & {
|
||||
color: @disabled-color;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&-active:not(&-disabled) {
|
||||
&,
|
||||
&:hover {
|
||||
|
@ -206,25 +78,27 @@
|
|||
background-color: @cascader-item-selected-bg;
|
||||
}
|
||||
}
|
||||
&-expand {
|
||||
position: relative;
|
||||
padding-right: 24px;
|
||||
|
||||
&-content {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
&-expand &-expand-icon,
|
||||
&-loading-icon {
|
||||
.iconfont-size-under-12px(10px);
|
||||
|
||||
position: absolute;
|
||||
right: @control-padding-horizontal;
|
||||
margin-left: @padding-xss;
|
||||
color: @text-color-secondary;
|
||||
font-size: 10px;
|
||||
|
||||
.@{cascader-prefix-cls}-menu-item-disabled& {
|
||||
color: @disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
& &-keyword {
|
||||
&-keyword {
|
||||
color: @highlight-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import './rtl';
|
||||
|
|
|
@ -3,4 +3,4 @@ import './index.less';
|
|||
|
||||
// style dependencies
|
||||
import '../../empty/style';
|
||||
import '../../input/style';
|
||||
import '../../select/style';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean;
|
||||
render?: (arg?: {
|
||||
|
@ -30,100 +54,110 @@ export interface BaseOptionType {
|
|||
[name: string]: any;
|
||||
}
|
||||
export interface DefaultOptionType extends BaseOptionType {
|
||||
label: React.ReactNode;
|
||||
label?: any;
|
||||
value?: string | number | null;
|
||||
children?: DefaultOptionType[];
|
||||
}
|
||||
|
||||
interface BaseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>
|
||||
extends Omit<
|
||||
BaseSelectPropsWithoutPrivate,
|
||||
'tokenSeparators' | 'labelInValue' | 'mode' | 'showSearch'
|
||||
> {
|
||||
function baseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
|
||||
return {
|
||||
...omit(baseSelectPropsWithoutPrivate(), ['tokenSeparators', 'mode', 'showSearch']),
|
||||
// MISC
|
||||
id?: string;
|
||||
prefixCls?: string;
|
||||
fieldNames?: FieldNames;
|
||||
children?: React.ReactElement;
|
||||
id: String,
|
||||
prefixCls: String,
|
||||
fieldNames: Object as PropType<FieldNames>,
|
||||
children: Array as PropType<VueNode[]>,
|
||||
|
||||
// Value
|
||||
value?: ValueType;
|
||||
defaultValue?: ValueType;
|
||||
changeOnSelect?: boolean;
|
||||
onChange?: (value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void;
|
||||
displayRender?: (label: string[], selectedOptions?: OptionType[]) => React.ReactNode;
|
||||
checkable?: boolean | React.ReactNode;
|
||||
value: { type: [String, Number, Array] as PropType<ValueType> },
|
||||
defaultValue: { type: [String, Number, Array] as PropType<ValueType> },
|
||||
changeOnSelect: { type: Boolean, default: undefined },
|
||||
onChange: Function as PropType<
|
||||
(value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void
|
||||
>,
|
||||
displayRender: Function as PropType<
|
||||
(opt: { labels: string[]; selectedOptions?: OptionType[] }) => any
|
||||
>,
|
||||
checkable: { type: Boolean, default: undefined },
|
||||
|
||||
// Search
|
||||
showSearch?: boolean | ShowSearchType<OptionType>;
|
||||
searchValue?: string;
|
||||
onSearch?: (value: string) => void;
|
||||
showSearch: { type: [Boolean, Object] as PropType<boolean | ShowSearchType<OptionType>> },
|
||||
searchValue: String,
|
||||
onSearch: Function as PropType<(value: string) => void>,
|
||||
|
||||
// Trigger
|
||||
expandTrigger?: 'hover' | 'click';
|
||||
expandTrigger: String as PropType<'hover' | 'click'>,
|
||||
|
||||
// Options
|
||||
options?: OptionType[];
|
||||
options: Array as PropType<OptionType[]>,
|
||||
/** @private Internal usage. Do not use in your production. */
|
||||
dropdownPrefixCls?: string;
|
||||
loadData?: (selectOptions: OptionType[]) => void;
|
||||
dropdownPrefixCls: String,
|
||||
loadData: Function as PropType<(selectOptions: OptionType[]) => void>,
|
||||
|
||||
// Open
|
||||
/** @deprecated Use `open` instead */
|
||||
popupVisible?: boolean;
|
||||
popupVisible: { type: Boolean, default: undefined },
|
||||
|
||||
/** @deprecated Use `dropdownClassName` instead */
|
||||
popupClassName?: string;
|
||||
dropdownClassName?: string;
|
||||
dropdownMenuColumnStyle?: React.CSSProperties;
|
||||
popupClassName: String,
|
||||
dropdownClassName: String,
|
||||
dropdownMenuColumnStyle: Object as PropType<CSSProperties>,
|
||||
|
||||
/** @deprecated Use `placement` instead */
|
||||
popupPlacement?: Placement;
|
||||
placement?: Placement;
|
||||
popupPlacement: String as PropType<Placement>,
|
||||
placement: String as PropType<Placement>,
|
||||
|
||||
/** @deprecated Use `onDropdownVisibleChange` instead */
|
||||
onPopupVisibleChange?: (open: boolean) => void;
|
||||
onDropdownVisibleChange?: (open: boolean) => void;
|
||||
onPopupVisibleChange: Function as PropType<(open: boolean) => void>,
|
||||
onDropdownVisibleChange: Function as PropType<(open: boolean) => void>,
|
||||
|
||||
// Icon
|
||||
expandIcon?: React.ReactNode;
|
||||
loadingIcon?: React.ReactNode;
|
||||
expandIcon: PropTypes.any,
|
||||
loadingIcon: PropTypes.any,
|
||||
};
|
||||
}
|
||||
|
||||
export type BaseCascaderProps = Partial<ExtractPropTypes<ReturnType<typeof baseCascaderProps>>>;
|
||||
|
||||
type OnSingleChange<OptionType> = (value: SingleValueType, selectOptions: OptionType[]) => void;
|
||||
type OnMultipleChange<OptionType> = (
|
||||
value: SingleValueType[],
|
||||
selectOptions: OptionType[][],
|
||||
) => void;
|
||||
|
||||
export interface SingleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>
|
||||
extends BaseCascaderProps<OptionType> {
|
||||
checkable?: false;
|
||||
|
||||
onChange?: OnSingleChange<OptionType>;
|
||||
}
|
||||
|
||||
export interface MultipleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>
|
||||
extends BaseCascaderProps<OptionType> {
|
||||
checkable: true | React.ReactNode;
|
||||
|
||||
onChange?: OnMultipleChange<OptionType>;
|
||||
}
|
||||
|
||||
export type CascaderProps<OptionType extends BaseOptionType = DefaultOptionType> =
|
||||
| SingleCascaderProps<OptionType>
|
||||
| MultipleCascaderProps<OptionType>;
|
||||
|
||||
type InternalCascaderProps<OptionType extends BaseOptionType = DefaultOptionType> = Omit<
|
||||
SingleCascaderProps<OptionType> | MultipleCascaderProps<OptionType>,
|
||||
'onChange'
|
||||
> & {
|
||||
onChange?: (
|
||||
value: SingleValueType | SingleValueType[],
|
||||
selectOptions: OptionType[] | OptionType[][],
|
||||
) => void;
|
||||
export function singleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
|
||||
return {
|
||||
...baseCascaderProps(),
|
||||
checkable: Boolean as PropType<false>,
|
||||
onChange: Function as PropType<OnSingleChange<OptionType>>,
|
||||
};
|
||||
}
|
||||
|
||||
export type SingleCascaderProps = Partial<ExtractPropTypes<ReturnType<typeof singleCascaderProps>>>;
|
||||
|
||||
export function multipleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
|
||||
return {
|
||||
...baseCascaderProps(),
|
||||
checkable: Boolean as PropType<true>,
|
||||
onChange: Function as PropType<OnMultipleChange<OptionType>>,
|
||||
};
|
||||
}
|
||||
|
||||
export type MultipleCascaderProps = Partial<
|
||||
ExtractPropTypes<ReturnType<typeof singleCascaderProps>>
|
||||
>;
|
||||
|
||||
export function internalCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
|
||||
return {
|
||||
...baseCascaderProps(),
|
||||
onChange: Function as PropType<
|
||||
(value: ValueType, selectOptions: OptionType[] | OptionType[][]) => void
|
||||
>,
|
||||
customSlots: Object as PropType<Record<string, Function>>,
|
||||
};
|
||||
}
|
||||
|
||||
export type CascaderProps = Partial<ExtractPropTypes<ReturnType<typeof internalCascaderProps>>>;
|
||||
export type CascaderRef = Omit<BaseSelectRef, 'scrollTo'>;
|
||||
|
||||
function isMultipleValue(value: ValueType): value is SingleValueType[] {
|
||||
|
@ -142,196 +176,157 @@ function toRawValues(value: ValueType): SingleValueType[] {
|
|||
return value.length === 0 ? [] : [value];
|
||||
}
|
||||
|
||||
const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, ref) => {
|
||||
const {
|
||||
// MISC
|
||||
id,
|
||||
prefixCls = 'rc-cascader',
|
||||
fieldNames,
|
||||
|
||||
// Value
|
||||
defaultValue,
|
||||
value,
|
||||
changeOnSelect,
|
||||
onChange,
|
||||
displayRender,
|
||||
checkable,
|
||||
|
||||
// Search
|
||||
searchValue,
|
||||
onSearch,
|
||||
showSearch,
|
||||
|
||||
// Trigger
|
||||
expandTrigger,
|
||||
|
||||
// Options
|
||||
options,
|
||||
dropdownPrefixCls,
|
||||
loadData,
|
||||
|
||||
// Open
|
||||
popupVisible,
|
||||
open,
|
||||
|
||||
popupClassName,
|
||||
dropdownClassName,
|
||||
dropdownMenuColumnStyle,
|
||||
|
||||
popupPlacement,
|
||||
placement,
|
||||
|
||||
onDropdownVisibleChange,
|
||||
onPopupVisibleChange,
|
||||
|
||||
// Icon
|
||||
expandIcon = '>',
|
||||
loadingIcon,
|
||||
|
||||
// Children
|
||||
children,
|
||||
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const mergedId = useId(id);
|
||||
const multiple = !!checkable;
|
||||
export default defineComponent({
|
||||
name: 'Cascader',
|
||||
inheritAttrs: false,
|
||||
props: initDefaultProps(internalCascaderProps(), {}),
|
||||
setup(props, { attrs, expose, slots }) {
|
||||
const mergedId = useId(toRef(props, 'id'));
|
||||
const multiple = computed(() => !!props.checkable);
|
||||
|
||||
// =========================== Values ===========================
|
||||
const [rawValues, setRawValues] = useMergedState<ValueType, SingleValueType[]>(defaultValue, {
|
||||
value,
|
||||
const [rawValues, setRawValues] = useMergedState<ValueType, Ref<SingleValueType[]>>(
|
||||
props.defaultValue,
|
||||
{
|
||||
value: computed(() => props.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 */
|
||||
},
|
||||
);
|
||||
|
||||
// ========================= FieldNames =========================
|
||||
const mergedFieldNames = computed(() => fillFieldNames(props.fieldNames));
|
||||
|
||||
// =========================== Option ===========================
|
||||
const mergedOptions = React.useMemo(() => options || [], [options]);
|
||||
const mergedOptions = computed(() => props.options || []);
|
||||
|
||||
// Only used in multiple mode, this fn will not call in single mode
|
||||
const getPathKeyEntities = useEntities(mergedOptions, mergedFieldNames);
|
||||
const pathKeyEntities = useEntities(mergedOptions, mergedFieldNames);
|
||||
|
||||
/** Convert path key back to value format */
|
||||
const getValueByKeyPath = React.useCallback(
|
||||
(pathKeys: React.Key[]): SingleValueType[] => {
|
||||
const ketPathEntities = getPathKeyEntities();
|
||||
const getValueByKeyPath = (pathKeys: Key[]): SingleValueType[] => {
|
||||
const ketPathEntities = pathKeyEntities.value;
|
||||
|
||||
return pathKeys.map(pathKey => {
|
||||
const { nodes } = ketPathEntities[pathKey];
|
||||
|
||||
return nodes.map(node => node[mergedFieldNames.value]);
|
||||
return nodes.map(node => node[mergedFieldNames.value.value]);
|
||||
});
|
||||
},
|
||||
[getPathKeyEntities, mergedFieldNames],
|
||||
);
|
||||
};
|
||||
|
||||
// =========================== Search ===========================
|
||||
const [mergedSearchValue, setSearchValue] = useMergedState('', {
|
||||
value: searchValue,
|
||||
value: computed(() => props.searchValue),
|
||||
postState: search => search || '',
|
||||
});
|
||||
|
||||
const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => {
|
||||
setSearchValue(searchText);
|
||||
|
||||
if (info.source !== 'blur' && onSearch) {
|
||||
onSearch(searchText);
|
||||
if (info.source !== 'blur' && props.onSearch) {
|
||||
props.onSearch(searchText);
|
||||
}
|
||||
};
|
||||
|
||||
const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch);
|
||||
const { showSearch: mergedShowSearch, searchConfig: mergedSearchConfig } = useSearchConfig(
|
||||
toRef(props, 'showSearch'),
|
||||
);
|
||||
|
||||
const searchOptions = useSearchOptions(
|
||||
mergedSearchValue,
|
||||
mergedOptions,
|
||||
mergedFieldNames,
|
||||
dropdownPrefixCls || prefixCls,
|
||||
searchConfig,
|
||||
changeOnSelect,
|
||||
computed(() => props.dropdownPrefixCls || props.prefixCls),
|
||||
mergedSearchConfig,
|
||||
toRef(props, 'changeOnSelect'),
|
||||
);
|
||||
|
||||
// =========================== Values ===========================
|
||||
const getMissingValues = useMissingValues(mergedOptions, mergedFieldNames);
|
||||
const missingValuesInfo = useMissingValues(mergedOptions, mergedFieldNames, rawValues);
|
||||
|
||||
// Fill `rawValues` with checked conduction values
|
||||
const [checkedValues, halfCheckedValues, missingCheckedValues] = React.useMemo(() => {
|
||||
const [existValues, missingValues] = getMissingValues(rawValues);
|
||||
const [checkedValues, halfCheckedValues, missingCheckedValues] = [
|
||||
ref<SingleValueType[]>([]),
|
||||
ref<SingleValueType[]>([]),
|
||||
ref<SingleValueType[]>([]),
|
||||
];
|
||||
watchEffect(() => {
|
||||
const [existValues, missingValues] = missingValuesInfo.value;
|
||||
|
||||
if (!multiple || !rawValues.length) {
|
||||
return [existValues, [], missingValues];
|
||||
if (!multiple.value || !rawValues.value.length) {
|
||||
[checkedValues.value, halfCheckedValues.value, missingCheckedValues.value] = [
|
||||
existValues,
|
||||
[],
|
||||
missingValues,
|
||||
];
|
||||
}
|
||||
|
||||
const keyPathValues = toPathKeys(existValues);
|
||||
const ketPathEntities = getPathKeyEntities();
|
||||
const ketPathEntities = pathKeyEntities.value;
|
||||
|
||||
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);
|
||||
const deDuplicatedValues = computed(() => {
|
||||
const checkedKeys = toPathKeys(checkedValues.value);
|
||||
const deduplicateKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value);
|
||||
|
||||
return [...missingCheckedValues, ...getValueByKeyPath(deduplicateKeys)];
|
||||
}, [checkedValues, getPathKeyEntities, getValueByKeyPath, missingCheckedValues]);
|
||||
return [...missingCheckedValues.value, ...getValueByKeyPath(deduplicateKeys)];
|
||||
});
|
||||
|
||||
const displayValues = useDisplayValues(
|
||||
deDuplicatedValues,
|
||||
mergedOptions,
|
||||
mergedFieldNames,
|
||||
multiple,
|
||||
displayRender,
|
||||
toRef(props, 'displayRender'),
|
||||
);
|
||||
|
||||
// =========================== Change ===========================
|
||||
const triggerChange = useRefFunc((nextValues: ValueType) => {
|
||||
const triggerChange = (nextValues: ValueType) => {
|
||||
setRawValues(nextValues);
|
||||
|
||||
// Save perf if no need trigger event
|
||||
if (onChange) {
|
||||
if (props.onChange) {
|
||||
const nextRawValues = toRawValues(nextValues);
|
||||
|
||||
const valueOptions = nextRawValues.map(valueCells =>
|
||||
toPathOptions(valueCells, mergedOptions, mergedFieldNames).map(valueOpt => valueOpt.option),
|
||||
toPathOptions(valueCells, mergedOptions.value, mergedFieldNames.value).map(
|
||||
valueOpt => valueOpt.option,
|
||||
),
|
||||
);
|
||||
|
||||
const triggerValues = multiple ? nextRawValues : nextRawValues[0];
|
||||
const triggerOptions = multiple ? valueOptions : valueOptions[0];
|
||||
const triggerValues = multiple.value ? nextRawValues : nextRawValues[0];
|
||||
const triggerOptions = multiple.value ? valueOptions : valueOptions[0];
|
||||
|
||||
onChange(triggerValues, triggerOptions);
|
||||
props.onChange(triggerValues, triggerOptions);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// =========================== Select ===========================
|
||||
const onInternalSelect = useRefFunc((valuePath: SingleValueType) => {
|
||||
if (!multiple) {
|
||||
const onInternalSelect = (valuePath: SingleValueType) => {
|
||||
if (!multiple.value) {
|
||||
triggerChange(valuePath);
|
||||
} else {
|
||||
// Prepare conduct required info
|
||||
const pathKey = toPathKey(valuePath);
|
||||
const checkedPathKeys = toPathKeys(checkedValues);
|
||||
const halfCheckedPathKeys = toPathKeys(halfCheckedValues);
|
||||
const checkedPathKeys = toPathKeys(checkedValues.value);
|
||||
const halfCheckedPathKeys = toPathKeys(halfCheckedValues.value);
|
||||
|
||||
const existInChecked = checkedPathKeys.includes(pathKey);
|
||||
const existInMissing = missingCheckedValues.some(
|
||||
const existInMissing = missingCheckedValues.value.some(
|
||||
valueCells => toPathKey(valueCells) === pathKey,
|
||||
);
|
||||
|
||||
// Do update
|
||||
let nextCheckedValues = checkedValues;
|
||||
let nextMissingValues = missingCheckedValues;
|
||||
let nextCheckedValues = checkedValues.value;
|
||||
let nextMissingValues = missingCheckedValues.value;
|
||||
|
||||
if (existInMissing && !existInChecked) {
|
||||
// Missing value only do filter
|
||||
nextMissingValues = missingCheckedValues.filter(
|
||||
nextMissingValues = missingCheckedValues.value.filter(
|
||||
valueCells => toPathKey(valueCells) !== pathKey,
|
||||
);
|
||||
} else {
|
||||
|
@ -340,28 +335,26 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
|
|||
? checkedPathKeys.filter(key => key !== pathKey)
|
||||
: [...checkedPathKeys, pathKey];
|
||||
|
||||
const pathKeyEntities = getPathKeyEntities();
|
||||
|
||||
// Conduction by selected or not
|
||||
let checkedKeys: React.Key[];
|
||||
let checkedKeys: Key[];
|
||||
if (existInChecked) {
|
||||
({ checkedKeys } = conductCheck(
|
||||
nextRawCheckedKeys,
|
||||
{ checked: false, halfCheckedKeys: halfCheckedPathKeys },
|
||||
pathKeyEntities,
|
||||
pathKeyEntities.value,
|
||||
));
|
||||
} else {
|
||||
({ checkedKeys } = conductCheck(nextRawCheckedKeys, true, pathKeyEntities));
|
||||
({ checkedKeys } = conductCheck(nextRawCheckedKeys, true, pathKeyEntities.value));
|
||||
}
|
||||
|
||||
// Roll up to parent level keys
|
||||
const deDuplicatedKeys = formatStrategyValues(checkedKeys, getPathKeyEntities);
|
||||
const deDuplicatedKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value);
|
||||
nextCheckedValues = getValueByKeyPath(deDuplicatedKeys);
|
||||
}
|
||||
|
||||
triggerChange([...nextMissingValues, ...nextCheckedValues]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Display Value change logic
|
||||
const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (_, info) => {
|
||||
|
@ -377,35 +370,48 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
|
|||
|
||||
// ============================ Open ============================
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
watchEffect(() => {
|
||||
warning(
|
||||
!onPopupVisibleChange,
|
||||
'`onPopupVisibleChange` is deprecated. Please use `onDropdownVisibleChange` instead.',
|
||||
!props.onPopupVisibleChange,
|
||||
'`popupVisibleChange` is deprecated. Please use `dropdownVisibleChange` instead.',
|
||||
);
|
||||
warning(popupVisible === undefined, '`popupVisible` is deprecated. Please use `open` instead.');
|
||||
warning(
|
||||
popupClassName === undefined,
|
||||
props.popupVisible === undefined,
|
||||
'`popupVisible` is deprecated. Please use `open` instead.',
|
||||
);
|
||||
warning(
|
||||
props.popupClassName === undefined,
|
||||
'`popupClassName` is deprecated. Please use `dropdownClassName` instead.',
|
||||
);
|
||||
warning(
|
||||
popupPlacement === undefined,
|
||||
props.popupPlacement === undefined,
|
||||
'`popupPlacement` is deprecated. Please use `placement` instead.',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const mergedOpen = open !== undefined ? open : popupVisible;
|
||||
const mergedOpen = computed(() => (props.open !== undefined ? props.open : props.popupVisible));
|
||||
|
||||
const mergedDropdownClassName = dropdownClassName || popupClassName;
|
||||
const mergedDropdownClassName = computed(() => props.dropdownClassName || props.popupClassName);
|
||||
|
||||
const mergedPlacement = placement || popupPlacement;
|
||||
const mergedPlacement = computed(() => props.placement || props.popupPlacement);
|
||||
|
||||
const onInternalDropdownVisibleChange = (nextVisible: boolean) => {
|
||||
onDropdownVisibleChange?.(nextVisible);
|
||||
onPopupVisibleChange?.(nextVisible);
|
||||
props.onDropdownVisibleChange?.(nextVisible);
|
||||
props.onPopupVisibleChange?.(nextVisible);
|
||||
};
|
||||
|
||||
// ========================== Context ===========================
|
||||
const cascaderContext = React.useMemo(
|
||||
() => ({
|
||||
const {
|
||||
changeOnSelect,
|
||||
checkable,
|
||||
dropdownPrefixCls,
|
||||
loadData,
|
||||
expandTrigger,
|
||||
expandIcon,
|
||||
loadingIcon,
|
||||
dropdownMenuColumnStyle,
|
||||
customSlots,
|
||||
} = toRefs(props);
|
||||
useProvideCascader({
|
||||
options: mergedOptions,
|
||||
fieldNames: mergedFieldNames,
|
||||
values: checkedValues,
|
||||
|
@ -420,81 +426,116 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
|
|||
expandIcon,
|
||||
loadingIcon,
|
||||
dropdownMenuColumnStyle,
|
||||
}),
|
||||
[
|
||||
mergedOptions,
|
||||
mergedFieldNames,
|
||||
checkedValues,
|
||||
halfCheckedValues,
|
||||
changeOnSelect,
|
||||
onInternalSelect,
|
||||
checkable,
|
||||
searchOptions,
|
||||
dropdownPrefixCls,
|
||||
loadData,
|
||||
expandTrigger,
|
||||
expandIcon,
|
||||
loadingIcon,
|
||||
dropdownMenuColumnStyle,
|
||||
],
|
||||
);
|
||||
customSlots,
|
||||
});
|
||||
const selectRef = ref<BaseSelectRef>();
|
||||
|
||||
// ==============================================================
|
||||
// == Render ==
|
||||
// ==============================================================
|
||||
const emptyOptions = !(mergedSearchValue ? searchOptions : mergedOptions).length;
|
||||
expose({
|
||||
focus() {
|
||||
selectRef.value?.focus();
|
||||
},
|
||||
blur() {
|
||||
selectRef.value?.blur();
|
||||
},
|
||||
scrollTo(arg) {
|
||||
selectRef.value?.scrollTo(arg);
|
||||
},
|
||||
} as BaseSelectRef);
|
||||
|
||||
const dropdownStyle: React.CSSProperties =
|
||||
const pickProps = computed(() => {
|
||||
return omit(props, [
|
||||
'id',
|
||||
'prefixCls',
|
||||
'fieldNames',
|
||||
|
||||
// Value
|
||||
'defaultValue',
|
||||
'value',
|
||||
'changeOnSelect',
|
||||
'onChange',
|
||||
'displayRender',
|
||||
'checkable',
|
||||
|
||||
// Search
|
||||
'searchValue',
|
||||
'onSearch',
|
||||
'showSearch',
|
||||
|
||||
// Trigger
|
||||
'expandTrigger',
|
||||
|
||||
// Options
|
||||
'options',
|
||||
'dropdownPrefixCls',
|
||||
'loadData',
|
||||
|
||||
// Open
|
||||
'popupVisible',
|
||||
'open',
|
||||
|
||||
'popupClassName',
|
||||
'dropdownClassName',
|
||||
'dropdownMenuColumnStyle',
|
||||
|
||||
'popupPlacement',
|
||||
'placement',
|
||||
|
||||
'onDropdownVisibleChange',
|
||||
'onPopupVisibleChange',
|
||||
|
||||
// Icon
|
||||
'expandIcon',
|
||||
'loadingIcon',
|
||||
'customSlots',
|
||||
|
||||
// Children
|
||||
'children',
|
||||
]);
|
||||
});
|
||||
return () => {
|
||||
const emptyOptions = !(mergedSearchValue.value ? searchOptions.value : mergedOptions.value)
|
||||
.length;
|
||||
|
||||
const dropdownStyle: CSSProperties =
|
||||
// Search to match width
|
||||
(mergedSearchValue && searchConfig.matchInputWidth) ||
|
||||
(mergedSearchValue.value && mergedSearchConfig.value.matchInputWidth) ||
|
||||
// Empty keep the width
|
||||
emptyOptions
|
||||
? {}
|
||||
: {
|
||||
minWidth: 'auto',
|
||||
};
|
||||
|
||||
return (
|
||||
<CascaderContext.Provider value={cascaderContext}>
|
||||
<BaseSelect
|
||||
{...restProps}
|
||||
{...pickProps.value}
|
||||
{...attrs}
|
||||
// MISC
|
||||
ref={ref as any}
|
||||
ref={selectRef}
|
||||
id={mergedId}
|
||||
prefixCls={prefixCls}
|
||||
prefixCls={props.prefixCls}
|
||||
dropdownMatchSelectWidth={false}
|
||||
dropdownStyle={dropdownStyle}
|
||||
// Value
|
||||
displayValues={displayValues}
|
||||
displayValues={displayValues.value}
|
||||
onDisplayValuesChange={onDisplayValuesChange}
|
||||
mode={multiple ? 'multiple' : undefined}
|
||||
mode={multiple.value ? 'multiple' : undefined}
|
||||
// Search
|
||||
searchValue={mergedSearchValue}
|
||||
searchValue={mergedSearchValue.value}
|
||||
onSearch={onInternalSearch}
|
||||
showSearch={mergedShowSearch}
|
||||
showSearch={mergedShowSearch.value}
|
||||
// Options
|
||||
OptionList={OptionList}
|
||||
emptyOptions={emptyOptions}
|
||||
// Open
|
||||
open={mergedOpen}
|
||||
dropdownClassName={mergedDropdownClassName}
|
||||
placement={mergedPlacement}
|
||||
open={mergedOpen.value}
|
||||
dropdownClassName={mergedDropdownClassName.value}
|
||||
placement={mergedPlacement.value}
|
||||
onDropdownVisibleChange={onInternalDropdownVisibleChange}
|
||||
// Children
|
||||
getRawInputElement={() => children}
|
||||
getRawInputElement={() => slots.default?.()}
|
||||
v-slots={slots}
|
||||
/>
|
||||
</CascaderContext.Provider>
|
||||
);
|
||||
}) as (<OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType>(
|
||||
props: React.PropsWithChildren<CascaderProps<OptionType>> & {
|
||||
ref?: React.Ref<BaseSelectRef>;
|
||||
},
|
||||
) => React.ReactElement) & {
|
||||
displayName?: string;
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Cascader.displayName = 'Cascader';
|
||||
}
|
||||
|
||||
export default Cascader;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,9 +16,9 @@ export default function Checkbox({
|
|||
disabled,
|
||||
onClick,
|
||||
}: 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 =
|
||||
typeof mergedCheckable === 'function'
|
||||
? mergedCheckable()
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { isLeaf, toPathKey } from '../utils/commonUtil';
|
||||
import CascaderContext from '../context';
|
||||
import Checkbox from './Checkbox';
|
||||
import type { DefaultOptionType, SingleValueType } from '../Cascader';
|
||||
import { SEARCH_MARK } from '../hooks/useSearchOptions';
|
||||
import type { Key } from '../../_util/type';
|
||||
import { useInjectCascader } from '../context';
|
||||
|
||||
export interface ColumnProps {
|
||||
prefixCls: string;
|
||||
multiple?: boolean;
|
||||
options: DefaultOptionType[];
|
||||
/** Current Column opened item key */
|
||||
activeValue?: React.Key;
|
||||
activeValue?: Key;
|
||||
/** The value path before current column */
|
||||
prevValuePath: React.Key[];
|
||||
prevValuePath: Key[];
|
||||
onToggleOpen: (open: boolean) => void;
|
||||
onSelect: (valuePath: SingleValueType, leaf: boolean) => void;
|
||||
onActive: (valuePath: SingleValueType) => void;
|
||||
checkedSet: Set<React.Key>;
|
||||
halfCheckedSet: Set<React.Key>;
|
||||
loadingKeys: React.Key[];
|
||||
checkedSet: Set<Key>;
|
||||
halfCheckedSet: Set<Key>;
|
||||
loadingKeys: Key[];
|
||||
isSelectable: (option: DefaultOptionType) => boolean;
|
||||
}
|
||||
|
||||
|
@ -44,27 +43,29 @@ export default function Column({
|
|||
fieldNames,
|
||||
changeOnSelect,
|
||||
expandTrigger,
|
||||
expandIcon,
|
||||
loadingIcon,
|
||||
expandIcon: expandIconRef,
|
||||
loadingIcon: loadingIconRef,
|
||||
dropdownMenuColumnStyle,
|
||||
} = React.useContext(CascaderContext);
|
||||
|
||||
const hoverOpen = expandTrigger === 'hover';
|
||||
customSlots,
|
||||
} = useInjectCascader();
|
||||
const expandIcon = expandIconRef.value ?? customSlots.value.expandIcon?.();
|
||||
const loadingIcon = loadingIconRef.value ?? customSlots.value.loadingIcon?.();
|
||||
|
||||
const hoverOpen = expandTrigger.value === 'hover';
|
||||
// ============================ Render ============================
|
||||
return (
|
||||
<ul className={menuPrefixCls} role="menu">
|
||||
<ul class={menuPrefixCls} role="menu">
|
||||
{options.map(option => {
|
||||
const { disabled } = option;
|
||||
const searchOptions = option[SEARCH_MARK];
|
||||
const label = option[fieldNames.label];
|
||||
const value = option[fieldNames.value];
|
||||
const label = option[fieldNames.value.label];
|
||||
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.
|
||||
const fullPath = searchOptions
|
||||
? searchOptions.map(opt => opt[fieldNames.value])
|
||||
? searchOptions.map(opt => opt[fieldNames.value.value])
|
||||
: [...prevValuePath, value];
|
||||
const fullPathKey = toPathKey(fullPath);
|
||||
|
||||
|
@ -75,7 +76,6 @@ export default function Column({
|
|||
|
||||
// >>>>> halfChecked
|
||||
const halfChecked = halfCheckedSet.has(fullPathKey);
|
||||
|
||||
// >>>>> Open
|
||||
const triggerOpenPath = () => {
|
||||
if (!disabled && (!hoverOpen || !isMergedLeaf)) {
|
||||
|
@ -102,13 +102,16 @@ export default function Column({
|
|||
return (
|
||||
<li
|
||||
key={fullPathKey}
|
||||
className={classNames(menuItemPrefixCls, {
|
||||
class={[
|
||||
menuItemPrefixCls,
|
||||
{
|
||||
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
|
||||
[`${menuItemPrefixCls}-active`]: activeValue === value,
|
||||
[`${menuItemPrefixCls}-disabled`]: disabled,
|
||||
[`${menuItemPrefixCls}-loading`]: isLoading,
|
||||
})}
|
||||
style={dropdownMenuColumnStyle}
|
||||
},
|
||||
]}
|
||||
style={dropdownMenuColumnStyle.value}
|
||||
role="menuitemcheckbox"
|
||||
title={title}
|
||||
aria-checked={checked}
|
||||
|
@ -119,12 +122,12 @@ export default function Column({
|
|||
triggerSelect();
|
||||
}
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (changeOnSelect) {
|
||||
onDblclick={() => {
|
||||
if (changeOnSelect.value) {
|
||||
onToggleOpen(false);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
onMouseenter={() => {
|
||||
if (hoverOpen) {
|
||||
triggerOpenPath();
|
||||
}
|
||||
|
@ -136,18 +139,18 @@ export default function Column({
|
|||
checked={checked}
|
||||
halfChecked={halfChecked}
|
||||
disabled={disabled}
|
||||
onClick={(e: React.MouseEvent<HTMLSpanElement>) => {
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
triggerSelect();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className={`${menuItemPrefixCls}-content`}>{option[fieldNames.label]}</div>
|
||||
<div class={`${menuItemPrefixCls}-content`}>{option[fieldNames.value.label]}</div>
|
||||
{!isLoading && expandIcon && !isMergedLeaf && (
|
||||
<div className={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
|
||||
<div class={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
|
||||
)}
|
||||
{isLoading && loadingIcon && (
|
||||
<div className={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
|
||||
<div class={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
|
@ -155,3 +158,19 @@ export default function Column({
|
|||
</ul>
|
||||
);
|
||||
}
|
||||
Column.props = [
|
||||
'prefixCls',
|
||||
'multiple',
|
||||
'options',
|
||||
'activeValue',
|
||||
'prevValuePath',
|
||||
'onToggleOpen',
|
||||
'onSelect',
|
||||
'onActive',
|
||||
'checkedSet',
|
||||
'halfCheckedSet',
|
||||
'loadingKeys',
|
||||
'isSelectable',
|
||||
];
|
||||
Column.displayName = 'Column';
|
||||
Column.inheritAttrs = false;
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
/* 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 CascaderContext from '../context';
|
||||
import type { DefaultOptionType, SingleValueType } from '../Cascader';
|
||||
import { isLeaf, toPathKey, toPathKeys, toPathValueStr } from '../utils/commonUtil';
|
||||
import useActive from './useActive';
|
||||
import useKeyboard from './useKeyboard';
|
||||
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) => {
|
||||
const { prefixCls, multiple, searchValue, toggleOpen, notFoundContent, direction } =
|
||||
useBaseProps();
|
||||
|
||||
const containerRef = React.useRef<HTMLDivElement>();
|
||||
const rtl = direction === 'rtl';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OptionList',
|
||||
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,
|
||||
|
@ -29,58 +30,63 @@ const RefOptionList = React.forwardRef<RefOptionListProps>((props, ref) => {
|
|||
dropdownPrefixCls,
|
||||
loadData,
|
||||
expandTrigger,
|
||||
} = React.useContext(CascaderContext);
|
||||
customSlots,
|
||||
} = useInjectCascader();
|
||||
|
||||
const mergedPrefixCls = dropdownPrefixCls || prefixCls;
|
||||
const mergedPrefixCls = computed(() => dropdownPrefixCls.value || baseProps.prefixCls);
|
||||
|
||||
// ========================= loadData =========================
|
||||
const [loadingKeys, setLoadingKeys] = React.useState([]);
|
||||
|
||||
const internalLoadData = (valueCells: React.Key[]) => {
|
||||
const loadingKeys = shallowRef<string[]>([]);
|
||||
const internalLoadData = (valueCells: Key[]) => {
|
||||
// Do not load when search
|
||||
if (!loadData || searchValue) {
|
||||
if (!loadData.value || baseProps.searchValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optionList = toPathOptions(valueCells, options, fieldNames);
|
||||
const optionList = toPathOptions(valueCells, options.value, fieldNames.value);
|
||||
const rawOptions = optionList.map(({ option }) => option);
|
||||
const lastOption = rawOptions[rawOptions.length - 1];
|
||||
|
||||
if (lastOption && !isLeaf(lastOption, fieldNames)) {
|
||||
if (lastOption && !isLeaf(lastOption, fieldNames.value)) {
|
||||
const pathKey = toPathKey(valueCells);
|
||||
|
||||
setLoadingKeys(keys => [...keys, pathKey]);
|
||||
|
||||
loadData(rawOptions);
|
||||
loadingKeys.value = [...loadingKeys.value, pathKey];
|
||||
loadData.value(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 => {
|
||||
watchEffect(() => {
|
||||
if (loadingKeys.value.length) {
|
||||
loadingKeys.value.forEach(loadingKey => {
|
||||
const valueStrCells = toPathValueStr(loadingKey);
|
||||
const optionList = toPathOptions(valueStrCells, options, fieldNames, true).map(
|
||||
({ option }) => option,
|
||||
);
|
||||
const optionList = toPathOptions(
|
||||
valueStrCells,
|
||||
options.value,
|
||||
fieldNames.value,
|
||||
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));
|
||||
if (
|
||||
!lastOption ||
|
||||
lastOption[fieldNames.value.children] ||
|
||||
isLeaf(lastOption, fieldNames.value)
|
||||
) {
|
||||
loadingKeys.value = loadingKeys.value.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]);
|
||||
const checkedSet = computed(() => new Set(toPathKeys(values.value)));
|
||||
const halfCheckedSet = computed(() => new Set(toPathKeys(halfValues.value)));
|
||||
|
||||
// ====================== Accessibility =======================
|
||||
const [activeValueCells, setActiveValueCells] = useActive();
|
||||
|
||||
// =========================== Path ===========================
|
||||
const onPathOpen = (nextValueCells: React.Key[]) => {
|
||||
const onPathOpen = (nextValueCells: Key[]) => {
|
||||
setActiveValueCells(nextValueCells);
|
||||
|
||||
// Trigger loadData
|
||||
|
@ -90,39 +96,41 @@ const RefOptionList = React.forwardRef<RefOptionListProps>((props, ref) => {
|
|||
const isSelectable = (option: DefaultOptionType) => {
|
||||
const { disabled } = option;
|
||||
|
||||
const isMergedLeaf = isLeaf(option, fieldNames);
|
||||
return !disabled && (isMergedLeaf || changeOnSelect || multiple);
|
||||
const isMergedLeaf = isLeaf(option, fieldNames.value);
|
||||
return !disabled && (isMergedLeaf || changeOnSelect.value || baseProps.multiple);
|
||||
};
|
||||
|
||||
const onPathSelect = (valuePath: SingleValueType, leaf: boolean, fromKeyboard = false) => {
|
||||
onSelect(valuePath);
|
||||
|
||||
if (!multiple && (leaf || (changeOnSelect && (expandTrigger === 'hover' || fromKeyboard)))) {
|
||||
toggleOpen(false);
|
||||
if (
|
||||
!baseProps.multiple &&
|
||||
(leaf || (changeOnSelect.value && (expandTrigger.value === 'hover' || fromKeyboard)))
|
||||
) {
|
||||
baseProps.toggleOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ========================== Option ==========================
|
||||
const mergedOptions = React.useMemo(() => {
|
||||
if (searchValue) {
|
||||
return searchOptions;
|
||||
const mergedOptions = computed(() => {
|
||||
if (baseProps.searchValue) {
|
||||
return searchOptions.value;
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [searchValue, searchOptions, options]);
|
||||
return options.value;
|
||||
});
|
||||
|
||||
// ========================== 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 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] === activeValueCell,
|
||||
option => option[fieldNames.value.value] === activeValueCell,
|
||||
);
|
||||
|
||||
const subOptions = currentOption?.[fieldNames.children];
|
||||
const subOptions = currentOption?.[fieldNames.value.children];
|
||||
if (!subOptions?.length) {
|
||||
break;
|
||||
}
|
||||
|
@ -132,17 +140,17 @@ const RefOptionList = React.forwardRef<RefOptionListProps>((props, ref) => {
|
|||
}
|
||||
|
||||
return optionList;
|
||||
}, [mergedOptions, activeValueCells, fieldNames]);
|
||||
});
|
||||
|
||||
// ========================= Keyboard =========================
|
||||
const onKeyboardSelect = (selectValueCells: SingleValueType, option: DefaultOptionType) => {
|
||||
if (isSelectable(option)) {
|
||||
onPathSelect(selectValueCells, isLeaf(option, fieldNames), true);
|
||||
onPathSelect(selectValueCells, isLeaf(option, fieldNames.value), true);
|
||||
}
|
||||
};
|
||||
|
||||
useKeyboard(
|
||||
ref,
|
||||
context,
|
||||
mergedOptions,
|
||||
fieldNames,
|
||||
activeValueCells,
|
||||
|
@ -150,64 +158,71 @@ const RefOptionList = React.forwardRef<RefOptionListProps>((props, ref) => {
|
|||
containerRef,
|
||||
onKeyboardSelect,
|
||||
);
|
||||
|
||||
const onListMouseDown: EventHandler = event => {
|
||||
event.preventDefault();
|
||||
};
|
||||
return () => {
|
||||
// ========================== Render ==========================
|
||||
const {
|
||||
notFoundContent = slots.notFoundContent?.() || customSlots.value.notFoundContent?.(),
|
||||
multiple,
|
||||
toggleOpen,
|
||||
} = baseProps;
|
||||
// >>>>> Empty
|
||||
const isEmpty = !optionColumns[0]?.options?.length;
|
||||
const isEmpty = !optionColumns.value[0]?.options?.length;
|
||||
|
||||
const emptyList: DefaultOptionType[] = [
|
||||
{
|
||||
[fieldNames.label as 'label']: notFoundContent,
|
||||
[fieldNames.value as 'value']: '__EMPTY__',
|
||||
[fieldNames.value.label as 'label']: notFoundContent,
|
||||
[fieldNames.value.value as 'value']: '__EMPTY__',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const columnProps = {
|
||||
...props,
|
||||
...attrs,
|
||||
multiple: !isEmpty && multiple,
|
||||
onSelect: onPathSelect,
|
||||
onActive: onPathOpen,
|
||||
onToggleOpen: toggleOpen,
|
||||
checkedSet,
|
||||
halfCheckedSet,
|
||||
loadingKeys,
|
||||
checkedSet: checkedSet.value,
|
||||
halfCheckedSet: halfCheckedSet.value,
|
||||
loadingKeys: loadingKeys.value,
|
||||
isSelectable,
|
||||
};
|
||||
|
||||
// >>>>> Columns
|
||||
const mergedOptionColumns = isEmpty ? [{ options: emptyList }] : optionColumns;
|
||||
const mergedOptionColumns = isEmpty ? [{ options: emptyList }] : optionColumns.value;
|
||||
|
||||
const columnNodes: React.ReactElement[] = mergedOptionColumns.map((col, index) => {
|
||||
const prevValuePath = activeValueCells.slice(0, index);
|
||||
const activeValue = activeValueCells[index];
|
||||
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}
|
||||
prefixCls={mergedPrefixCls.value}
|
||||
options={col.options}
|
||||
prevValuePath={prevValuePath}
|
||||
activeValue={activeValue}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// >>>>> Render
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(`${mergedPrefixCls}-menus`, {
|
||||
[`${mergedPrefixCls}-menu-empty`]: isEmpty,
|
||||
[`${mergedPrefixCls}-rtl`]: rtl,
|
||||
})}
|
||||
class={[
|
||||
`${mergedPrefixCls.value}-menus`,
|
||||
{
|
||||
[`${mergedPrefixCls.value}-menu-empty`]: isEmpty,
|
||||
[`${mergedPrefixCls.value}-rtl`]: rtl.value,
|
||||
},
|
||||
]}
|
||||
onMousedown={onListMouseDown}
|
||||
ref={containerRef}
|
||||
>
|
||||
{columnNodes}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default RefOptionList;
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
import * as React from 'react';
|
||||
import CascaderContext from '../context';
|
||||
import { useBaseProps } from 'rc-select';
|
||||
import { useInjectCascader } from '../context';
|
||||
import type { Ref } from 'vue';
|
||||
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.
|
||||
*/
|
||||
export default (): [React.Key[], (activeValueCells: React.Key[]) => void] => {
|
||||
const { multiple, open } = useBaseProps();
|
||||
const { values } = React.useContext(CascaderContext);
|
||||
export default (): [Ref<Key[]>, (activeValueCells: Key[]) => void] => {
|
||||
const baseProps = useBaseProps();
|
||||
const { values } = useInjectCascader();
|
||||
|
||||
// Record current dropdown active options
|
||||
// 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) {
|
||||
const firstValueCells = values[0];
|
||||
if (baseProps.open && !baseProps.multiple) {
|
||||
const firstValueCells = values.value[0];
|
||||
setActiveValueCells(firstValueCells || []);
|
||||
}
|
||||
},
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
[open],
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return [activeValueCells, setActiveValueCells];
|
||||
|
|
|
@ -1,36 +1,42 @@
|
|||
import * as React from 'react';
|
||||
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
|
||||
import KeyCode from 'rc-util/lib/KeyCode';
|
||||
import type { RefOptionListProps } from '../../vc-select/OptionList';
|
||||
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||
import type { Ref, SetupContext } from 'vue';
|
||||
import { ref, watchEffect } from 'vue';
|
||||
import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader';
|
||||
import { toPathKey } from '../utils/commonUtil';
|
||||
import { useBaseProps } from 'rc-select';
|
||||
import { useBaseProps } from '../../vc-select';
|
||||
import KeyCode from '../../_util/KeyCode';
|
||||
|
||||
export default (
|
||||
ref: React.Ref<RefOptionListProps>,
|
||||
options: DefaultOptionType[],
|
||||
fieldNames: InternalFieldNames,
|
||||
activeValueCells: React.Key[],
|
||||
setActiveValueCells: (activeValueCells: React.Key[]) => void,
|
||||
containerRef: React.RefObject<HTMLElement>,
|
||||
context: SetupContext,
|
||||
options: Ref<DefaultOptionType[]>,
|
||||
fieldNames: Ref<InternalFieldNames>,
|
||||
activeValueCells: Ref<Key[]>,
|
||||
setActiveValueCells: (activeValueCells: Key[]) => void,
|
||||
containerRef: Ref<HTMLElement>,
|
||||
onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void,
|
||||
) => {
|
||||
const { direction, searchValue, toggleOpen, open } = useBaseProps();
|
||||
const rtl = direction === 'rtl';
|
||||
|
||||
const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = React.useMemo(() => {
|
||||
const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = [
|
||||
ref<Key[]>([]),
|
||||
ref<number>(),
|
||||
ref<DefaultOptionType[]>([]),
|
||||
];
|
||||
watchEffect(() => {
|
||||
let activeIndex = -1;
|
||||
let currentOptions = options;
|
||||
let currentOptions = options.value;
|
||||
|
||||
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
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
// Mark the active index for current options
|
||||
const nextActiveIndex = currentOptions.findIndex(
|
||||
option => option[fieldNames.value] === activeValueCells[i],
|
||||
option => option[fieldNames.value.value] === activeValueCells.value[i],
|
||||
);
|
||||
|
||||
if (nextActiveIndex === -1) {
|
||||
|
@ -39,44 +45,48 @@ export default (
|
|||
|
||||
activeIndex = nextActiveIndex;
|
||||
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
|
||||
let activeOptions = options;
|
||||
let activeOptions = options.value;
|
||||
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];
|
||||
}, [activeValueCells, fieldNames, options]);
|
||||
[validActiveValueCells.value, lastActiveIndex.value, lastActiveOptions.value] = [
|
||||
mergedActiveValueCells,
|
||||
activeIndex,
|
||||
activeOptions,
|
||||
];
|
||||
});
|
||||
|
||||
// Update active value cells and scroll to target element
|
||||
const internalSetActiveValueCells = (next: React.Key[]) => {
|
||||
const internalSetActiveValueCells = (next: Key[]) => {
|
||||
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' });
|
||||
};
|
||||
|
||||
// Same options offset
|
||||
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) {
|
||||
currentIndex = len;
|
||||
}
|
||||
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
currentIndex = (currentIndex + offset + len) % len;
|
||||
const option = lastActiveOptions[currentIndex];
|
||||
const option = lastActiveOptions.value[currentIndex];
|
||||
|
||||
if (option && !option.disabled) {
|
||||
const value = option[fieldNames.value];
|
||||
const nextActiveCells = validActiveValueCells.slice(0, -1).concat(value);
|
||||
const value = option[fieldNames.value.value];
|
||||
const nextActiveCells = validActiveValueCells.value.slice(0, -1).concat(value);
|
||||
internalSetActiveValueCells(nextActiveCells);
|
||||
return;
|
||||
}
|
||||
|
@ -85,8 +95,8 @@ export default (
|
|||
|
||||
// Different options offset
|
||||
const prevColumn = () => {
|
||||
if (validActiveValueCells.length > 1) {
|
||||
const nextActiveCells = validActiveValueCells.slice(0, -1);
|
||||
if (validActiveValueCells.value.length > 1) {
|
||||
const nextActiveCells = validActiveValueCells.value.slice(0, -1);
|
||||
internalSetActiveValueCells(nextActiveCells);
|
||||
} else {
|
||||
toggleOpen(false);
|
||||
|
@ -95,19 +105,19 @@ export default (
|
|||
|
||||
const nextColumn = () => {
|
||||
const nextOptions: DefaultOptionType[] =
|
||||
lastActiveOptions[lastActiveIndex]?.[fieldNames.children] || [];
|
||||
lastActiveOptions.value[lastActiveIndex.value]?.[fieldNames.value.children] || [];
|
||||
|
||||
const nextOption = nextOptions.find(option => !option.disabled);
|
||||
|
||||
if (nextOption) {
|
||||
const nextActiveCells = [...validActiveValueCells, nextOption[fieldNames.value]];
|
||||
const nextActiveCells = [...validActiveValueCells.value, nextOption[fieldNames.value.value]];
|
||||
internalSetActiveValueCells(nextActiveCells);
|
||||
}
|
||||
};
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
context.expose({
|
||||
// scrollTo: treeRef.current?.scrollTo,
|
||||
onKeyDown: event => {
|
||||
onKeydown: event => {
|
||||
const { which } = event;
|
||||
|
||||
switch (which) {
|
||||
|
@ -155,8 +165,11 @@ export default (
|
|||
|
||||
// >>> Select
|
||||
case KeyCode.ENTER: {
|
||||
if (validActiveValueCells.length) {
|
||||
onKeyBoardSelect(validActiveValueCells, lastActiveOptions[lastActiveIndex]);
|
||||
if (validActiveValueCells.value.length) {
|
||||
onKeyBoardSelect(
|
||||
validActiveValueCells.value,
|
||||
lastActiveOptions.value[lastActiveIndex.value],
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -171,6 +184,6 @@ export default (
|
|||
}
|
||||
}
|
||||
},
|
||||
onKeyUp: () => {},
|
||||
}));
|
||||
onKeyup: () => {},
|
||||
} as RefOptionListProps);
|
||||
};
|
||||
|
|
|
@ -2,14 +2,14 @@ import type { CSSProperties, InjectionKey, Ref } from 'vue';
|
|||
import { inject, provide } from 'vue';
|
||||
import type { VueNode } from '../_util/type';
|
||||
import type {
|
||||
CascaderProps,
|
||||
BaseCascaderProps,
|
||||
InternalFieldNames,
|
||||
DefaultOptionType,
|
||||
SingleValueType,
|
||||
} from './Cascader';
|
||||
|
||||
export interface CascaderContextProps {
|
||||
options: Ref<CascaderProps['options']>;
|
||||
options: Ref<BaseCascaderProps['options']>;
|
||||
fieldNames: Ref<InternalFieldNames>;
|
||||
values: Ref<SingleValueType[]>;
|
||||
halfValues: Ref<SingleValueType[]>;
|
||||
|
@ -23,7 +23,7 @@ export interface CascaderContextProps {
|
|||
expandIcon: Ref<VueNode>;
|
||||
loadingIcon: Ref<VueNode>;
|
||||
dropdownMenuColumnStyle: Ref<CSSProperties>;
|
||||
slotsContext: Ref<Record<string, Function>>;
|
||||
customSlots: Ref<Record<string, Function>>;
|
||||
}
|
||||
|
||||
const CascaderContextKey: InjectionKey<CascaderContextProps> = Symbol('CascaderContextKey');
|
||||
|
|
|
@ -1,37 +1,38 @@
|
|||
import { toPathOptions } from '../utils/treeUtil';
|
||||
import * as React from 'react';
|
||||
import type {
|
||||
DefaultOptionType,
|
||||
SingleValueType,
|
||||
CascaderProps,
|
||||
BaseCascaderProps,
|
||||
InternalFieldNames,
|
||||
} from '../Cascader';
|
||||
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 (
|
||||
rawValues: SingleValueType[],
|
||||
options: DefaultOptionType[],
|
||||
fieldNames: InternalFieldNames,
|
||||
multiple: boolean,
|
||||
displayRender: CascaderProps['displayRender'],
|
||||
rawValues: Ref<SingleValueType[]>,
|
||||
options: Ref<DefaultOptionType[]>,
|
||||
fieldNames: Ref<InternalFieldNames>,
|
||||
multiple: Ref<boolean>,
|
||||
displayRender: Ref<BaseCascaderProps['displayRender']>,
|
||||
) => {
|
||||
return React.useMemo(() => {
|
||||
return computed(() => {
|
||||
const mergedDisplayRender =
|
||||
displayRender ||
|
||||
displayRender.value ||
|
||||
// Default displayRender
|
||||
(labels => {
|
||||
const mergedLabels = multiple ? labels.slice(-1) : labels;
|
||||
(({ labels }) => {
|
||||
const mergedLabels = multiple.value ? labels.slice(-1) : labels;
|
||||
const SPLIT = ' / ';
|
||||
|
||||
if (mergedLabels.every(label => ['string', 'number'].includes(typeof label))) {
|
||||
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) => {
|
||||
const keyedLabel = React.isValidElement(label)
|
||||
? React.cloneElement(label, { key: index })
|
||||
: label;
|
||||
const keyedLabel = isValidElement(label) ? cloneElement(label, { key: index }) : label;
|
||||
|
||||
if (index === 0) {
|
||||
return [keyedLabel];
|
||||
|
@ -41,13 +42,13 @@ export default (
|
|||
}, []);
|
||||
});
|
||||
|
||||
return rawValues.map(valueCells => {
|
||||
const valueOptions = toPathOptions(valueCells, options, fieldNames);
|
||||
return rawValues.value.map(valueCells => {
|
||||
const valueOptions = toPathOptions(valueCells, options.value, fieldNames.value);
|
||||
|
||||
const label = mergedDisplayRender(
|
||||
valueOptions.map(({ option, value }) => option?.[fieldNames.label] ?? value),
|
||||
valueOptions.map(({ option }) => option),
|
||||
);
|
||||
const label = mergedDisplayRender({
|
||||
labels: valueOptions.map(({ option, value }) => option?.[fieldNames.value.label] ?? value),
|
||||
selectedOptions: valueOptions.map(({ option }) => option),
|
||||
});
|
||||
|
||||
return {
|
||||
label,
|
||||
|
@ -55,5 +56,5 @@ export default (
|
|||
valueCells,
|
||||
};
|
||||
});
|
||||
}, [rawValues, options, fieldNames, displayRender, multiple]);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -10,13 +10,11 @@ export interface OptionsInfo {
|
|||
pathKeyEntities: Record<string, DataEntity>;
|
||||
}
|
||||
|
||||
export type GetEntities = () => OptionsInfo['pathKeyEntities'];
|
||||
|
||||
/** Lazy parse options data into conduct-able info to avoid perf issue in single mode */
|
||||
export default (options: Ref<DefaultOptionType[]>, fieldNames: Ref<InternalFieldNames>) => {
|
||||
const entities = computed(() => {
|
||||
return (
|
||||
convertDataToEntities(options as any, {
|
||||
convertDataToEntities(options.value as any, {
|
||||
fieldNames: fieldNames.value,
|
||||
initWrapper: wrapper => ({
|
||||
...wrapper,
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import type { CascaderProps, ShowSearchType } from '../Cascader';
|
||||
import type { BaseCascaderProps, ShowSearchType } from '../Cascader';
|
||||
import type { Ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { ref, watchEffect } from 'vue';
|
||||
import { warning } from '../../vc-util/warning';
|
||||
|
||||
// Convert `showSearch` to unique config
|
||||
export default function useSearchConfig(showSearch?: Ref<CascaderProps['showSearch']>) {
|
||||
return computed(() => {
|
||||
export default function useSearchConfig(showSearch?: Ref<BaseCascaderProps['showSearch']>) {
|
||||
const mergedShowSearch = ref(false);
|
||||
const mergedSearchConfig = ref<ShowSearchType>({});
|
||||
watchEffect(() => {
|
||||
if (!showSearch.value) {
|
||||
return [false, {}];
|
||||
mergedShowSearch.value = false;
|
||||
mergedSearchConfig.value = {};
|
||||
return;
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
return [true, searchConfig];
|
||||
mergedShowSearch.value = true;
|
||||
mergedSearchConfig.value = searchConfig;
|
||||
return;
|
||||
});
|
||||
return { showSearch: mergedShowSearch, searchConfig: mergedSearchConfig };
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -1,10 +1,12 @@
|
|||
import type { Key } from '../../_util/type';
|
||||
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 keyPathEntities = getKeyPathEntities();
|
||||
|
||||
return pathKeys.filter(key => {
|
||||
const entity = keyPathEntities[key];
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
} from 'vue';
|
||||
import type { CSSProperties, ExtractPropTypes, PropType, VNode } from 'vue';
|
||||
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 KeyCode from '../_util/KeyCode';
|
||||
import { toReactive } from '../_util/toReactive';
|
||||
|
@ -38,6 +38,7 @@ import classNames from '../_util/classNames';
|
|||
import createRef from '../_util/createRef';
|
||||
import type { BaseOptionType } from './Select';
|
||||
import useInjectLegacySelectContext from '../vc-tree-select/LegacyContext';
|
||||
import { cloneElement } from '../_util/vnode';
|
||||
|
||||
const DEFAULT_OMIT_PROPS = [
|
||||
'value',
|
||||
|
@ -543,6 +544,8 @@ export default defineComponent({
|
|||
focus: onContainerFocus,
|
||||
blur: onContainerBlur,
|
||||
});
|
||||
|
||||
// Give focus back of Select
|
||||
const activeTimeoutIds: any[] = [];
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -591,7 +594,7 @@ export default defineComponent({
|
|||
triggerOpen,
|
||||
() => {
|
||||
if (triggerOpen.value) {
|
||||
const newWidth = Math.ceil(containerRef.value.offsetWidth);
|
||||
const newWidth = Math.ceil(containerRef.value?.offsetWidth);
|
||||
if (containerWidth.value !== newWidth && !Number.isNaN(newWidth)) {
|
||||
containerWidth.value = newWidth;
|
||||
}
|
||||
|
@ -740,7 +743,7 @@ export default defineComponent({
|
|||
onInternalSearch('', false, false);
|
||||
};
|
||||
|
||||
if (!disabled && allowClear && (displayValues.length || mergedSearchValue)) {
|
||||
if (!disabled && allowClear && (displayValues.length || mergedSearchValue.value)) {
|
||||
clearNode = (
|
||||
<TransBtn
|
||||
class={`${prefixCls}-clear`}
|
||||
|
@ -800,7 +803,15 @@ export default defineComponent({
|
|||
v-slots={{
|
||||
default: () => {
|
||||
return customizeRawInputElement ? (
|
||||
customizeRawInputElement
|
||||
isValidElement(customizeRawInputElement) &&
|
||||
cloneElement(
|
||||
customizeRawInputElement,
|
||||
{
|
||||
ref: selectorDomRef,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
)
|
||||
) : (
|
||||
<Selector
|
||||
{...props}
|
||||
|
|
|
@ -161,8 +161,8 @@ const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
|
|||
return (
|
||||
<Trigger
|
||||
{...props}
|
||||
showAction={[]}
|
||||
hideAction={[]}
|
||||
showAction={onPopupVisibleChange ? ['click'] : []}
|
||||
hideAction={onPopupVisibleChange ? ['click'] : []}
|
||||
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
|
||||
builtinPlacements={builtInPlacements.value}
|
||||
prefixCls={dropdownPrefixCls}
|
||||
|
|
|
@ -174,11 +174,11 @@ const Input = defineComponent({
|
|||
this.VCSelectContainerEvent?.focus(args[0]);
|
||||
},
|
||||
onBlur: (...args: any[]) => {
|
||||
this.blurTimeout = setTimeout(() => {
|
||||
// this.blurTimeout = setTimeout(() => {
|
||||
onOriginBlur && onOriginBlur(args[0]);
|
||||
onBlur && onBlur(args[0]);
|
||||
this.VCSelectContainerEvent?.blur(args[0]);
|
||||
}, 200);
|
||||
// }, 200);
|
||||
},
|
||||
},
|
||||
inputNode.type === 'textarea' ? {} : { type: 'search' },
|
||||
|
|
Loading…
Reference in New Issue