refactor: cascader
parent
d46762c1d6
commit
e08c6da9b5
|
@ -1,7 +1,7 @@
|
||||||
import type { FunctionalComponent } from 'vue';
|
import type { FunctionalComponent } from 'vue';
|
||||||
import type { OptionCoreData } from '../vc-select/interface';
|
import type { DefaultOptionType } from '../vc-select/Select';
|
||||||
|
|
||||||
export interface OptionProps extends Omit<OptionCoreData, 'label'> {
|
export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
|
||||||
/** Save for customize data */
|
/** Save for customize data */
|
||||||
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,8 @@ Cascade selection box for selecting province/city/district.
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
const options: CascaderProps['options'] = [
|
||||||
label: string;
|
|
||||||
children?: Option[];
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
|
|
@ -16,16 +16,17 @@ Allow only select parent options.
|
||||||
|
|
||||||
</docs>
|
</docs>
|
||||||
<template>
|
<template>
|
||||||
<a-cascader v-model:value="value" :options="options" change-on-select />
|
<a-cascader
|
||||||
|
v-model:value="value"
|
||||||
|
:options="options"
|
||||||
|
placeholder="Please select"
|
||||||
|
change-on-select
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
const options: CascaderProps['options'] = [
|
||||||
label: string;
|
|
||||||
children?: Option[];
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
|
|
@ -16,7 +16,12 @@ For instance, add an external link after the selected value.
|
||||||
|
|
||||||
</docs>
|
</docs>
|
||||||
<template>
|
<template>
|
||||||
<a-cascader v-model:value="value" :options="options" style="width: 100%">
|
<a-cascader
|
||||||
|
v-model:value="value"
|
||||||
|
placeholder="Please select"
|
||||||
|
:options="options"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
<template #displayRender="{ labels, selectedOptions }">
|
<template #displayRender="{ labels, selectedOptions }">
|
||||||
<span v-for="(label, index) in labels" :key="selectedOptions[index].value">
|
<span v-for="(label, index) in labels" :key="selectedOptions[index].value">
|
||||||
<span v-if="index === labels.length - 1">
|
<span v-if="index === labels.length - 1">
|
||||||
|
@ -33,14 +38,8 @@ For instance, add an external link after the selected value.
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
const options: CascaderProps['options'] = [
|
||||||
label: string;
|
|
||||||
children?: Option[];
|
|
||||||
code?: number;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
|
|
@ -18,21 +18,20 @@ Separate trigger button and result.
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
{{ text }}
|
{{ 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 href="#">Change city</a>
|
||||||
</a-cascader>
|
</a-cascader>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
const options: CascaderProps['options'] = [
|
||||||
label: string;
|
|
||||||
children?: Option[];
|
|
||||||
code?: number;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
|
|
@ -16,19 +16,12 @@ Disable option by specifying the `disabled` property in `options`.
|
||||||
|
|
||||||
</docs>
|
</docs>
|
||||||
<template>
|
<template>
|
||||||
<a-cascader v-model:value="value" :options="options" />
|
<a-cascader v-model:value="value" placeholder="Please select" :options="options" />
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
const options: CascaderProps['options'] = [
|
||||||
label: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
children?: Option[];
|
|
||||||
code?: number;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
|
|
@ -25,14 +25,8 @@ Custom Field Names
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
code: string;
|
const options: CascaderProps['options'] = [
|
||||||
name: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
items?: Option[];
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
code: 'zhejiang',
|
code: 'zhejiang',
|
||||||
name: 'Zhejiang',
|
name: 'Zhejiang',
|
||||||
|
|
|
@ -19,19 +19,14 @@ Hover to expand sub menu, click to select option.
|
||||||
<a-cascader
|
<a-cascader
|
||||||
v-model:value="value"
|
v-model:value="value"
|
||||||
:options="options"
|
:options="options"
|
||||||
:display-render="displayRender"
|
|
||||||
expand-trigger="hover"
|
expand-trigger="hover"
|
||||||
placeholder="Please select"
|
placeholder="Please select"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
const options: CascaderProps['options'] = [
|
||||||
label: string;
|
|
||||||
children?: Option[];
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
@ -67,14 +62,9 @@ const options: Option[] = [
|
||||||
];
|
];
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const displayRender = ({ labels }: { labels: string[] }) => {
|
|
||||||
return labels[labels.length - 1];
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: ref<string[]>([]),
|
value: ref<string[]>([]),
|
||||||
options,
|
options,
|
||||||
displayRender,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,16 +28,11 @@ Load options lazily with `loadData`.
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
loading?: boolean;
|
|
||||||
isLeaf?: boolean;
|
|
||||||
children?: Option[];
|
|
||||||
}
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const options = ref<Option[]>([
|
const options = ref<CascaderProps['options']>([
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
|
|
@ -27,13 +27,8 @@ Search and select options directly.
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
const options: CascaderProps['options'] = [
|
||||||
label: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
children?: Option[];
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
|
|
@ -16,24 +16,20 @@ Cascade selection box of different sizes.
|
||||||
|
|
||||||
</docs>
|
</docs>
|
||||||
<template>
|
<template>
|
||||||
<a-cascader v-model:value="value" size="large" :options="options" />
|
<a-cascader v-model:value="value" placeholder="Please select" size="large" :options="options" />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<a-cascader v-model:value="value" :options="options" />
|
<a-cascader v-model:value="value" placeholder="Please select" :options="options" />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<a-cascader v-model:value="value" size="small" :options="options" />
|
<a-cascader v-model:value="value" placeholder="Please select" size="small" :options="options" />
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
const options: CascaderProps['options'] = [
|
||||||
label: string;
|
|
||||||
children?: Option[];
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
|
|
@ -35,12 +35,8 @@ Custom suffix icon
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { SmileOutlined } from '@ant-design/icons-vue';
|
import { SmileOutlined } from '@ant-design/icons-vue';
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
interface Option {
|
import type { CascaderProps } from 'ant-design-vue';
|
||||||
value: string;
|
const options: CascaderProps['options'] = [
|
||||||
label: string;
|
|
||||||
children?: Option[];
|
|
||||||
}
|
|
||||||
const options: Option[] = [
|
|
||||||
{
|
{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
|
|
@ -1,646 +1,286 @@
|
||||||
import type { PropType, CSSProperties, ExtractPropTypes } from 'vue';
|
import type {
|
||||||
import { inject, provide, defineComponent } from 'vue';
|
ShowSearchType,
|
||||||
import PropTypes from '../_util/vue-types';
|
FieldNames,
|
||||||
import VcCascader from '../vc-cascader';
|
BaseOptionType,
|
||||||
import arrayTreeFilter from 'array-tree-filter';
|
DefaultOptionType,
|
||||||
import classNames from '../_util/classNames';
|
} from '../vc-cascader2';
|
||||||
import KeyCode from '../_util/KeyCode';
|
import VcCascader, { cascaderProps as vcCascaderProps } from '../vc-cascader2';
|
||||||
import Input from '../input';
|
|
||||||
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
|
|
||||||
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
|
|
||||||
import RightOutlined from '@ant-design/icons-vue/RightOutlined';
|
import RightOutlined from '@ant-design/icons-vue/RightOutlined';
|
||||||
import RedoOutlined from '@ant-design/icons-vue/RedoOutlined';
|
import RedoOutlined from '@ant-design/icons-vue/RedoOutlined';
|
||||||
import {
|
import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
|
||||||
hasProp,
|
import getIcons from '../select/utils/iconUtil';
|
||||||
getOptionProps,
|
|
||||||
isValidElement,
|
|
||||||
getComponent,
|
|
||||||
splitAttrs,
|
|
||||||
findDOMNode,
|
|
||||||
getSlot,
|
|
||||||
} from '../_util/props-util';
|
|
||||||
import BaseMixin from '../_util/BaseMixin';
|
|
||||||
import { cloneElement } from '../_util/vnode';
|
|
||||||
import warning from '../_util/warning';
|
|
||||||
import { defaultConfigProvider } from '../config-provider';
|
|
||||||
import type { VueNode } from '../_util/type';
|
import type { VueNode } from '../_util/type';
|
||||||
import { tuple, withInstall } from '../_util/type';
|
import { withInstall } from '../_util/type';
|
||||||
import type { RenderEmptyHandler } from '../config-provider/renderEmpty';
|
|
||||||
import { useInjectFormItemContext } from '../form/FormItemContext';
|
|
||||||
import omit from '../_util/omit';
|
import omit from '../_util/omit';
|
||||||
|
import { computed, defineComponent, ref, watchEffect } from 'vue';
|
||||||
|
import type { ExtractPropTypes, PropType } from 'vue';
|
||||||
|
import PropTypes from '../_util/vue-types';
|
||||||
|
import { initDefaultProps } from '../_util/props-util';
|
||||||
|
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
import type { SizeType } from '../config-provider';
|
||||||
|
import devWarning from '../vc-util/devWarning';
|
||||||
import { getTransitionName } from '../_util/transition';
|
import { getTransitionName } from '../_util/transition';
|
||||||
|
import { useInjectFormItemContext } from '../form';
|
||||||
|
import type { ValueType } from '../vc-cascader2/Cascader';
|
||||||
|
|
||||||
export interface CascaderOptionType {
|
// Align the design since we use `rc-select` in root. This help:
|
||||||
value?: string | number;
|
// - List search content will show all content
|
||||||
label?: VueNode;
|
// - Hover opacity style
|
||||||
disabled?: boolean;
|
// - Search filter match case
|
||||||
|
|
||||||
|
export { BaseOptionType, DefaultOptionType };
|
||||||
|
|
||||||
|
export type FieldNamesType = FieldNames;
|
||||||
|
|
||||||
|
export type FilledFieldNamesType = Required<FieldNamesType>;
|
||||||
|
|
||||||
|
function highlightKeyword(str: string, lowerKeyword: string, prefixCls: string | undefined) {
|
||||||
|
const cells = str
|
||||||
|
.toLowerCase()
|
||||||
|
.split(lowerKeyword)
|
||||||
|
.reduce((list, cur, index) => (index === 0 ? [cur] : [...list, lowerKeyword, cur]), []);
|
||||||
|
const fillCells: VueNode[] = [];
|
||||||
|
let start = 0;
|
||||||
|
|
||||||
|
cells.forEach((cell, index) => {
|
||||||
|
const end = start + cell.length;
|
||||||
|
let originWorld: VueNode = str.slice(start, end);
|
||||||
|
start = end;
|
||||||
|
|
||||||
|
if (index % 2 === 1) {
|
||||||
|
originWorld = (
|
||||||
|
<span class={`${prefixCls}-menu-item-keyword`} key="seperator">
|
||||||
|
{originWorld}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fillCells.push(originWorld);
|
||||||
|
});
|
||||||
|
|
||||||
|
return fillCells;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSearchRender: ShowSearchType['render'] = ({
|
||||||
|
inputValue,
|
||||||
|
path,
|
||||||
|
prefixCls,
|
||||||
|
fieldNames,
|
||||||
|
}) => {
|
||||||
|
const optionList: VueNode[] = [];
|
||||||
|
|
||||||
|
// We do lower here to save perf
|
||||||
|
const lower = inputValue.toLowerCase();
|
||||||
|
|
||||||
|
path.forEach((node, index) => {
|
||||||
|
if (index !== 0) {
|
||||||
|
optionList.push(' / ');
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = (node as any)[fieldNames.label!];
|
||||||
|
const type = typeof label;
|
||||||
|
if (type === 'string' || type === 'number') {
|
||||||
|
label = highlightKeyword(String(label), lower, prefixCls);
|
||||||
|
}
|
||||||
|
|
||||||
|
optionList.push(label);
|
||||||
|
});
|
||||||
|
return optionList;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CascaderOptionType extends DefaultOptionType {
|
||||||
isLeaf?: boolean;
|
isLeaf?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
children?: CascaderOptionType[];
|
children?: CascaderOptionType[];
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
export function cascaderProps<DataNodeType extends CascaderOptionType = CascaderOptionType>() {
|
||||||
|
return {
|
||||||
|
...omit(vcCascaderProps(), ['customSlots', 'checkable', 'options']),
|
||||||
|
multiple: { type: Boolean, default: undefined },
|
||||||
|
size: String as PropType<SizeType>,
|
||||||
|
bordered: { type: Boolean, default: undefined },
|
||||||
|
|
||||||
export interface FieldNamesType {
|
suffixIcon: PropTypes.any,
|
||||||
value?: string;
|
options: Array as PropType<DataNodeType[]>,
|
||||||
label?: string;
|
'onUpdate:value': Function as PropType<(value: ValueType) => void>,
|
||||||
children?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FilledFieldNamesType {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
children: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// const CascaderOptionType = PropTypes.shape({
|
|
||||||
// value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
// label: PropTypes.any,
|
|
||||||
// disabled: PropTypes.looseBool,
|
|
||||||
// children: PropTypes.array,
|
|
||||||
// key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
// }).loose;
|
|
||||||
|
|
||||||
// const FieldNamesType = PropTypes.shape({
|
|
||||||
// value: PropTypes.string.isRequired,
|
|
||||||
// label: PropTypes.string.isRequired,
|
|
||||||
// children: PropTypes.string,
|
|
||||||
// }).loose;
|
|
||||||
|
|
||||||
export interface ShowSearchType {
|
|
||||||
filter?: (inputValue: string, path: CascaderOptionType[], names: FilledFieldNamesType) => boolean;
|
|
||||||
render?: (
|
|
||||||
inputValue: string,
|
|
||||||
path: CascaderOptionType[],
|
|
||||||
prefixCls: string | undefined,
|
|
||||||
names: FilledFieldNamesType,
|
|
||||||
) => VueNode;
|
|
||||||
sort?: (
|
|
||||||
a: CascaderOptionType[],
|
|
||||||
b: CascaderOptionType[],
|
|
||||||
inputValue: string,
|
|
||||||
names: FilledFieldNamesType,
|
|
||||||
) => number;
|
|
||||||
matchInputWidth?: boolean;
|
|
||||||
limit?: number | false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmptyFilteredOptionsType {
|
|
||||||
disabled: boolean;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FilteredOptionsType extends EmptyFilteredOptionsType {
|
|
||||||
__IS_FILTERED_OPTION: boolean;
|
|
||||||
path: CascaderOptionType[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// const ShowSearchType = PropTypes.shape({
|
|
||||||
// filter: PropTypes.func,
|
|
||||||
// render: PropTypes.func,
|
|
||||||
// sort: PropTypes.func,
|
|
||||||
// matchInputWidth: PropTypes.looseBool,
|
|
||||||
// limit: withUndefined(PropTypes.oneOfType([Boolean, Number])),
|
|
||||||
// }).loose;
|
|
||||||
function noop() {}
|
|
||||||
|
|
||||||
const cascaderProps = {
|
|
||||||
/** 可选项数据源 */
|
|
||||||
options: { type: Array as PropType<CascaderOptionType[]>, default: [] },
|
|
||||||
/** 默认的选中项 */
|
|
||||||
defaultValue: PropTypes.array,
|
|
||||||
/** 指定选中项 */
|
|
||||||
value: PropTypes.array,
|
|
||||||
/** 选择完成后的回调 */
|
|
||||||
// onChange?: (value: string[], selectedOptions?: CascaderOptionType[]) => void;
|
|
||||||
/** 选择后展示的渲染函数 */
|
|
||||||
displayRender: PropTypes.func,
|
|
||||||
transitionName: PropTypes.string,
|
|
||||||
popupStyle: PropTypes.object.def(() => ({})),
|
|
||||||
/** 自定义浮层类名 */
|
|
||||||
popupClassName: PropTypes.string,
|
|
||||||
/** 浮层预设位置:`bottomLeft` `bottomRight` `topLeft` `topRight` */
|
|
||||||
popupPlacement: PropTypes.oneOf(tuple('bottomLeft', 'bottomRight', 'topLeft', 'topRight')).def(
|
|
||||||
'bottomLeft',
|
|
||||||
),
|
|
||||||
/** 输入框占位文本*/
|
|
||||||
placeholder: PropTypes.string.def('Please select'),
|
|
||||||
/** 输入框大小,可选 `large` `default` `small` */
|
|
||||||
size: PropTypes.oneOf(tuple('large', 'default', 'small')),
|
|
||||||
/** 禁用*/
|
|
||||||
disabled: PropTypes.looseBool.def(false),
|
|
||||||
/** 是否支持清除*/
|
|
||||||
allowClear: PropTypes.looseBool.def(true),
|
|
||||||
showSearch: {
|
|
||||||
type: [Boolean, Object] as PropType<boolean | ShowSearchType | undefined>,
|
|
||||||
default: undefined as PropType<boolean | ShowSearchType | undefined>,
|
|
||||||
},
|
|
||||||
notFoundContent: PropTypes.any,
|
|
||||||
loadData: PropTypes.func,
|
|
||||||
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
|
|
||||||
expandTrigger: PropTypes.oneOf(tuple('click', 'hover')),
|
|
||||||
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
|
|
||||||
changeOnSelect: PropTypes.looseBool,
|
|
||||||
/** 浮层可见变化时回调 */
|
|
||||||
// onPopupVisibleChange?: (popupVisible: boolean) => void;
|
|
||||||
prefixCls: PropTypes.string,
|
|
||||||
inputPrefixCls: PropTypes.string,
|
|
||||||
getPopupContainer: PropTypes.func,
|
|
||||||
popupVisible: PropTypes.looseBool,
|
|
||||||
fieldNames: { type: Object as PropType<FieldNamesType> },
|
|
||||||
autofocus: PropTypes.looseBool,
|
|
||||||
suffixIcon: PropTypes.any,
|
|
||||||
showSearchRender: PropTypes.any,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
onPopupVisibleChange: PropTypes.func,
|
|
||||||
onFocus: PropTypes.func,
|
|
||||||
onBlur: PropTypes.func,
|
|
||||||
onSearch: PropTypes.func,
|
|
||||||
'onUpdate:value': PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CascaderProps = Partial<ExtractPropTypes<typeof cascaderProps>>;
|
|
||||||
|
|
||||||
// We limit the filtered item count by default
|
|
||||||
const defaultLimit = 50;
|
|
||||||
|
|
||||||
function defaultFilterOption(
|
|
||||||
inputValue: string,
|
|
||||||
path: CascaderOptionType[],
|
|
||||||
names: FilledFieldNamesType,
|
|
||||||
) {
|
|
||||||
return path.some(option => option[names.label].indexOf(inputValue) > -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultSortFilteredOption(
|
|
||||||
a: CascaderOptionType[],
|
|
||||||
b: CascaderOptionType[],
|
|
||||||
inputValue: string,
|
|
||||||
names: FilledFieldNamesType,
|
|
||||||
) {
|
|
||||||
function callback(elem: CascaderOptionType) {
|
|
||||||
return elem[names.label].indexOf(inputValue) > -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.findIndex(callback) - b.findIndex(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilledFieldNames(props: any) {
|
|
||||||
const fieldNames = (props.fieldNames || {}) as FieldNamesType;
|
|
||||||
const names: FilledFieldNamesType = {
|
|
||||||
children: fieldNames.children || 'children',
|
|
||||||
label: fieldNames.label || 'label',
|
|
||||||
value: fieldNames.value || 'value',
|
|
||||||
};
|
};
|
||||||
return names;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenTree(
|
export type CascaderProps = Partial<ExtractPropTypes<ReturnType<typeof cascaderProps>>>;
|
||||||
options: CascaderOptionType[],
|
|
||||||
props: any,
|
|
||||||
ancestor: CascaderOptionType[] = [],
|
|
||||||
) {
|
|
||||||
const names: FilledFieldNamesType = getFilledFieldNames(props);
|
|
||||||
let flattenOptions = [];
|
|
||||||
const childrenName = names.children;
|
|
||||||
options.forEach(option => {
|
|
||||||
const path = ancestor.concat(option);
|
|
||||||
if (props.changeOnSelect || !option[childrenName] || !option[childrenName].length) {
|
|
||||||
flattenOptions.push(path);
|
|
||||||
}
|
|
||||||
if (option[childrenName]) {
|
|
||||||
flattenOptions = flattenOptions.concat(flattenTree(option[childrenName], props, path));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return flattenOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultDisplayRender = ({ labels }) => labels.join(' / ');
|
export interface CascaderRef {
|
||||||
|
focus: () => void;
|
||||||
|
blur: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const Cascader = defineComponent({
|
const Cascader = defineComponent({
|
||||||
name: 'ACascader',
|
name: 'ACascader',
|
||||||
mixins: [BaseMixin],
|
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: cascaderProps,
|
props: initDefaultProps(cascaderProps(), {
|
||||||
setup() {
|
bordered: true,
|
||||||
|
choiceTransitionName: '',
|
||||||
|
allowClear: true,
|
||||||
|
}),
|
||||||
|
setup(props, { attrs, expose, slots, emit }) {
|
||||||
const formItemContext = useInjectFormItemContext();
|
const formItemContext = useInjectFormItemContext();
|
||||||
return {
|
|
||||||
configProvider: inject('configProvider', defaultConfigProvider),
|
|
||||||
localeData: inject('localeData', {} as any),
|
|
||||||
cachedOptions: [],
|
|
||||||
popupRef: undefined,
|
|
||||||
input: undefined,
|
|
||||||
formItemContext,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
const { value, defaultValue, popupVisible, showSearch, options } = this.$props;
|
|
||||||
return {
|
|
||||||
sValue: (value || defaultValue || []) as any[],
|
|
||||||
inputValue: '',
|
|
||||||
inputFocused: false,
|
|
||||||
sPopupVisible: popupVisible as boolean,
|
|
||||||
flattenOptions: showSearch
|
|
||||||
? flattenTree(options as CascaderOptionType[], this.$props)
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
value(val) {
|
|
||||||
this.setState({ sValue: val || [] });
|
|
||||||
},
|
|
||||||
popupVisible(val) {
|
|
||||||
this.setState({ sPopupVisible: val });
|
|
||||||
},
|
|
||||||
options(val) {
|
|
||||||
if (this.showSearch) {
|
|
||||||
this.setState({ flattenOptions: flattenTree(val, this.$props as any) });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// model: {
|
|
||||||
// prop: 'value',
|
|
||||||
// event: 'change',
|
|
||||||
// },
|
|
||||||
created() {
|
|
||||||
provide('savePopupRef', this.savePopupRef);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
savePopupRef(ref: any) {
|
|
||||||
this.popupRef = ref;
|
|
||||||
},
|
|
||||||
highlightKeyword(str: string, keyword: string, prefixCls: string | undefined) {
|
|
||||||
return str
|
|
||||||
.split(keyword)
|
|
||||||
.map((node, index) =>
|
|
||||||
index === 0
|
|
||||||
? node
|
|
||||||
: [<span class={`${prefixCls}-menu-item-keyword`}>{keyword}</span>, node],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
defaultRenderFilteredOption(opt: {
|
|
||||||
inputValue: string;
|
|
||||||
path: CascaderOptionType[];
|
|
||||||
prefixCls: string | undefined;
|
|
||||||
names: FilledFieldNamesType;
|
|
||||||
}) {
|
|
||||||
const { inputValue, path, prefixCls, names } = opt;
|
|
||||||
return path.map((option, index) => {
|
|
||||||
const label = option[names.label];
|
|
||||||
const node =
|
|
||||||
label.indexOf(inputValue) > -1
|
|
||||||
? this.highlightKeyword(label, inputValue, prefixCls)
|
|
||||||
: label;
|
|
||||||
return index === 0 ? node : [' / ', node];
|
|
||||||
});
|
|
||||||
},
|
|
||||||
saveInput(node: any) {
|
|
||||||
this.input = node;
|
|
||||||
},
|
|
||||||
handleChange(value: any, selectedOptions: CascaderOptionType[]) {
|
|
||||||
this.setState({ inputValue: '' });
|
|
||||||
if (selectedOptions[0].__IS_FILTERED_OPTION) {
|
|
||||||
const unwrappedValue = value[0];
|
|
||||||
const unwrappedSelectedOptions = selectedOptions[0].path;
|
|
||||||
this.setValue(unwrappedValue, unwrappedSelectedOptions);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setValue(value, selectedOptions);
|
|
||||||
},
|
|
||||||
|
|
||||||
handlePopupVisibleChange(popupVisible: boolean) {
|
|
||||||
if (!hasProp(this, 'popupVisible')) {
|
|
||||||
this.setState((state: any) => ({
|
|
||||||
sPopupVisible: popupVisible,
|
|
||||||
inputFocused: popupVisible,
|
|
||||||
inputValue: popupVisible ? state.inputValue : '',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
this.$emit('popupVisibleChange', popupVisible);
|
|
||||||
},
|
|
||||||
handleInputFocus(e: InputEvent) {
|
|
||||||
this.$emit('focus', e);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleInputBlur(e: InputEvent) {
|
|
||||||
this.setState({
|
|
||||||
inputFocused: false,
|
|
||||||
});
|
|
||||||
this.$emit('blur', e);
|
|
||||||
this.formItemContext.onFieldBlur();
|
|
||||||
},
|
|
||||||
|
|
||||||
handleInputClick(e: MouseEvent & { nativeEvent?: any }) {
|
|
||||||
const { inputFocused, sPopupVisible } = this;
|
|
||||||
// Prevent `Trigger` behavior.
|
|
||||||
if (inputFocused || sPopupVisible) {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (e.nativeEvent && e.nativeEvent.stopImmediatePropagation) {
|
|
||||||
e.nativeEvent.stopImmediatePropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleKeyDown(e: KeyboardEvent) {
|
|
||||||
if (e.keyCode === KeyCode.BACKSPACE || e.keyCode === KeyCode.SPACE) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleInputChange(e: Event) {
|
|
||||||
const inputValue = (e.target as HTMLInputElement).value;
|
|
||||||
this.setState({ inputValue });
|
|
||||||
this.$emit('search', inputValue);
|
|
||||||
},
|
|
||||||
|
|
||||||
setValue(value: string[] | number[], selectedOptions: CascaderOptionType[] = []) {
|
|
||||||
if (!hasProp(this, 'value')) {
|
|
||||||
this.setState({ sValue: value });
|
|
||||||
}
|
|
||||||
this.$emit('update:value', value);
|
|
||||||
this.$emit('change', value, selectedOptions);
|
|
||||||
this.formItemContext.onFieldChange();
|
|
||||||
},
|
|
||||||
|
|
||||||
getLabel() {
|
|
||||||
const { options } = this;
|
|
||||||
const names = getFilledFieldNames(this.$props);
|
|
||||||
const displayRender = getComponent(this, 'displayRender', {}, false) || defaultDisplayRender;
|
|
||||||
const value = this.sValue;
|
|
||||||
const unwrappedValue = Array.isArray(value[0]) ? value[0] : value;
|
|
||||||
const selectedOptions = arrayTreeFilter<CascaderOptionType>(
|
|
||||||
options as CascaderOptionType[],
|
|
||||||
(o, level) => o[names.value] === unwrappedValue[level],
|
|
||||||
{ childrenKeyName: names.children },
|
|
||||||
);
|
|
||||||
const labels = selectedOptions.map(o => o[names.label]);
|
|
||||||
return displayRender({ labels, selectedOptions });
|
|
||||||
},
|
|
||||||
|
|
||||||
clearSelection(e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!this.inputValue) {
|
|
||||||
this.setValue([]);
|
|
||||||
this.handlePopupVisibleChange(false);
|
|
||||||
} else {
|
|
||||||
this.setState({ inputValue: '' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
generateFilteredOptions(
|
|
||||||
prefixCls: string | undefined,
|
|
||||||
renderEmpty: RenderEmptyHandler,
|
|
||||||
): EmptyFilteredOptionsType[] | FilteredOptionsType[] {
|
|
||||||
const { showSearch, notFoundContent } = this;
|
|
||||||
const names: FilledFieldNamesType = getFilledFieldNames(this.$props);
|
|
||||||
const {
|
|
||||||
filter = defaultFilterOption,
|
|
||||||
// render = this.defaultRenderFilteredOption,
|
|
||||||
sort = defaultSortFilteredOption,
|
|
||||||
limit = defaultLimit,
|
|
||||||
} = showSearch as ShowSearchType;
|
|
||||||
const render =
|
|
||||||
(showSearch as ShowSearchType).render ||
|
|
||||||
getComponent(this, 'showSearchRender') ||
|
|
||||||
this.defaultRenderFilteredOption;
|
|
||||||
const { flattenOptions = [], inputValue } = this.$data;
|
|
||||||
|
|
||||||
// Limit the filter if needed
|
|
||||||
let filtered: Array<CascaderOptionType[]>;
|
|
||||||
if (limit > 0) {
|
|
||||||
filtered = [];
|
|
||||||
let matchCount = 0;
|
|
||||||
|
|
||||||
// Perf optimization to filter items only below the limit
|
|
||||||
flattenOptions.some(path => {
|
|
||||||
const match = filter(inputValue, path, names);
|
|
||||||
if (match) {
|
|
||||||
filtered.push(path);
|
|
||||||
matchCount += 1;
|
|
||||||
}
|
|
||||||
return matchCount >= limit;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
warning(
|
|
||||||
typeof limit !== 'number',
|
|
||||||
'Cascader',
|
|
||||||
"'limit' of showSearch in Cascader should be positive number or false.",
|
|
||||||
);
|
|
||||||
filtered = flattenOptions.filter(path => filter(inputValue, path, names));
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered.sort((a, b) => sort(a, b, inputValue, names));
|
|
||||||
|
|
||||||
if (filtered.length > 0) {
|
|
||||||
return filtered.map(path => {
|
|
||||||
return {
|
|
||||||
__IS_FILTERED_OPTION: true,
|
|
||||||
path,
|
|
||||||
[names.label]: render({ inputValue, path, prefixCls, names }),
|
|
||||||
[names.value]: path.map(o => o[names.value]),
|
|
||||||
disabled: path.some(o => !!o.disabled),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
[names.label]: notFoundContent || renderEmpty('Cascader'),
|
|
||||||
[names.value]: 'ANT_CASCADER_NOT_FOUND',
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.input && this.input.focus();
|
|
||||||
},
|
|
||||||
|
|
||||||
blur() {
|
|
||||||
this.input && this.input.blur();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { sPopupVisible, inputValue, configProvider, localeData } = this;
|
|
||||||
const { sValue: value, inputFocused } = this.$data;
|
|
||||||
const props = getOptionProps(this);
|
|
||||||
let suffixIcon = getComponent(this, 'suffixIcon');
|
|
||||||
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon;
|
|
||||||
const { getPopupContainer: getContextPopupContainer } = configProvider;
|
|
||||||
const {
|
const {
|
||||||
prefixCls: customizePrefixCls,
|
prefixCls: cascaderPrefixCls,
|
||||||
inputPrefixCls: customizeInputPrefixCls,
|
rootPrefixCls,
|
||||||
placeholder = localeData.placeholder,
|
getPrefixCls,
|
||||||
size,
|
direction,
|
||||||
disabled,
|
|
||||||
allowClear,
|
|
||||||
showSearch = false,
|
|
||||||
notFoundContent,
|
|
||||||
...otherProps
|
|
||||||
} = props as any;
|
|
||||||
const { onEvents, extraAttrs } = splitAttrs(this.$attrs);
|
|
||||||
const {
|
|
||||||
class: className,
|
|
||||||
style,
|
|
||||||
id = this.formItemContext.id.value,
|
|
||||||
...restAttrs
|
|
||||||
} = extraAttrs;
|
|
||||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
|
||||||
const renderEmpty = this.configProvider.renderEmpty;
|
|
||||||
const rootPrefixCls = getPrefixCls();
|
|
||||||
const prefixCls = getPrefixCls('cascader', customizePrefixCls);
|
|
||||||
const inputPrefixCls = getPrefixCls('input', customizeInputPrefixCls);
|
|
||||||
|
|
||||||
const sizeCls = classNames({
|
|
||||||
[`${inputPrefixCls}-lg`]: size === 'large',
|
|
||||||
[`${inputPrefixCls}-sm`]: size === 'small',
|
|
||||||
});
|
|
||||||
const clearIcon =
|
|
||||||
(allowClear && !disabled && value.length > 0) || inputValue ? (
|
|
||||||
<CloseCircleFilled
|
|
||||||
class={`${prefixCls}-picker-clear`}
|
|
||||||
onClick={this.clearSelection}
|
|
||||||
key="clear-icon"
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
const arrowCls = classNames({
|
|
||||||
[`${prefixCls}-picker-arrow`]: true,
|
|
||||||
[`${prefixCls}-picker-arrow-expand`]: sPopupVisible,
|
|
||||||
});
|
|
||||||
const pickerCls = classNames(className, `${prefixCls}-picker`, {
|
|
||||||
[`${prefixCls}-picker-with-value`]: inputValue,
|
|
||||||
[`${prefixCls}-picker-disabled`]: disabled,
|
|
||||||
[`${prefixCls}-picker-${size}`]: !!size,
|
|
||||||
[`${prefixCls}-picker-show-search`]: !!showSearch,
|
|
||||||
[`${prefixCls}-picker-focused`]: inputFocused,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fix bug of https://github.com/facebook/react/pull/5004
|
|
||||||
// and https://fb.me/react-unknown-prop
|
|
||||||
const tempInputProps = omit(otherProps, [
|
|
||||||
'popupStyle',
|
|
||||||
'options',
|
|
||||||
'popupPlacement',
|
|
||||||
'transitionName',
|
|
||||||
'displayRender',
|
|
||||||
'changeOnSelect',
|
|
||||||
'expandTrigger',
|
|
||||||
'popupVisible',
|
|
||||||
'getPopupContainer',
|
|
||||||
'loadData',
|
|
||||||
'popupClassName',
|
|
||||||
'filterOption',
|
|
||||||
'renderFilteredOption',
|
|
||||||
'sortFilteredOption',
|
|
||||||
'notFoundContent',
|
|
||||||
'defaultValue',
|
|
||||||
'fieldNames',
|
|
||||||
'onChange',
|
|
||||||
'onPopupVisibleChange',
|
|
||||||
'onFocus',
|
|
||||||
'onBlur',
|
|
||||||
'onSearch',
|
|
||||||
'onUpdate:value',
|
|
||||||
]);
|
|
||||||
|
|
||||||
let options = props.options;
|
|
||||||
const names = getFilledFieldNames(this.$props);
|
|
||||||
if (options && options.length > 0) {
|
|
||||||
if (inputValue) {
|
|
||||||
options = this.generateFilteredOptions(prefixCls, renderEmpty);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
options = [
|
|
||||||
{
|
|
||||||
[names.label]: notFoundContent || renderEmpty('Cascader'),
|
|
||||||
[names.value]: 'ANT_CASCADER_NOT_FOUND',
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dropdown menu should keep previous status until it is fully closed.
|
|
||||||
if (!sPopupVisible) {
|
|
||||||
options = this.cachedOptions;
|
|
||||||
} else {
|
|
||||||
this.cachedOptions = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropdownMenuColumnStyle: CSSProperties = {};
|
|
||||||
const isNotFound =
|
|
||||||
(options || []).length === 1 && options[0].value === 'ANT_CASCADER_NOT_FOUND';
|
|
||||||
if (isNotFound) {
|
|
||||||
dropdownMenuColumnStyle.height = 'auto'; // Height of one row.
|
|
||||||
}
|
|
||||||
// The default value of `matchInputWidth` is `true`
|
|
||||||
const resultListMatchInputWidth = showSearch.matchInputWidth !== false;
|
|
||||||
if (resultListMatchInputWidth && (inputValue || isNotFound) && this.input) {
|
|
||||||
dropdownMenuColumnStyle.width = findDOMNode(this.input.input).offsetWidth + 'px';
|
|
||||||
}
|
|
||||||
// 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,
|
getPopupContainer,
|
||||||
options,
|
renderEmpty,
|
||||||
prefixCls,
|
size,
|
||||||
value,
|
} = useConfigInject('cascader', props);
|
||||||
popupVisible: sPopupVisible,
|
const prefixCls = computed(() => getPrefixCls('select', props.prefixCls));
|
||||||
dropdownMenuColumnStyle,
|
const isRtl = computed(() => direction.value === 'rtl');
|
||||||
expandIcon,
|
// =================== Warning =====================
|
||||||
loadingIcon,
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
...onEvents,
|
watchEffect(() => {
|
||||||
onPopupVisibleChange: this.handlePopupVisibleChange,
|
devWarning(
|
||||||
onChange: this.handleChange,
|
props.popupClassName === undefined,
|
||||||
transitionName: getTransitionName(rootPrefixCls, 'slide-up', props.transitionName),
|
'Cascader',
|
||||||
|
'`popupClassName` is deprecated. Please use `dropdownClassName` instead.',
|
||||||
|
);
|
||||||
|
devWarning(
|
||||||
|
!props.multiple || !props.displayRender || !slots.displayRender,
|
||||||
|
'Cascader',
|
||||||
|
'`displayRender` not work on `multiple`. Please use `tagRender` instead.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ==================== Search =====================
|
||||||
|
const mergedShowSearch = computed(() => {
|
||||||
|
if (!props.showSearch) {
|
||||||
|
return props.showSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchConfig: ShowSearchType = {
|
||||||
|
render: defaultSearchRender,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof props.showSearch === 'object') {
|
||||||
|
searchConfig = {
|
||||||
|
...searchConfig,
|
||||||
|
...props.showSearch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================== Dropdown ====================
|
||||||
|
const mergedDropdownClassName = computed(() =>
|
||||||
|
classNames(
|
||||||
|
props.dropdownClassName || props.popupClassName,
|
||||||
|
`${cascaderPrefixCls.value}-dropdown`,
|
||||||
|
{
|
||||||
|
[`${cascaderPrefixCls.value}-dropdown-rtl`]: isRtl.value,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectRef = ref<CascaderRef>();
|
||||||
|
expose({
|
||||||
|
focus() {
|
||||||
|
selectRef.value?.focus();
|
||||||
|
},
|
||||||
|
blur() {
|
||||||
|
selectRef.value?.blur();
|
||||||
|
},
|
||||||
|
} as CascaderRef);
|
||||||
|
|
||||||
|
const handleChange: CascaderProps['onChange'] = (...args) => {
|
||||||
|
emit('update:value', args[0]);
|
||||||
|
emit('change', ...args);
|
||||||
|
formItemContext.onFieldChange();
|
||||||
|
};
|
||||||
|
const handleBlur: CascaderProps['onBlur'] = (...args) => {
|
||||||
|
emit('blur', ...args);
|
||||||
|
formItemContext.onFieldBlur();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
notFoundContent = slots.notFoundContent?.(),
|
||||||
|
expandIcon = slots.expandIcon?.(),
|
||||||
|
multiple,
|
||||||
|
bordered,
|
||||||
|
allowClear,
|
||||||
|
choiceTransitionName,
|
||||||
|
transitionName,
|
||||||
|
id = formItemContext.id.value,
|
||||||
|
...restProps
|
||||||
|
} = props;
|
||||||
|
// =================== No Found ====================
|
||||||
|
const mergedNotFoundContent = notFoundContent || renderEmpty.value('Cascader');
|
||||||
|
|
||||||
|
// ===================== Icon ======================
|
||||||
|
let mergedExpandIcon = expandIcon;
|
||||||
|
if (!expandIcon) {
|
||||||
|
mergedExpandIcon = isRtl.value ? <LeftOutlined /> : <RightOutlined />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingIcon = (
|
||||||
|
<span class={`${prefixCls.value}-menu-item-loading-icon`}>
|
||||||
|
<RedoOutlined spin />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===================== Icons =====================
|
||||||
|
const { suffixIcon, removeIcon, clearIcon } = getIcons(
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
multiple,
|
||||||
|
prefixCls: prefixCls.value,
|
||||||
|
},
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<VcCascader
|
||||||
|
{...restProps}
|
||||||
|
{...attrs}
|
||||||
|
id={id}
|
||||||
|
prefixCls={prefixCls.value}
|
||||||
|
class={[
|
||||||
|
cascaderPrefixCls.value,
|
||||||
|
{
|
||||||
|
[`${prefixCls.value}-lg`]: size.value === 'large',
|
||||||
|
[`${prefixCls.value}-sm`]: size.value === 'small',
|
||||||
|
[`${prefixCls.value}-rtl`]: isRtl.value,
|
||||||
|
[`${prefixCls.value}-borderless`]: !bordered,
|
||||||
|
},
|
||||||
|
attrs.class,
|
||||||
|
]}
|
||||||
|
direction={direction.value}
|
||||||
|
notFoundContent={mergedNotFoundContent}
|
||||||
|
allowClear={allowClear}
|
||||||
|
showSearch={mergedShowSearch.value}
|
||||||
|
expandIcon={mergedExpandIcon}
|
||||||
|
inputIcon={suffixIcon}
|
||||||
|
removeIcon={removeIcon}
|
||||||
|
clearIcon={clearIcon}
|
||||||
|
loadingIcon={loadingIcon}
|
||||||
|
checkable={!!multiple}
|
||||||
|
dropdownClassName={mergedDropdownClassName.value}
|
||||||
|
dropdownPrefixCls={cascaderPrefixCls.value}
|
||||||
|
choiceTransitionName={getTransitionName(rootPrefixCls.value, '', choiceTransitionName)}
|
||||||
|
transitionName={getTransitionName(rootPrefixCls.value, 'slide-up', transitionName)}
|
||||||
|
getPopupContainer={getPopupContainer.value}
|
||||||
|
customSlots={{
|
||||||
|
...slots,
|
||||||
|
checkable: () => <span class={`${cascaderPrefixCls.value}-checkbox-inner`} />,
|
||||||
|
}}
|
||||||
|
displayRender={props.displayRender || slots.displayRender}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
v-slots={slots}
|
||||||
|
ref={selectRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
return <VcCascader {...cascaderProps}>{input}</VcCascader>;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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/themes/index';
|
||||||
@import '../../style/mixins/index';
|
@import '../../style/mixins/index';
|
||||||
@import '../../input/style/mixin';
|
@import '../../input/style/mixin';
|
||||||
|
@import '../../checkbox/style/mixin';
|
||||||
|
|
||||||
@cascader-prefix-cls: ~'@{ant-prefix}-cascader';
|
@cascader-prefix-cls: ~'@{ant-prefix}-cascader';
|
||||||
|
|
||||||
|
.antCheckboxFn(@checkbox-prefix-cls: ~'@{cascader-prefix-cls}-checkbox');
|
||||||
|
|
||||||
.@{cascader-prefix-cls} {
|
.@{cascader-prefix-cls} {
|
||||||
.reset-component();
|
width: 184px;
|
||||||
|
|
||||||
&-input.@{ant-prefix}-input {
|
&-checkbox {
|
||||||
// Keep it static for https://github.com/ant-design/ant-design/issues/16738
|
top: 0;
|
||||||
position: static;
|
margin-right: @padding-xs;
|
||||||
width: 100%;
|
|
||||||
// https://github.com/ant-design/ant-design/issues/17582
|
|
||||||
padding-right: 24px;
|
|
||||||
// Add important to fix https://github.com/ant-design/ant-design/issues/5078
|
|
||||||
// because input.less will compile after cascader.less
|
|
||||||
background-color: transparent !important;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-picker-show-search &-input.@{ant-prefix}-input {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-picker {
|
|
||||||
.reset-component();
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: @cascader-bg;
|
|
||||||
border-radius: @border-radius-base;
|
|
||||||
outline: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.3s;
|
|
||||||
|
|
||||||
&-with-value &-label {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-disabled {
|
|
||||||
color: @disabled-color;
|
|
||||||
background: @input-disabled-bg;
|
|
||||||
cursor: not-allowed;
|
|
||||||
.@{cascader-prefix-cls}-input {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus .@{cascader-prefix-cls}-input {
|
|
||||||
.active();
|
|
||||||
}
|
|
||||||
|
|
||||||
&-show-search&-focused {
|
|
||||||
color: @disabled-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 20px;
|
|
||||||
margin-top: -10px;
|
|
||||||
padding: 0 20px 0 @control-padding-horizontal;
|
|
||||||
overflow: hidden;
|
|
||||||
line-height: 20px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-clear {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: @control-padding-horizontal;
|
|
||||||
z-index: 2;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
margin-top: -6px;
|
|
||||||
color: @disabled-color;
|
|
||||||
font-size: @font-size-sm;
|
|
||||||
line-height: 12px;
|
|
||||||
background: @component-background;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
transition: color 0.3s ease, opacity 0.15s ease;
|
|
||||||
&:hover {
|
|
||||||
color: @text-color-secondary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover &-clear {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// arrow
|
|
||||||
&-arrow {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: @control-padding-horizontal;
|
|
||||||
z-index: 1;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
margin-top: -6px;
|
|
||||||
color: @disabled-color;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 12px;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
&&-expand {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/ant-design/ant-design/pull/12407#issuecomment-424657810
|
|
||||||
&-picker-label:hover + &-input {
|
|
||||||
.hover();
|
|
||||||
}
|
|
||||||
|
|
||||||
&-picker-small &-picker-clear,
|
|
||||||
&-picker-small &-picker-arrow {
|
|
||||||
right: @control-padding-horizontal-sm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-menus {
|
&-menus {
|
||||||
position: absolute;
|
display: flex;
|
||||||
z-index: @zindex-dropdown;
|
flex-wrap: nowrap;
|
||||||
font-size: @cascader-dropdown-font-size;
|
align-items: flex-start;
|
||||||
white-space: nowrap;
|
|
||||||
background: @cascader-menu-bg;
|
|
||||||
border-radius: @border-radius-base;
|
|
||||||
box-shadow: @box-shadow-base;
|
|
||||||
|
|
||||||
ul,
|
&.@{cascader-prefix-cls}-menu-empty {
|
||||||
ol {
|
.@{cascader-prefix-cls}-menu {
|
||||||
margin: 0;
|
width: 100%;
|
||||||
list-style: none;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-empty,
|
|
||||||
&-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.slide-up-enter.slide-up-enter-active&-placement-bottomLeft,
|
|
||||||
&.slide-up-appear.slide-up-appear-active&-placement-bottomLeft {
|
|
||||||
animation-name: antSlideUpIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.slide-up-enter.slide-up-enter-active&-placement-topLeft,
|
|
||||||
&.slide-up-appear.slide-up-appear-active&-placement-topLeft {
|
|
||||||
animation-name: antSlideDownIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.slide-up-leave.slide-up-leave-active&-placement-bottomLeft {
|
|
||||||
animation-name: antSlideUpOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.slide-up-leave.slide-up-leave-active&-placement-topLeft {
|
|
||||||
animation-name: antSlideDownOut;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-menu {
|
&-menu {
|
||||||
display: inline-block;
|
|
||||||
min-width: 111px;
|
min-width: 111px;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin: -@dropdown-edge-child-vertical-padding 0;
|
||||||
padding: @cascader-dropdown-edge-child-vertical-padding 0;
|
padding: @cascader-dropdown-edge-child-vertical-padding 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@ -171,60 +40,65 @@
|
||||||
border-right: @border-width-base @border-style-base @cascader-menu-border-color-split;
|
border-right: @border-width-base @border-style-base @cascader-menu-border-color-split;
|
||||||
-ms-overflow-style: -ms-autohiding-scrollbar; // https://github.com/ant-design/ant-design/issues/11857
|
-ms-overflow-style: -ms-autohiding-scrollbar; // https://github.com/ant-design/ant-design/issues/11857
|
||||||
|
|
||||||
&:first-child {
|
&-item {
|
||||||
border-radius: @border-radius-base 0 0 @border-radius-base;
|
display: flex;
|
||||||
}
|
flex-wrap: nowrap;
|
||||||
&:last-child {
|
align-items: center;
|
||||||
margin-right: -1px;
|
padding: @cascader-dropdown-vertical-padding @control-padding-horizontal;
|
||||||
border-right-color: transparent;
|
overflow: hidden;
|
||||||
border-radius: 0 @border-radius-base @border-radius-base 0;
|
line-height: @cascader-dropdown-line-height;
|
||||||
}
|
white-space: nowrap;
|
||||||
&:only-child {
|
text-overflow: ellipsis;
|
||||||
border-radius: @border-radius-base;
|
cursor: pointer;
|
||||||
}
|
transition: all 0.3s;
|
||||||
}
|
|
||||||
&-menu-item {
|
|
||||||
padding: @cascader-dropdown-vertical-padding @control-padding-horizontal;
|
|
||||||
line-height: @cascader-dropdown-line-height;
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
&:hover {
|
|
||||||
background: @item-hover-bg;
|
|
||||||
}
|
|
||||||
&-disabled {
|
|
||||||
color: @disabled-color;
|
|
||||||
cursor: not-allowed;
|
|
||||||
&:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&-active:not(&-disabled) {
|
|
||||||
&,
|
|
||||||
&:hover {
|
|
||||||
font-weight: @select-item-selected-font-weight;
|
|
||||||
background-color: @cascader-item-selected-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&-expand {
|
|
||||||
position: relative;
|
|
||||||
padding-right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-expand &-expand-icon,
|
&:hover {
|
||||||
&-loading-icon {
|
background: @item-hover-bg;
|
||||||
.iconfont-size-under-12px(10px);
|
}
|
||||||
|
|
||||||
position: absolute;
|
&-disabled {
|
||||||
right: @control-padding-horizontal;
|
|
||||||
color: @text-color-secondary;
|
|
||||||
.@{cascader-prefix-cls}-menu-item-disabled& {
|
|
||||||
color: @disabled-color;
|
color: @disabled-color;
|
||||||
}
|
cursor: not-allowed;
|
||||||
}
|
|
||||||
|
|
||||||
& &-keyword {
|
&:hover {
|
||||||
color: @highlight-color;
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{cascader-prefix-cls}-menu-empty & {
|
||||||
|
color: @disabled-color;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-active:not(&-disabled) {
|
||||||
|
&,
|
||||||
|
&:hover {
|
||||||
|
font-weight: @select-item-selected-font-weight;
|
||||||
|
background-color: @cascader-item-selected-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-expand &-expand-icon,
|
||||||
|
&-loading-icon {
|
||||||
|
margin-left: @padding-xss;
|
||||||
|
color: @text-color-secondary;
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
.@{cascader-prefix-cls}-menu-item-disabled& {
|
||||||
|
color: @disabled-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-keyword {
|
||||||
|
color: @highlight-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import './rtl';
|
||||||
|
|
|
@ -3,4 +3,4 @@ import './index.less';
|
||||||
|
|
||||||
// style dependencies
|
// style dependencies
|
||||||
import '../../empty/style';
|
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> {
|
export interface ShowSearchType<OptionType extends BaseOptionType = DefaultOptionType> {
|
||||||
filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean;
|
filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean;
|
||||||
render?: (arg?: {
|
render?: (arg?: {
|
||||||
|
@ -30,100 +54,110 @@ export interface BaseOptionType {
|
||||||
[name: string]: any;
|
[name: string]: any;
|
||||||
}
|
}
|
||||||
export interface DefaultOptionType extends BaseOptionType {
|
export interface DefaultOptionType extends BaseOptionType {
|
||||||
label: React.ReactNode;
|
label?: any;
|
||||||
value?: string | number | null;
|
value?: string | number | null;
|
||||||
children?: DefaultOptionType[];
|
children?: DefaultOptionType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>
|
function baseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
|
||||||
extends Omit<
|
return {
|
||||||
BaseSelectPropsWithoutPrivate,
|
...omit(baseSelectPropsWithoutPrivate(), ['tokenSeparators', 'mode', 'showSearch']),
|
||||||
'tokenSeparators' | 'labelInValue' | 'mode' | 'showSearch'
|
// MISC
|
||||||
> {
|
id: String,
|
||||||
// MISC
|
prefixCls: String,
|
||||||
id?: string;
|
fieldNames: Object as PropType<FieldNames>,
|
||||||
prefixCls?: string;
|
children: Array as PropType<VueNode[]>,
|
||||||
fieldNames?: FieldNames;
|
|
||||||
children?: React.ReactElement;
|
|
||||||
|
|
||||||
// Value
|
// Value
|
||||||
value?: ValueType;
|
value: { type: [String, Number, Array] as PropType<ValueType> },
|
||||||
defaultValue?: ValueType;
|
defaultValue: { type: [String, Number, Array] as PropType<ValueType> },
|
||||||
changeOnSelect?: boolean;
|
changeOnSelect: { type: Boolean, default: undefined },
|
||||||
onChange?: (value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void;
|
onChange: Function as PropType<
|
||||||
displayRender?: (label: string[], selectedOptions?: OptionType[]) => React.ReactNode;
|
(value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void
|
||||||
checkable?: boolean | React.ReactNode;
|
>,
|
||||||
|
displayRender: Function as PropType<
|
||||||
|
(opt: { labels: string[]; selectedOptions?: OptionType[] }) => any
|
||||||
|
>,
|
||||||
|
checkable: { type: Boolean, default: undefined },
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
showSearch?: boolean | ShowSearchType<OptionType>;
|
showSearch: { type: [Boolean, Object] as PropType<boolean | ShowSearchType<OptionType>> },
|
||||||
searchValue?: string;
|
searchValue: String,
|
||||||
onSearch?: (value: string) => void;
|
onSearch: Function as PropType<(value: string) => void>,
|
||||||
|
|
||||||
// Trigger
|
// Trigger
|
||||||
expandTrigger?: 'hover' | 'click';
|
expandTrigger: String as PropType<'hover' | 'click'>,
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
options?: OptionType[];
|
options: Array as PropType<OptionType[]>,
|
||||||
/** @private Internal usage. Do not use in your production. */
|
/** @private Internal usage. Do not use in your production. */
|
||||||
dropdownPrefixCls?: string;
|
dropdownPrefixCls: String,
|
||||||
loadData?: (selectOptions: OptionType[]) => void;
|
loadData: Function as PropType<(selectOptions: OptionType[]) => void>,
|
||||||
|
|
||||||
// Open
|
// Open
|
||||||
/** @deprecated Use `open` instead */
|
/** @deprecated Use `open` instead */
|
||||||
popupVisible?: boolean;
|
popupVisible: { type: Boolean, default: undefined },
|
||||||
|
|
||||||
/** @deprecated Use `dropdownClassName` instead */
|
/** @deprecated Use `dropdownClassName` instead */
|
||||||
popupClassName?: string;
|
popupClassName: String,
|
||||||
dropdownClassName?: string;
|
dropdownClassName: String,
|
||||||
dropdownMenuColumnStyle?: React.CSSProperties;
|
dropdownMenuColumnStyle: Object as PropType<CSSProperties>,
|
||||||
|
|
||||||
/** @deprecated Use `placement` instead */
|
/** @deprecated Use `placement` instead */
|
||||||
popupPlacement?: Placement;
|
popupPlacement: String as PropType<Placement>,
|
||||||
placement?: Placement;
|
placement: String as PropType<Placement>,
|
||||||
|
|
||||||
/** @deprecated Use `onDropdownVisibleChange` instead */
|
/** @deprecated Use `onDropdownVisibleChange` instead */
|
||||||
onPopupVisibleChange?: (open: boolean) => void;
|
onPopupVisibleChange: Function as PropType<(open: boolean) => void>,
|
||||||
onDropdownVisibleChange?: (open: boolean) => void;
|
onDropdownVisibleChange: Function as PropType<(open: boolean) => void>,
|
||||||
|
|
||||||
// Icon
|
// Icon
|
||||||
expandIcon?: React.ReactNode;
|
expandIcon: PropTypes.any,
|
||||||
loadingIcon?: React.ReactNode;
|
loadingIcon: PropTypes.any,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BaseCascaderProps = Partial<ExtractPropTypes<ReturnType<typeof baseCascaderProps>>>;
|
||||||
|
|
||||||
type OnSingleChange<OptionType> = (value: SingleValueType, selectOptions: OptionType[]) => void;
|
type OnSingleChange<OptionType> = (value: SingleValueType, selectOptions: OptionType[]) => void;
|
||||||
type OnMultipleChange<OptionType> = (
|
type OnMultipleChange<OptionType> = (
|
||||||
value: SingleValueType[],
|
value: SingleValueType[],
|
||||||
selectOptions: OptionType[][],
|
selectOptions: OptionType[][],
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export interface SingleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>
|
export function singleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
|
||||||
extends BaseCascaderProps<OptionType> {
|
return {
|
||||||
checkable?: false;
|
...baseCascaderProps(),
|
||||||
|
checkable: Boolean as PropType<false>,
|
||||||
onChange?: OnSingleChange<OptionType>;
|
onChange: Function as PropType<OnSingleChange<OptionType>>,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultipleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>
|
export type SingleCascaderProps = Partial<ExtractPropTypes<ReturnType<typeof singleCascaderProps>>>;
|
||||||
extends BaseCascaderProps<OptionType> {
|
|
||||||
checkable: true | React.ReactNode;
|
|
||||||
|
|
||||||
onChange?: OnMultipleChange<OptionType>;
|
export function multipleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
|
||||||
|
return {
|
||||||
|
...baseCascaderProps(),
|
||||||
|
checkable: Boolean as PropType<true>,
|
||||||
|
onChange: Function as PropType<OnMultipleChange<OptionType>>,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CascaderProps<OptionType extends BaseOptionType = DefaultOptionType> =
|
export type MultipleCascaderProps = Partial<
|
||||||
| SingleCascaderProps<OptionType>
|
ExtractPropTypes<ReturnType<typeof singleCascaderProps>>
|
||||||
| MultipleCascaderProps<OptionType>;
|
>;
|
||||||
|
|
||||||
type InternalCascaderProps<OptionType extends BaseOptionType = DefaultOptionType> = Omit<
|
export function internalCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>() {
|
||||||
SingleCascaderProps<OptionType> | MultipleCascaderProps<OptionType>,
|
return {
|
||||||
'onChange'
|
...baseCascaderProps(),
|
||||||
> & {
|
onChange: Function as PropType<
|
||||||
onChange?: (
|
(value: ValueType, selectOptions: OptionType[] | OptionType[][]) => void
|
||||||
value: SingleValueType | SingleValueType[],
|
>,
|
||||||
selectOptions: OptionType[] | OptionType[][],
|
customSlots: Object as PropType<Record<string, Function>>,
|
||||||
) => void;
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export type CascaderProps = Partial<ExtractPropTypes<ReturnType<typeof internalCascaderProps>>>;
|
||||||
export type CascaderRef = Omit<BaseSelectRef, 'scrollTo'>;
|
export type CascaderRef = Omit<BaseSelectRef, 'scrollTo'>;
|
||||||
|
|
||||||
function isMultipleValue(value: ValueType): value is SingleValueType[] {
|
function isMultipleValue(value: ValueType): value is SingleValueType[] {
|
||||||
|
@ -142,270 +176,242 @@ function toRawValues(value: ValueType): SingleValueType[] {
|
||||||
return value.length === 0 ? [] : [value];
|
return value.length === 0 ? [] : [value];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, ref) => {
|
export default defineComponent({
|
||||||
const {
|
name: 'Cascader',
|
||||||
// MISC
|
inheritAttrs: false,
|
||||||
id,
|
props: initDefaultProps(internalCascaderProps(), {}),
|
||||||
prefixCls = 'rc-cascader',
|
setup(props, { attrs, expose, slots }) {
|
||||||
fieldNames,
|
const mergedId = useId(toRef(props, 'id'));
|
||||||
|
const multiple = computed(() => !!props.checkable);
|
||||||
|
|
||||||
// Value
|
// =========================== Values ===========================
|
||||||
defaultValue,
|
const [rawValues, setRawValues] = useMergedState<ValueType, Ref<SingleValueType[]>>(
|
||||||
value,
|
props.defaultValue,
|
||||||
changeOnSelect,
|
{
|
||||||
onChange,
|
value: computed(() => props.value),
|
||||||
displayRender,
|
postState: toRawValues,
|
||||||
checkable,
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Search
|
// ========================= FieldNames =========================
|
||||||
searchValue,
|
const mergedFieldNames = computed(() => fillFieldNames(props.fieldNames));
|
||||||
onSearch,
|
|
||||||
showSearch,
|
|
||||||
|
|
||||||
// Trigger
|
// =========================== Option ===========================
|
||||||
expandTrigger,
|
const mergedOptions = computed(() => props.options || []);
|
||||||
|
|
||||||
// Options
|
// Only used in multiple mode, this fn will not call in single mode
|
||||||
options,
|
const pathKeyEntities = useEntities(mergedOptions, mergedFieldNames);
|
||||||
dropdownPrefixCls,
|
|
||||||
loadData,
|
|
||||||
|
|
||||||
// Open
|
/** Convert path key back to value format */
|
||||||
popupVisible,
|
const getValueByKeyPath = (pathKeys: Key[]): SingleValueType[] => {
|
||||||
open,
|
const ketPathEntities = pathKeyEntities.value;
|
||||||
|
|
||||||
popupClassName,
|
|
||||||
dropdownClassName,
|
|
||||||
dropdownMenuColumnStyle,
|
|
||||||
|
|
||||||
popupPlacement,
|
|
||||||
placement,
|
|
||||||
|
|
||||||
onDropdownVisibleChange,
|
|
||||||
onPopupVisibleChange,
|
|
||||||
|
|
||||||
// Icon
|
|
||||||
expandIcon = '>',
|
|
||||||
loadingIcon,
|
|
||||||
|
|
||||||
// Children
|
|
||||||
children,
|
|
||||||
|
|
||||||
...restProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const mergedId = useId(id);
|
|
||||||
const multiple = !!checkable;
|
|
||||||
|
|
||||||
// =========================== Values ===========================
|
|
||||||
const [rawValues, setRawValues] = useMergedState<ValueType, SingleValueType[]>(defaultValue, {
|
|
||||||
value,
|
|
||||||
postState: toRawValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========================= FieldNames =========================
|
|
||||||
const mergedFieldNames = React.useMemo(
|
|
||||||
() => fillFieldNames(fieldNames),
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
[JSON.stringify(fieldNames)],
|
|
||||||
/* eslint-enable react-hooks/exhaustive-deps */
|
|
||||||
);
|
|
||||||
|
|
||||||
// =========================== Option ===========================
|
|
||||||
const mergedOptions = React.useMemo(() => options || [], [options]);
|
|
||||||
|
|
||||||
// Only used in multiple mode, this fn will not call in single mode
|
|
||||||
const getPathKeyEntities = useEntities(mergedOptions, mergedFieldNames);
|
|
||||||
|
|
||||||
/** Convert path key back to value format */
|
|
||||||
const getValueByKeyPath = React.useCallback(
|
|
||||||
(pathKeys: React.Key[]): SingleValueType[] => {
|
|
||||||
const ketPathEntities = getPathKeyEntities();
|
|
||||||
|
|
||||||
return pathKeys.map(pathKey => {
|
return pathKeys.map(pathKey => {
|
||||||
const { nodes } = ketPathEntities[pathKey];
|
const { nodes } = ketPathEntities[pathKey];
|
||||||
|
|
||||||
return nodes.map(node => node[mergedFieldNames.value]);
|
return nodes.map(node => node[mergedFieldNames.value.value]);
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[getPathKeyEntities, mergedFieldNames],
|
|
||||||
);
|
|
||||||
|
|
||||||
// =========================== Search ===========================
|
// =========================== Search ===========================
|
||||||
const [mergedSearchValue, setSearchValue] = useMergedState('', {
|
const [mergedSearchValue, setSearchValue] = useMergedState('', {
|
||||||
value: searchValue,
|
value: computed(() => props.searchValue),
|
||||||
postState: search => search || '',
|
postState: search => search || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => {
|
const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => {
|
||||||
setSearchValue(searchText);
|
setSearchValue(searchText);
|
||||||
|
|
||||||
if (info.source !== 'blur' && onSearch) {
|
if (info.source !== 'blur' && props.onSearch) {
|
||||||
onSearch(searchText);
|
props.onSearch(searchText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch);
|
const { showSearch: mergedShowSearch, searchConfig: mergedSearchConfig } = useSearchConfig(
|
||||||
|
toRef(props, 'showSearch'),
|
||||||
|
);
|
||||||
|
|
||||||
const searchOptions = useSearchOptions(
|
const searchOptions = useSearchOptions(
|
||||||
mergedSearchValue,
|
mergedSearchValue,
|
||||||
mergedOptions,
|
mergedOptions,
|
||||||
mergedFieldNames,
|
mergedFieldNames,
|
||||||
dropdownPrefixCls || prefixCls,
|
computed(() => props.dropdownPrefixCls || props.prefixCls),
|
||||||
searchConfig,
|
mergedSearchConfig,
|
||||||
changeOnSelect,
|
toRef(props, 'changeOnSelect'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================== Values ===========================
|
// =========================== Values ===========================
|
||||||
const getMissingValues = useMissingValues(mergedOptions, mergedFieldNames);
|
const missingValuesInfo = useMissingValues(mergedOptions, mergedFieldNames, rawValues);
|
||||||
|
|
||||||
// Fill `rawValues` with checked conduction values
|
// Fill `rawValues` with checked conduction values
|
||||||
const [checkedValues, halfCheckedValues, missingCheckedValues] = React.useMemo(() => {
|
const [checkedValues, halfCheckedValues, missingCheckedValues] = [
|
||||||
const [existValues, missingValues] = getMissingValues(rawValues);
|
ref<SingleValueType[]>([]),
|
||||||
|
ref<SingleValueType[]>([]),
|
||||||
|
ref<SingleValueType[]>([]),
|
||||||
|
];
|
||||||
|
watchEffect(() => {
|
||||||
|
const [existValues, missingValues] = missingValuesInfo.value;
|
||||||
|
|
||||||
if (!multiple || !rawValues.length) {
|
if (!multiple.value || !rawValues.value.length) {
|
||||||
return [existValues, [], missingValues];
|
[checkedValues.value, halfCheckedValues.value, missingCheckedValues.value] = [
|
||||||
}
|
existValues,
|
||||||
|
[],
|
||||||
const keyPathValues = toPathKeys(existValues);
|
missingValues,
|
||||||
const ketPathEntities = getPathKeyEntities();
|
];
|
||||||
|
|
||||||
const { checkedKeys, halfCheckedKeys } = conductCheck(keyPathValues, true, ketPathEntities);
|
|
||||||
|
|
||||||
// Convert key back to value cells
|
|
||||||
return [getValueByKeyPath(checkedKeys), getValueByKeyPath(halfCheckedKeys), missingValues];
|
|
||||||
}, [multiple, rawValues, getPathKeyEntities, getValueByKeyPath, getMissingValues]);
|
|
||||||
|
|
||||||
const deDuplicatedValues = React.useMemo(() => {
|
|
||||||
const checkedKeys = toPathKeys(checkedValues);
|
|
||||||
const deduplicateKeys = formatStrategyValues(checkedKeys, getPathKeyEntities);
|
|
||||||
|
|
||||||
return [...missingCheckedValues, ...getValueByKeyPath(deduplicateKeys)];
|
|
||||||
}, [checkedValues, getPathKeyEntities, getValueByKeyPath, missingCheckedValues]);
|
|
||||||
|
|
||||||
const displayValues = useDisplayValues(
|
|
||||||
deDuplicatedValues,
|
|
||||||
mergedOptions,
|
|
||||||
mergedFieldNames,
|
|
||||||
multiple,
|
|
||||||
displayRender,
|
|
||||||
);
|
|
||||||
|
|
||||||
// =========================== Change ===========================
|
|
||||||
const triggerChange = useRefFunc((nextValues: ValueType) => {
|
|
||||||
setRawValues(nextValues);
|
|
||||||
|
|
||||||
// Save perf if no need trigger event
|
|
||||||
if (onChange) {
|
|
||||||
const nextRawValues = toRawValues(nextValues);
|
|
||||||
|
|
||||||
const valueOptions = nextRawValues.map(valueCells =>
|
|
||||||
toPathOptions(valueCells, mergedOptions, mergedFieldNames).map(valueOpt => valueOpt.option),
|
|
||||||
);
|
|
||||||
|
|
||||||
const triggerValues = multiple ? nextRawValues : nextRawValues[0];
|
|
||||||
const triggerOptions = multiple ? valueOptions : valueOptions[0];
|
|
||||||
|
|
||||||
onChange(triggerValues, triggerOptions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// =========================== Select ===========================
|
|
||||||
const onInternalSelect = useRefFunc((valuePath: SingleValueType) => {
|
|
||||||
if (!multiple) {
|
|
||||||
triggerChange(valuePath);
|
|
||||||
} else {
|
|
||||||
// Prepare conduct required info
|
|
||||||
const pathKey = toPathKey(valuePath);
|
|
||||||
const checkedPathKeys = toPathKeys(checkedValues);
|
|
||||||
const halfCheckedPathKeys = toPathKeys(halfCheckedValues);
|
|
||||||
|
|
||||||
const existInChecked = checkedPathKeys.includes(pathKey);
|
|
||||||
const existInMissing = missingCheckedValues.some(
|
|
||||||
valueCells => toPathKey(valueCells) === pathKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Do update
|
|
||||||
let nextCheckedValues = checkedValues;
|
|
||||||
let nextMissingValues = missingCheckedValues;
|
|
||||||
|
|
||||||
if (existInMissing && !existInChecked) {
|
|
||||||
// Missing value only do filter
|
|
||||||
nextMissingValues = missingCheckedValues.filter(
|
|
||||||
valueCells => toPathKey(valueCells) !== pathKey,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Update checked key first
|
|
||||||
const nextRawCheckedKeys = existInChecked
|
|
||||||
? checkedPathKeys.filter(key => key !== pathKey)
|
|
||||||
: [...checkedPathKeys, pathKey];
|
|
||||||
|
|
||||||
const pathKeyEntities = getPathKeyEntities();
|
|
||||||
|
|
||||||
// Conduction by selected or not
|
|
||||||
let checkedKeys: React.Key[];
|
|
||||||
if (existInChecked) {
|
|
||||||
({ checkedKeys } = conductCheck(
|
|
||||||
nextRawCheckedKeys,
|
|
||||||
{ checked: false, halfCheckedKeys: halfCheckedPathKeys },
|
|
||||||
pathKeyEntities,
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
({ checkedKeys } = conductCheck(nextRawCheckedKeys, true, pathKeyEntities));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roll up to parent level keys
|
|
||||||
const deDuplicatedKeys = formatStrategyValues(checkedKeys, getPathKeyEntities);
|
|
||||||
nextCheckedValues = getValueByKeyPath(deDuplicatedKeys);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerChange([...nextMissingValues, ...nextCheckedValues]);
|
const keyPathValues = toPathKeys(existValues);
|
||||||
}
|
const ketPathEntities = pathKeyEntities.value;
|
||||||
});
|
|
||||||
|
|
||||||
// Display Value change logic
|
const { checkedKeys, halfCheckedKeys } = conductCheck(keyPathValues, true, ketPathEntities);
|
||||||
const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (_, info) => {
|
|
||||||
if (info.type === 'clear') {
|
// Convert key back to value cells
|
||||||
triggerChange([]);
|
return [getValueByKeyPath(checkedKeys), getValueByKeyPath(halfCheckedKeys), missingValues];
|
||||||
return;
|
});
|
||||||
|
|
||||||
|
const deDuplicatedValues = computed(() => {
|
||||||
|
const checkedKeys = toPathKeys(checkedValues.value);
|
||||||
|
const deduplicateKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value);
|
||||||
|
|
||||||
|
return [...missingCheckedValues.value, ...getValueByKeyPath(deduplicateKeys)];
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayValues = useDisplayValues(
|
||||||
|
deDuplicatedValues,
|
||||||
|
mergedOptions,
|
||||||
|
mergedFieldNames,
|
||||||
|
multiple,
|
||||||
|
toRef(props, 'displayRender'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================== Change ===========================
|
||||||
|
const triggerChange = (nextValues: ValueType) => {
|
||||||
|
setRawValues(nextValues);
|
||||||
|
|
||||||
|
// Save perf if no need trigger event
|
||||||
|
if (props.onChange) {
|
||||||
|
const nextRawValues = toRawValues(nextValues);
|
||||||
|
|
||||||
|
const valueOptions = nextRawValues.map(valueCells =>
|
||||||
|
toPathOptions(valueCells, mergedOptions.value, mergedFieldNames.value).map(
|
||||||
|
valueOpt => valueOpt.option,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const triggerValues = multiple.value ? nextRawValues : nextRawValues[0];
|
||||||
|
const triggerOptions = multiple.value ? valueOptions : valueOptions[0];
|
||||||
|
|
||||||
|
props.onChange(triggerValues, triggerOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================== Select ===========================
|
||||||
|
const onInternalSelect = (valuePath: SingleValueType) => {
|
||||||
|
if (!multiple.value) {
|
||||||
|
triggerChange(valuePath);
|
||||||
|
} else {
|
||||||
|
// Prepare conduct required info
|
||||||
|
const pathKey = toPathKey(valuePath);
|
||||||
|
const checkedPathKeys = toPathKeys(checkedValues.value);
|
||||||
|
const halfCheckedPathKeys = toPathKeys(halfCheckedValues.value);
|
||||||
|
|
||||||
|
const existInChecked = checkedPathKeys.includes(pathKey);
|
||||||
|
const existInMissing = missingCheckedValues.value.some(
|
||||||
|
valueCells => toPathKey(valueCells) === pathKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do update
|
||||||
|
let nextCheckedValues = checkedValues.value;
|
||||||
|
let nextMissingValues = missingCheckedValues.value;
|
||||||
|
|
||||||
|
if (existInMissing && !existInChecked) {
|
||||||
|
// Missing value only do filter
|
||||||
|
nextMissingValues = missingCheckedValues.value.filter(
|
||||||
|
valueCells => toPathKey(valueCells) !== pathKey,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Update checked key first
|
||||||
|
const nextRawCheckedKeys = existInChecked
|
||||||
|
? checkedPathKeys.filter(key => key !== pathKey)
|
||||||
|
: [...checkedPathKeys, pathKey];
|
||||||
|
|
||||||
|
// Conduction by selected or not
|
||||||
|
let checkedKeys: Key[];
|
||||||
|
if (existInChecked) {
|
||||||
|
({ checkedKeys } = conductCheck(
|
||||||
|
nextRawCheckedKeys,
|
||||||
|
{ checked: false, halfCheckedKeys: halfCheckedPathKeys },
|
||||||
|
pathKeyEntities.value,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
({ checkedKeys } = conductCheck(nextRawCheckedKeys, true, pathKeyEntities.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roll up to parent level keys
|
||||||
|
const deDuplicatedKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value);
|
||||||
|
nextCheckedValues = getValueByKeyPath(deDuplicatedKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerChange([...nextMissingValues, ...nextCheckedValues]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display Value change logic
|
||||||
|
const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (_, info) => {
|
||||||
|
if (info.type === 'clear') {
|
||||||
|
triggerChange([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cascader do not support `add` type. Only support `remove`
|
||||||
|
const { valueCells } = info.values[0] as DisplayValueType & { valueCells: SingleValueType };
|
||||||
|
onInternalSelect(valueCells);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================ Open ============================
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
watchEffect(() => {
|
||||||
|
warning(
|
||||||
|
!props.onPopupVisibleChange,
|
||||||
|
'`popupVisibleChange` is deprecated. Please use `dropdownVisibleChange` instead.',
|
||||||
|
);
|
||||||
|
warning(
|
||||||
|
props.popupVisible === undefined,
|
||||||
|
'`popupVisible` is deprecated. Please use `open` instead.',
|
||||||
|
);
|
||||||
|
warning(
|
||||||
|
props.popupClassName === undefined,
|
||||||
|
'`popupClassName` is deprecated. Please use `dropdownClassName` instead.',
|
||||||
|
);
|
||||||
|
warning(
|
||||||
|
props.popupPlacement === undefined,
|
||||||
|
'`popupPlacement` is deprecated. Please use `placement` instead.',
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cascader do not support `add` type. Only support `remove`
|
const mergedOpen = computed(() => (props.open !== undefined ? props.open : props.popupVisible));
|
||||||
const { valueCells } = info.values[0] as DisplayValueType & { valueCells: SingleValueType };
|
|
||||||
onInternalSelect(valueCells);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================ Open ============================
|
const mergedDropdownClassName = computed(() => props.dropdownClassName || props.popupClassName);
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
warning(
|
|
||||||
!onPopupVisibleChange,
|
|
||||||
'`onPopupVisibleChange` is deprecated. Please use `onDropdownVisibleChange` instead.',
|
|
||||||
);
|
|
||||||
warning(popupVisible === undefined, '`popupVisible` is deprecated. Please use `open` instead.');
|
|
||||||
warning(
|
|
||||||
popupClassName === undefined,
|
|
||||||
'`popupClassName` is deprecated. Please use `dropdownClassName` instead.',
|
|
||||||
);
|
|
||||||
warning(
|
|
||||||
popupPlacement === undefined,
|
|
||||||
'`popupPlacement` is deprecated. Please use `placement` instead.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedOpen = open !== undefined ? open : popupVisible;
|
const mergedPlacement = computed(() => props.placement || props.popupPlacement);
|
||||||
|
|
||||||
const mergedDropdownClassName = dropdownClassName || popupClassName;
|
const onInternalDropdownVisibleChange = (nextVisible: boolean) => {
|
||||||
|
props.onDropdownVisibleChange?.(nextVisible);
|
||||||
const mergedPlacement = placement || popupPlacement;
|
props.onPopupVisibleChange?.(nextVisible);
|
||||||
|
};
|
||||||
const onInternalDropdownVisibleChange = (nextVisible: boolean) => {
|
const {
|
||||||
onDropdownVisibleChange?.(nextVisible);
|
changeOnSelect,
|
||||||
onPopupVisibleChange?.(nextVisible);
|
checkable,
|
||||||
};
|
dropdownPrefixCls,
|
||||||
|
loadData,
|
||||||
// ========================== Context ===========================
|
expandTrigger,
|
||||||
const cascaderContext = React.useMemo(
|
expandIcon,
|
||||||
() => ({
|
loadingIcon,
|
||||||
|
dropdownMenuColumnStyle,
|
||||||
|
customSlots,
|
||||||
|
} = toRefs(props);
|
||||||
|
useProvideCascader({
|
||||||
options: mergedOptions,
|
options: mergedOptions,
|
||||||
fieldNames: mergedFieldNames,
|
fieldNames: mergedFieldNames,
|
||||||
values: checkedValues,
|
values: checkedValues,
|
||||||
|
@ -420,81 +426,116 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
|
||||||
expandIcon,
|
expandIcon,
|
||||||
loadingIcon,
|
loadingIcon,
|
||||||
dropdownMenuColumnStyle,
|
dropdownMenuColumnStyle,
|
||||||
}),
|
customSlots,
|
||||||
[
|
});
|
||||||
mergedOptions,
|
const selectRef = ref<BaseSelectRef>();
|
||||||
mergedFieldNames,
|
|
||||||
checkedValues,
|
|
||||||
halfCheckedValues,
|
|
||||||
changeOnSelect,
|
|
||||||
onInternalSelect,
|
|
||||||
checkable,
|
|
||||||
searchOptions,
|
|
||||||
dropdownPrefixCls,
|
|
||||||
loadData,
|
|
||||||
expandTrigger,
|
|
||||||
expandIcon,
|
|
||||||
loadingIcon,
|
|
||||||
dropdownMenuColumnStyle,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// ==============================================================
|
expose({
|
||||||
// == Render ==
|
focus() {
|
||||||
// ==============================================================
|
selectRef.value?.focus();
|
||||||
const emptyOptions = !(mergedSearchValue ? searchOptions : mergedOptions).length;
|
},
|
||||||
|
blur() {
|
||||||
|
selectRef.value?.blur();
|
||||||
|
},
|
||||||
|
scrollTo(arg) {
|
||||||
|
selectRef.value?.scrollTo(arg);
|
||||||
|
},
|
||||||
|
} as BaseSelectRef);
|
||||||
|
|
||||||
const dropdownStyle: React.CSSProperties =
|
const pickProps = computed(() => {
|
||||||
// Search to match width
|
return omit(props, [
|
||||||
(mergedSearchValue && searchConfig.matchInputWidth) ||
|
'id',
|
||||||
// Empty keep the width
|
'prefixCls',
|
||||||
emptyOptions
|
'fieldNames',
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
minWidth: 'auto',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CascaderContext.Provider value={cascaderContext}>
|
|
||||||
<BaseSelect
|
|
||||||
{...restProps}
|
|
||||||
// MISC
|
|
||||||
ref={ref as any}
|
|
||||||
id={mergedId}
|
|
||||||
prefixCls={prefixCls}
|
|
||||||
dropdownMatchSelectWidth={false}
|
|
||||||
dropdownStyle={dropdownStyle}
|
|
||||||
// Value
|
// Value
|
||||||
displayValues={displayValues}
|
'defaultValue',
|
||||||
onDisplayValuesChange={onDisplayValuesChange}
|
'value',
|
||||||
mode={multiple ? 'multiple' : undefined}
|
'changeOnSelect',
|
||||||
|
'onChange',
|
||||||
|
'displayRender',
|
||||||
|
'checkable',
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
searchValue={mergedSearchValue}
|
'searchValue',
|
||||||
onSearch={onInternalSearch}
|
'onSearch',
|
||||||
showSearch={mergedShowSearch}
|
'showSearch',
|
||||||
|
|
||||||
|
// Trigger
|
||||||
|
'expandTrigger',
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
OptionList={OptionList}
|
'options',
|
||||||
emptyOptions={emptyOptions}
|
'dropdownPrefixCls',
|
||||||
|
'loadData',
|
||||||
|
|
||||||
// Open
|
// Open
|
||||||
open={mergedOpen}
|
'popupVisible',
|
||||||
dropdownClassName={mergedDropdownClassName}
|
'open',
|
||||||
placement={mergedPlacement}
|
|
||||||
onDropdownVisibleChange={onInternalDropdownVisibleChange}
|
'popupClassName',
|
||||||
|
'dropdownClassName',
|
||||||
|
'dropdownMenuColumnStyle',
|
||||||
|
|
||||||
|
'popupPlacement',
|
||||||
|
'placement',
|
||||||
|
|
||||||
|
'onDropdownVisibleChange',
|
||||||
|
'onPopupVisibleChange',
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
'expandIcon',
|
||||||
|
'loadingIcon',
|
||||||
|
'customSlots',
|
||||||
|
|
||||||
// Children
|
// Children
|
||||||
getRawInputElement={() => children}
|
'children',
|
||||||
/>
|
]);
|
||||||
</CascaderContext.Provider>
|
});
|
||||||
);
|
return () => {
|
||||||
}) as (<OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType>(
|
const emptyOptions = !(mergedSearchValue.value ? searchOptions.value : mergedOptions.value)
|
||||||
props: React.PropsWithChildren<CascaderProps<OptionType>> & {
|
.length;
|
||||||
ref?: React.Ref<BaseSelectRef>;
|
|
||||||
|
const dropdownStyle: CSSProperties =
|
||||||
|
// Search to match width
|
||||||
|
(mergedSearchValue.value && mergedSearchConfig.value.matchInputWidth) ||
|
||||||
|
// Empty keep the width
|
||||||
|
emptyOptions
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
minWidth: 'auto',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<BaseSelect
|
||||||
|
{...pickProps.value}
|
||||||
|
{...attrs}
|
||||||
|
// MISC
|
||||||
|
ref={selectRef}
|
||||||
|
id={mergedId}
|
||||||
|
prefixCls={props.prefixCls}
|
||||||
|
dropdownMatchSelectWidth={false}
|
||||||
|
dropdownStyle={dropdownStyle}
|
||||||
|
// Value
|
||||||
|
displayValues={displayValues.value}
|
||||||
|
onDisplayValuesChange={onDisplayValuesChange}
|
||||||
|
mode={multiple.value ? 'multiple' : undefined}
|
||||||
|
// Search
|
||||||
|
searchValue={mergedSearchValue.value}
|
||||||
|
onSearch={onInternalSearch}
|
||||||
|
showSearch={mergedShowSearch.value}
|
||||||
|
// Options
|
||||||
|
OptionList={OptionList}
|
||||||
|
emptyOptions={emptyOptions}
|
||||||
|
// Open
|
||||||
|
open={mergedOpen.value}
|
||||||
|
dropdownClassName={mergedDropdownClassName.value}
|
||||||
|
placement={mergedPlacement.value}
|
||||||
|
onDropdownVisibleChange={onInternalDropdownVisibleChange}
|
||||||
|
// Children
|
||||||
|
getRawInputElement={() => slots.default?.()}
|
||||||
|
v-slots={slots}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
) => React.ReactElement) & {
|
});
|
||||||
displayName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
Cascader.displayName = 'Cascader';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Cascader;
|
|
||||||
|
|
|
@ -16,9 +16,9 @@ export default function Checkbox({
|
||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
}: CheckboxProps) {
|
}: CheckboxProps) {
|
||||||
const { slotsContext, checkable } = useInjectCascader();
|
const { customSlots, checkable } = useInjectCascader();
|
||||||
|
|
||||||
const mergedCheckable = checkable.value === undefined ? slotsContext.value.checkable : checkable;
|
const mergedCheckable = checkable.value === undefined ? customSlots.value.checkable : checkable;
|
||||||
const customCheckbox =
|
const customCheckbox =
|
||||||
typeof mergedCheckable === 'function'
|
typeof mergedCheckable === 'function'
|
||||||
? mergedCheckable()
|
? mergedCheckable()
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
import * as React from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { isLeaf, toPathKey } from '../utils/commonUtil';
|
import { isLeaf, toPathKey } from '../utils/commonUtil';
|
||||||
import CascaderContext from '../context';
|
|
||||||
import Checkbox from './Checkbox';
|
import Checkbox from './Checkbox';
|
||||||
import type { DefaultOptionType, SingleValueType } from '../Cascader';
|
import type { DefaultOptionType, SingleValueType } from '../Cascader';
|
||||||
import { SEARCH_MARK } from '../hooks/useSearchOptions';
|
import { SEARCH_MARK } from '../hooks/useSearchOptions';
|
||||||
|
import type { Key } from '../../_util/type';
|
||||||
|
import { useInjectCascader } from '../context';
|
||||||
|
|
||||||
export interface ColumnProps {
|
export interface ColumnProps {
|
||||||
prefixCls: string;
|
prefixCls: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
options: DefaultOptionType[];
|
options: DefaultOptionType[];
|
||||||
/** Current Column opened item key */
|
/** Current Column opened item key */
|
||||||
activeValue?: React.Key;
|
activeValue?: Key;
|
||||||
/** The value path before current column */
|
/** The value path before current column */
|
||||||
prevValuePath: React.Key[];
|
prevValuePath: Key[];
|
||||||
onToggleOpen: (open: boolean) => void;
|
onToggleOpen: (open: boolean) => void;
|
||||||
onSelect: (valuePath: SingleValueType, leaf: boolean) => void;
|
onSelect: (valuePath: SingleValueType, leaf: boolean) => void;
|
||||||
onActive: (valuePath: SingleValueType) => void;
|
onActive: (valuePath: SingleValueType) => void;
|
||||||
checkedSet: Set<React.Key>;
|
checkedSet: Set<Key>;
|
||||||
halfCheckedSet: Set<React.Key>;
|
halfCheckedSet: Set<Key>;
|
||||||
loadingKeys: React.Key[];
|
loadingKeys: Key[];
|
||||||
isSelectable: (option: DefaultOptionType) => boolean;
|
isSelectable: (option: DefaultOptionType) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,27 +43,29 @@ export default function Column({
|
||||||
fieldNames,
|
fieldNames,
|
||||||
changeOnSelect,
|
changeOnSelect,
|
||||||
expandTrigger,
|
expandTrigger,
|
||||||
expandIcon,
|
expandIcon: expandIconRef,
|
||||||
loadingIcon,
|
loadingIcon: loadingIconRef,
|
||||||
dropdownMenuColumnStyle,
|
dropdownMenuColumnStyle,
|
||||||
} = React.useContext(CascaderContext);
|
customSlots,
|
||||||
|
} = useInjectCascader();
|
||||||
const hoverOpen = expandTrigger === 'hover';
|
const expandIcon = expandIconRef.value ?? customSlots.value.expandIcon?.();
|
||||||
|
const loadingIcon = loadingIconRef.value ?? customSlots.value.loadingIcon?.();
|
||||||
|
|
||||||
|
const hoverOpen = expandTrigger.value === 'hover';
|
||||||
// ============================ Render ============================
|
// ============================ Render ============================
|
||||||
return (
|
return (
|
||||||
<ul className={menuPrefixCls} role="menu">
|
<ul class={menuPrefixCls} role="menu">
|
||||||
{options.map(option => {
|
{options.map(option => {
|
||||||
const { disabled } = option;
|
const { disabled } = option;
|
||||||
const searchOptions = option[SEARCH_MARK];
|
const searchOptions = option[SEARCH_MARK];
|
||||||
const label = option[fieldNames.label];
|
const label = option[fieldNames.value.label];
|
||||||
const value = option[fieldNames.value];
|
const value = option[fieldNames.value.value];
|
||||||
|
|
||||||
const isMergedLeaf = isLeaf(option, fieldNames);
|
const isMergedLeaf = isLeaf(option, fieldNames.value);
|
||||||
|
|
||||||
// Get real value of option. Search option is different way.
|
// Get real value of option. Search option is different way.
|
||||||
const fullPath = searchOptions
|
const fullPath = searchOptions
|
||||||
? searchOptions.map(opt => opt[fieldNames.value])
|
? searchOptions.map(opt => opt[fieldNames.value.value])
|
||||||
: [...prevValuePath, value];
|
: [...prevValuePath, value];
|
||||||
const fullPathKey = toPathKey(fullPath);
|
const fullPathKey = toPathKey(fullPath);
|
||||||
|
|
||||||
|
@ -75,7 +76,6 @@ export default function Column({
|
||||||
|
|
||||||
// >>>>> halfChecked
|
// >>>>> halfChecked
|
||||||
const halfChecked = halfCheckedSet.has(fullPathKey);
|
const halfChecked = halfCheckedSet.has(fullPathKey);
|
||||||
|
|
||||||
// >>>>> Open
|
// >>>>> Open
|
||||||
const triggerOpenPath = () => {
|
const triggerOpenPath = () => {
|
||||||
if (!disabled && (!hoverOpen || !isMergedLeaf)) {
|
if (!disabled && (!hoverOpen || !isMergedLeaf)) {
|
||||||
|
@ -102,13 +102,16 @@ export default function Column({
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={fullPathKey}
|
key={fullPathKey}
|
||||||
className={classNames(menuItemPrefixCls, {
|
class={[
|
||||||
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
|
menuItemPrefixCls,
|
||||||
[`${menuItemPrefixCls}-active`]: activeValue === value,
|
{
|
||||||
[`${menuItemPrefixCls}-disabled`]: disabled,
|
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
|
||||||
[`${menuItemPrefixCls}-loading`]: isLoading,
|
[`${menuItemPrefixCls}-active`]: activeValue === value,
|
||||||
})}
|
[`${menuItemPrefixCls}-disabled`]: disabled,
|
||||||
style={dropdownMenuColumnStyle}
|
[`${menuItemPrefixCls}-loading`]: isLoading,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
style={dropdownMenuColumnStyle.value}
|
||||||
role="menuitemcheckbox"
|
role="menuitemcheckbox"
|
||||||
title={title}
|
title={title}
|
||||||
aria-checked={checked}
|
aria-checked={checked}
|
||||||
|
@ -119,12 +122,12 @@ export default function Column({
|
||||||
triggerSelect();
|
triggerSelect();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDoubleClick={() => {
|
onDblclick={() => {
|
||||||
if (changeOnSelect) {
|
if (changeOnSelect.value) {
|
||||||
onToggleOpen(false);
|
onToggleOpen(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => {
|
onMouseenter={() => {
|
||||||
if (hoverOpen) {
|
if (hoverOpen) {
|
||||||
triggerOpenPath();
|
triggerOpenPath();
|
||||||
}
|
}
|
||||||
|
@ -136,18 +139,18 @@ export default function Column({
|
||||||
checked={checked}
|
checked={checked}
|
||||||
halfChecked={halfChecked}
|
halfChecked={halfChecked}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={(e: React.MouseEvent<HTMLSpanElement>) => {
|
onClick={(e: MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
triggerSelect();
|
triggerSelect();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={`${menuItemPrefixCls}-content`}>{option[fieldNames.label]}</div>
|
<div class={`${menuItemPrefixCls}-content`}>{option[fieldNames.value.label]}</div>
|
||||||
{!isLoading && expandIcon && !isMergedLeaf && (
|
{!isLoading && expandIcon && !isMergedLeaf && (
|
||||||
<div className={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
|
<div class={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && loadingIcon && (
|
{isLoading && loadingIcon && (
|
||||||
<div className={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
|
<div class={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -155,3 +158,19 @@ export default function Column({
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Column.props = [
|
||||||
|
'prefixCls',
|
||||||
|
'multiple',
|
||||||
|
'options',
|
||||||
|
'activeValue',
|
||||||
|
'prevValuePath',
|
||||||
|
'onToggleOpen',
|
||||||
|
'onSelect',
|
||||||
|
'onActive',
|
||||||
|
'checkedSet',
|
||||||
|
'halfCheckedSet',
|
||||||
|
'loadingKeys',
|
||||||
|
'isSelectable',
|
||||||
|
];
|
||||||
|
Column.displayName = 'Column';
|
||||||
|
Column.inheritAttrs = false;
|
||||||
|
|
|
@ -1,213 +1,228 @@
|
||||||
/* eslint-disable default-case */
|
/* eslint-disable default-case */
|
||||||
import * as React from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useBaseProps } from 'rc-select';
|
|
||||||
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
|
|
||||||
import Column from './Column';
|
import Column from './Column';
|
||||||
import CascaderContext from '../context';
|
|
||||||
import type { DefaultOptionType, SingleValueType } from '../Cascader';
|
import type { DefaultOptionType, SingleValueType } from '../Cascader';
|
||||||
import { isLeaf, toPathKey, toPathKeys, toPathValueStr } from '../utils/commonUtil';
|
import { isLeaf, toPathKey, toPathKeys, toPathValueStr } from '../utils/commonUtil';
|
||||||
import useActive from './useActive';
|
import useActive from './useActive';
|
||||||
import useKeyboard from './useKeyboard';
|
import useKeyboard from './useKeyboard';
|
||||||
import { toPathOptions } from '../utils/treeUtil';
|
import { toPathOptions } from '../utils/treeUtil';
|
||||||
|
import { computed, defineComponent, ref, shallowRef, watchEffect } from 'vue';
|
||||||
|
import { useBaseProps } from '../../vc-select';
|
||||||
|
import { useInjectCascader } from '../context';
|
||||||
|
import type { Key } from '../../_util/type';
|
||||||
|
import type { EventHandler } from '../../_util/EventInterface';
|
||||||
|
|
||||||
const RefOptionList = React.forwardRef<RefOptionListProps>((props, ref) => {
|
export default defineComponent({
|
||||||
const { prefixCls, multiple, searchValue, toggleOpen, notFoundContent, direction } =
|
name: 'OptionList',
|
||||||
useBaseProps();
|
inheritAttrs: false,
|
||||||
|
setup(_props, context) {
|
||||||
|
const { attrs, slots } = context;
|
||||||
|
const baseProps = useBaseProps();
|
||||||
|
const containerRef = ref<HTMLDivElement>();
|
||||||
|
const rtl = computed(() => baseProps.direction === 'rtl');
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
values,
|
||||||
|
halfValues,
|
||||||
|
fieldNames,
|
||||||
|
changeOnSelect,
|
||||||
|
onSelect,
|
||||||
|
searchOptions,
|
||||||
|
dropdownPrefixCls,
|
||||||
|
loadData,
|
||||||
|
expandTrigger,
|
||||||
|
customSlots,
|
||||||
|
} = useInjectCascader();
|
||||||
|
|
||||||
const containerRef = React.useRef<HTMLDivElement>();
|
const mergedPrefixCls = computed(() => dropdownPrefixCls.value || baseProps.prefixCls);
|
||||||
const rtl = direction === 'rtl';
|
|
||||||
|
|
||||||
const {
|
// ========================= loadData =========================
|
||||||
options,
|
const loadingKeys = shallowRef<string[]>([]);
|
||||||
values,
|
const internalLoadData = (valueCells: Key[]) => {
|
||||||
halfValues,
|
// Do not load when search
|
||||||
fieldNames,
|
if (!loadData.value || baseProps.searchValue) {
|
||||||
changeOnSelect,
|
return;
|
||||||
onSelect,
|
|
||||||
searchOptions,
|
|
||||||
dropdownPrefixCls,
|
|
||||||
loadData,
|
|
||||||
expandTrigger,
|
|
||||||
} = React.useContext(CascaderContext);
|
|
||||||
|
|
||||||
const mergedPrefixCls = dropdownPrefixCls || prefixCls;
|
|
||||||
|
|
||||||
// ========================= loadData =========================
|
|
||||||
const [loadingKeys, setLoadingKeys] = React.useState([]);
|
|
||||||
|
|
||||||
const internalLoadData = (valueCells: React.Key[]) => {
|
|
||||||
// Do not load when search
|
|
||||||
if (!loadData || searchValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const optionList = toPathOptions(valueCells, options, fieldNames);
|
|
||||||
const rawOptions = optionList.map(({ option }) => option);
|
|
||||||
const lastOption = rawOptions[rawOptions.length - 1];
|
|
||||||
|
|
||||||
if (lastOption && !isLeaf(lastOption, fieldNames)) {
|
|
||||||
const pathKey = toPathKey(valueCells);
|
|
||||||
|
|
||||||
setLoadingKeys(keys => [...keys, pathKey]);
|
|
||||||
|
|
||||||
loadData(rawOptions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// zombieJ: This is bad. We should make this same as `rc-tree` to use Promise instead.
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (loadingKeys.length) {
|
|
||||||
loadingKeys.forEach(loadingKey => {
|
|
||||||
const valueStrCells = toPathValueStr(loadingKey);
|
|
||||||
const optionList = toPathOptions(valueStrCells, options, fieldNames, true).map(
|
|
||||||
({ option }) => option,
|
|
||||||
);
|
|
||||||
const lastOption = optionList[optionList.length - 1];
|
|
||||||
|
|
||||||
if (!lastOption || lastOption[fieldNames.children] || isLeaf(lastOption, fieldNames)) {
|
|
||||||
setLoadingKeys(keys => keys.filter(key => key !== loadingKey));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [options, loadingKeys, fieldNames]);
|
|
||||||
|
|
||||||
// ========================== Values ==========================
|
|
||||||
const checkedSet = React.useMemo(() => new Set(toPathKeys(values)), [values]);
|
|
||||||
const halfCheckedSet = React.useMemo(() => new Set(toPathKeys(halfValues)), [halfValues]);
|
|
||||||
|
|
||||||
// ====================== Accessibility =======================
|
|
||||||
const [activeValueCells, setActiveValueCells] = useActive();
|
|
||||||
|
|
||||||
// =========================== Path ===========================
|
|
||||||
const onPathOpen = (nextValueCells: React.Key[]) => {
|
|
||||||
setActiveValueCells(nextValueCells);
|
|
||||||
|
|
||||||
// Trigger loadData
|
|
||||||
internalLoadData(nextValueCells);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelectable = (option: DefaultOptionType) => {
|
|
||||||
const { disabled } = option;
|
|
||||||
|
|
||||||
const isMergedLeaf = isLeaf(option, fieldNames);
|
|
||||||
return !disabled && (isMergedLeaf || changeOnSelect || multiple);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPathSelect = (valuePath: SingleValueType, leaf: boolean, fromKeyboard = false) => {
|
|
||||||
onSelect(valuePath);
|
|
||||||
|
|
||||||
if (!multiple && (leaf || (changeOnSelect && (expandTrigger === 'hover' || fromKeyboard)))) {
|
|
||||||
toggleOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========================== Option ==========================
|
|
||||||
const mergedOptions = React.useMemo(() => {
|
|
||||||
if (searchValue) {
|
|
||||||
return searchOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}, [searchValue, searchOptions, options]);
|
|
||||||
|
|
||||||
// ========================== Column ==========================
|
|
||||||
const optionColumns = React.useMemo(() => {
|
|
||||||
const optionList = [{ options: mergedOptions }];
|
|
||||||
let currentList = mergedOptions;
|
|
||||||
|
|
||||||
for (let i = 0; i < activeValueCells.length; i += 1) {
|
|
||||||
const activeValueCell = activeValueCells[i];
|
|
||||||
const currentOption = currentList.find(
|
|
||||||
option => option[fieldNames.value] === activeValueCell,
|
|
||||||
);
|
|
||||||
|
|
||||||
const subOptions = currentOption?.[fieldNames.children];
|
|
||||||
if (!subOptions?.length) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentList = subOptions;
|
const optionList = toPathOptions(valueCells, options.value, fieldNames.value);
|
||||||
optionList.push({ options: subOptions });
|
const rawOptions = optionList.map(({ option }) => option);
|
||||||
}
|
const lastOption = rawOptions[rawOptions.length - 1];
|
||||||
|
|
||||||
return optionList;
|
if (lastOption && !isLeaf(lastOption, fieldNames.value)) {
|
||||||
}, [mergedOptions, activeValueCells, fieldNames]);
|
const pathKey = toPathKey(valueCells);
|
||||||
|
|
||||||
// ========================= Keyboard =========================
|
loadingKeys.value = [...loadingKeys.value, pathKey];
|
||||||
const onKeyboardSelect = (selectValueCells: SingleValueType, option: DefaultOptionType) => {
|
loadData.value(rawOptions);
|
||||||
if (isSelectable(option)) {
|
}
|
||||||
onPathSelect(selectValueCells, isLeaf(option, fieldNames), true);
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useKeyboard(
|
watchEffect(() => {
|
||||||
ref,
|
if (loadingKeys.value.length) {
|
||||||
mergedOptions,
|
loadingKeys.value.forEach(loadingKey => {
|
||||||
fieldNames,
|
const valueStrCells = toPathValueStr(loadingKey);
|
||||||
activeValueCells,
|
const optionList = toPathOptions(
|
||||||
onPathOpen,
|
valueStrCells,
|
||||||
containerRef,
|
options.value,
|
||||||
onKeyboardSelect,
|
fieldNames.value,
|
||||||
);
|
true,
|
||||||
|
).map(({ option }) => option);
|
||||||
|
const lastOption = optionList[optionList.length - 1];
|
||||||
|
|
||||||
// ========================== Render ==========================
|
if (
|
||||||
// >>>>> Empty
|
!lastOption ||
|
||||||
const isEmpty = !optionColumns[0]?.options?.length;
|
lastOption[fieldNames.value.children] ||
|
||||||
|
isLeaf(lastOption, fieldNames.value)
|
||||||
|
) {
|
||||||
|
loadingKeys.value = loadingKeys.value.filter(key => key !== loadingKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const emptyList: DefaultOptionType[] = [
|
// ========================== Values ==========================
|
||||||
{
|
const checkedSet = computed(() => new Set(toPathKeys(values.value)));
|
||||||
[fieldNames.label as 'label']: notFoundContent,
|
const halfCheckedSet = computed(() => new Set(toPathKeys(halfValues.value)));
|
||||||
[fieldNames.value as 'value']: '__EMPTY__',
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const columnProps = {
|
// ====================== Accessibility =======================
|
||||||
...props,
|
const [activeValueCells, setActiveValueCells] = useActive();
|
||||||
multiple: !isEmpty && multiple,
|
|
||||||
onSelect: onPathSelect,
|
|
||||||
onActive: onPathOpen,
|
|
||||||
onToggleOpen: toggleOpen,
|
|
||||||
checkedSet,
|
|
||||||
halfCheckedSet,
|
|
||||||
loadingKeys,
|
|
||||||
isSelectable,
|
|
||||||
};
|
|
||||||
|
|
||||||
// >>>>> Columns
|
// =========================== Path ===========================
|
||||||
const mergedOptionColumns = isEmpty ? [{ options: emptyList }] : optionColumns;
|
const onPathOpen = (nextValueCells: Key[]) => {
|
||||||
|
setActiveValueCells(nextValueCells);
|
||||||
|
|
||||||
const columnNodes: React.ReactElement[] = mergedOptionColumns.map((col, index) => {
|
// Trigger loadData
|
||||||
const prevValuePath = activeValueCells.slice(0, index);
|
internalLoadData(nextValueCells);
|
||||||
const activeValue = activeValueCells[index];
|
};
|
||||||
|
|
||||||
return (
|
const isSelectable = (option: DefaultOptionType) => {
|
||||||
<Column
|
const { disabled } = option;
|
||||||
key={index}
|
|
||||||
{...columnProps}
|
const isMergedLeaf = isLeaf(option, fieldNames.value);
|
||||||
prefixCls={mergedPrefixCls}
|
return !disabled && (isMergedLeaf || changeOnSelect.value || baseProps.multiple);
|
||||||
options={col.options}
|
};
|
||||||
prevValuePath={prevValuePath}
|
|
||||||
activeValue={activeValue}
|
const onPathSelect = (valuePath: SingleValueType, leaf: boolean, fromKeyboard = false) => {
|
||||||
/>
|
onSelect(valuePath);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!baseProps.multiple &&
|
||||||
|
(leaf || (changeOnSelect.value && (expandTrigger.value === 'hover' || fromKeyboard)))
|
||||||
|
) {
|
||||||
|
baseProps.toggleOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================== Option ==========================
|
||||||
|
const mergedOptions = computed(() => {
|
||||||
|
if (baseProps.searchValue) {
|
||||||
|
return searchOptions.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================== Column ==========================
|
||||||
|
const optionColumns = computed(() => {
|
||||||
|
const optionList = [{ options: mergedOptions.value }];
|
||||||
|
let currentList = mergedOptions.value;
|
||||||
|
for (let i = 0; i < activeValueCells.value.length; i += 1) {
|
||||||
|
const activeValueCell = activeValueCells.value[i];
|
||||||
|
const currentOption = currentList.find(
|
||||||
|
option => option[fieldNames.value.value] === activeValueCell,
|
||||||
|
);
|
||||||
|
|
||||||
|
const subOptions = currentOption?.[fieldNames.value.children];
|
||||||
|
if (!subOptions?.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentList = subOptions;
|
||||||
|
optionList.push({ options: subOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionList;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================= Keyboard =========================
|
||||||
|
const onKeyboardSelect = (selectValueCells: SingleValueType, option: DefaultOptionType) => {
|
||||||
|
if (isSelectable(option)) {
|
||||||
|
onPathSelect(selectValueCells, isLeaf(option, fieldNames.value), true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useKeyboard(
|
||||||
|
context,
|
||||||
|
mergedOptions,
|
||||||
|
fieldNames,
|
||||||
|
activeValueCells,
|
||||||
|
onPathOpen,
|
||||||
|
containerRef,
|
||||||
|
onKeyboardSelect,
|
||||||
);
|
);
|
||||||
});
|
const onListMouseDown: EventHandler = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
return () => {
|
||||||
|
// ========================== Render ==========================
|
||||||
|
const {
|
||||||
|
notFoundContent = slots.notFoundContent?.() || customSlots.value.notFoundContent?.(),
|
||||||
|
multiple,
|
||||||
|
toggleOpen,
|
||||||
|
} = baseProps;
|
||||||
|
// >>>>> Empty
|
||||||
|
const isEmpty = !optionColumns.value[0]?.options?.length;
|
||||||
|
|
||||||
// >>>>> Render
|
const emptyList: DefaultOptionType[] = [
|
||||||
return (
|
{
|
||||||
<>
|
[fieldNames.value.label as 'label']: notFoundContent,
|
||||||
<div
|
[fieldNames.value.value as 'value']: '__EMPTY__',
|
||||||
className={classNames(`${mergedPrefixCls}-menus`, {
|
disabled: true,
|
||||||
[`${mergedPrefixCls}-menu-empty`]: isEmpty,
|
},
|
||||||
[`${mergedPrefixCls}-rtl`]: rtl,
|
];
|
||||||
})}
|
const columnProps = {
|
||||||
ref={containerRef}
|
...attrs,
|
||||||
>
|
multiple: !isEmpty && multiple,
|
||||||
{columnNodes}
|
onSelect: onPathSelect,
|
||||||
</div>
|
onActive: onPathOpen,
|
||||||
</>
|
onToggleOpen: toggleOpen,
|
||||||
);
|
checkedSet: checkedSet.value,
|
||||||
|
halfCheckedSet: halfCheckedSet.value,
|
||||||
|
loadingKeys: loadingKeys.value,
|
||||||
|
isSelectable,
|
||||||
|
};
|
||||||
|
|
||||||
|
// >>>>> Columns
|
||||||
|
const mergedOptionColumns = isEmpty ? [{ options: emptyList }] : optionColumns.value;
|
||||||
|
|
||||||
|
const columnNodes = mergedOptionColumns.map((col, index) => {
|
||||||
|
const prevValuePath = activeValueCells.value.slice(0, index);
|
||||||
|
const activeValue = activeValueCells.value[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
key={index}
|
||||||
|
{...columnProps}
|
||||||
|
prefixCls={mergedPrefixCls.value}
|
||||||
|
options={col.options}
|
||||||
|
prevValuePath={prevValuePath}
|
||||||
|
activeValue={activeValue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
`${mergedPrefixCls.value}-menus`,
|
||||||
|
{
|
||||||
|
[`${mergedPrefixCls.value}-menu-empty`]: isEmpty,
|
||||||
|
[`${mergedPrefixCls.value}-rtl`]: rtl.value,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onMousedown={onListMouseDown}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
{columnNodes}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default RefOptionList;
|
|
||||||
|
|
|
@ -1,28 +1,30 @@
|
||||||
import * as React from 'react';
|
import { useInjectCascader } from '../context';
|
||||||
import CascaderContext from '../context';
|
import type { Ref } from 'vue';
|
||||||
import { useBaseProps } from 'rc-select';
|
import { watch } from 'vue';
|
||||||
|
import { useBaseProps } from '../../vc-select';
|
||||||
|
import type { Key } from '../../_util/type';
|
||||||
|
import useState from '../../_util/hooks/useState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Control the active open options path.
|
* Control the active open options path.
|
||||||
*/
|
*/
|
||||||
export default (): [React.Key[], (activeValueCells: React.Key[]) => void] => {
|
export default (): [Ref<Key[]>, (activeValueCells: Key[]) => void] => {
|
||||||
const { multiple, open } = useBaseProps();
|
const baseProps = useBaseProps();
|
||||||
const { values } = React.useContext(CascaderContext);
|
const { values } = useInjectCascader();
|
||||||
|
|
||||||
// Record current dropdown active options
|
// Record current dropdown active options
|
||||||
// This also control the open status
|
// This also control the open status
|
||||||
const [activeValueCells, setActiveValueCells] = React.useState<React.Key[]>([]);
|
const [activeValueCells, setActiveValueCells] = useState<Key[]>([]);
|
||||||
|
|
||||||
React.useEffect(
|
watch(
|
||||||
|
() => baseProps.open,
|
||||||
() => {
|
() => {
|
||||||
if (open && !multiple) {
|
if (baseProps.open && !baseProps.multiple) {
|
||||||
const firstValueCells = values[0];
|
const firstValueCells = values.value[0];
|
||||||
setActiveValueCells(firstValueCells || []);
|
setActiveValueCells(firstValueCells || []);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
{ immediate: true },
|
||||||
[open],
|
|
||||||
/* eslint-enable react-hooks/exhaustive-deps */
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return [activeValueCells, setActiveValueCells];
|
return [activeValueCells, setActiveValueCells];
|
||||||
|
|
|
@ -1,36 +1,42 @@
|
||||||
import * as React from 'react';
|
import type { RefOptionListProps } from '../../vc-select/OptionList';
|
||||||
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
|
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||||
import KeyCode from 'rc-util/lib/KeyCode';
|
import type { Ref, SetupContext } from 'vue';
|
||||||
|
import { ref, watchEffect } from 'vue';
|
||||||
import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader';
|
import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader';
|
||||||
import { toPathKey } from '../utils/commonUtil';
|
import { toPathKey } from '../utils/commonUtil';
|
||||||
import { useBaseProps } from 'rc-select';
|
import { useBaseProps } from '../../vc-select';
|
||||||
|
import KeyCode from '../../_util/KeyCode';
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
ref: React.Ref<RefOptionListProps>,
|
context: SetupContext,
|
||||||
options: DefaultOptionType[],
|
options: Ref<DefaultOptionType[]>,
|
||||||
fieldNames: InternalFieldNames,
|
fieldNames: Ref<InternalFieldNames>,
|
||||||
activeValueCells: React.Key[],
|
activeValueCells: Ref<Key[]>,
|
||||||
setActiveValueCells: (activeValueCells: React.Key[]) => void,
|
setActiveValueCells: (activeValueCells: Key[]) => void,
|
||||||
containerRef: React.RefObject<HTMLElement>,
|
containerRef: Ref<HTMLElement>,
|
||||||
onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void,
|
onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void,
|
||||||
) => {
|
) => {
|
||||||
const { direction, searchValue, toggleOpen, open } = useBaseProps();
|
const { direction, searchValue, toggleOpen, open } = useBaseProps();
|
||||||
const rtl = direction === 'rtl';
|
const rtl = direction === 'rtl';
|
||||||
|
const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = [
|
||||||
const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = React.useMemo(() => {
|
ref<Key[]>([]),
|
||||||
|
ref<number>(),
|
||||||
|
ref<DefaultOptionType[]>([]),
|
||||||
|
];
|
||||||
|
watchEffect(() => {
|
||||||
let activeIndex = -1;
|
let activeIndex = -1;
|
||||||
let currentOptions = options;
|
let currentOptions = options.value;
|
||||||
|
|
||||||
const mergedActiveIndexes: number[] = [];
|
const mergedActiveIndexes: number[] = [];
|
||||||
const mergedActiveValueCells: React.Key[] = [];
|
const mergedActiveValueCells: Key[] = [];
|
||||||
|
|
||||||
const len = activeValueCells.length;
|
const len = activeValueCells.value.length;
|
||||||
|
|
||||||
// Fill validate active value cells and index
|
// Fill validate active value cells and index
|
||||||
for (let i = 0; i < len; i += 1) {
|
for (let i = 0; i < len; i += 1) {
|
||||||
// Mark the active index for current options
|
// Mark the active index for current options
|
||||||
const nextActiveIndex = currentOptions.findIndex(
|
const nextActiveIndex = currentOptions.findIndex(
|
||||||
option => option[fieldNames.value] === activeValueCells[i],
|
option => option[fieldNames.value.value] === activeValueCells.value[i],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nextActiveIndex === -1) {
|
if (nextActiveIndex === -1) {
|
||||||
|
@ -39,44 +45,48 @@ export default (
|
||||||
|
|
||||||
activeIndex = nextActiveIndex;
|
activeIndex = nextActiveIndex;
|
||||||
mergedActiveIndexes.push(activeIndex);
|
mergedActiveIndexes.push(activeIndex);
|
||||||
mergedActiveValueCells.push(activeValueCells[i]);
|
mergedActiveValueCells.push(activeValueCells.value[i]);
|
||||||
|
|
||||||
currentOptions = currentOptions[activeIndex][fieldNames.children];
|
currentOptions = currentOptions[activeIndex][fieldNames.value.children];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill last active options
|
// Fill last active options
|
||||||
let activeOptions = options;
|
let activeOptions = options.value;
|
||||||
for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) {
|
for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) {
|
||||||
activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.children];
|
activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.value.children];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [mergedActiveValueCells, activeIndex, activeOptions];
|
[validActiveValueCells.value, lastActiveIndex.value, lastActiveOptions.value] = [
|
||||||
}, [activeValueCells, fieldNames, options]);
|
mergedActiveValueCells,
|
||||||
|
activeIndex,
|
||||||
|
activeOptions,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
// Update active value cells and scroll to target element
|
// Update active value cells and scroll to target element
|
||||||
const internalSetActiveValueCells = (next: React.Key[]) => {
|
const internalSetActiveValueCells = (next: Key[]) => {
|
||||||
setActiveValueCells(next);
|
setActiveValueCells(next);
|
||||||
|
|
||||||
const ele = containerRef.current?.querySelector(`li[data-path-key="${toPathKey(next)}"]`);
|
const ele = containerRef.value?.querySelector(`li[data-path-key="${toPathKey(next)}"]`);
|
||||||
ele?.scrollIntoView?.({ block: 'nearest' });
|
ele?.scrollIntoView?.({ block: 'nearest' });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Same options offset
|
// Same options offset
|
||||||
const offsetActiveOption = (offset: number) => {
|
const offsetActiveOption = (offset: number) => {
|
||||||
const len = lastActiveOptions.length;
|
const len = lastActiveOptions.value.length;
|
||||||
|
|
||||||
let currentIndex = lastActiveIndex;
|
let currentIndex = lastActiveIndex.value;
|
||||||
if (currentIndex === -1 && offset < 0) {
|
if (currentIndex === -1 && offset < 0) {
|
||||||
currentIndex = len;
|
currentIndex = len;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < len; i += 1) {
|
for (let i = 0; i < len; i += 1) {
|
||||||
currentIndex = (currentIndex + offset + len) % len;
|
currentIndex = (currentIndex + offset + len) % len;
|
||||||
const option = lastActiveOptions[currentIndex];
|
const option = lastActiveOptions.value[currentIndex];
|
||||||
|
|
||||||
if (option && !option.disabled) {
|
if (option && !option.disabled) {
|
||||||
const value = option[fieldNames.value];
|
const value = option[fieldNames.value.value];
|
||||||
const nextActiveCells = validActiveValueCells.slice(0, -1).concat(value);
|
const nextActiveCells = validActiveValueCells.value.slice(0, -1).concat(value);
|
||||||
internalSetActiveValueCells(nextActiveCells);
|
internalSetActiveValueCells(nextActiveCells);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -85,8 +95,8 @@ export default (
|
||||||
|
|
||||||
// Different options offset
|
// Different options offset
|
||||||
const prevColumn = () => {
|
const prevColumn = () => {
|
||||||
if (validActiveValueCells.length > 1) {
|
if (validActiveValueCells.value.length > 1) {
|
||||||
const nextActiveCells = validActiveValueCells.slice(0, -1);
|
const nextActiveCells = validActiveValueCells.value.slice(0, -1);
|
||||||
internalSetActiveValueCells(nextActiveCells);
|
internalSetActiveValueCells(nextActiveCells);
|
||||||
} else {
|
} else {
|
||||||
toggleOpen(false);
|
toggleOpen(false);
|
||||||
|
@ -95,19 +105,19 @@ export default (
|
||||||
|
|
||||||
const nextColumn = () => {
|
const nextColumn = () => {
|
||||||
const nextOptions: DefaultOptionType[] =
|
const nextOptions: DefaultOptionType[] =
|
||||||
lastActiveOptions[lastActiveIndex]?.[fieldNames.children] || [];
|
lastActiveOptions.value[lastActiveIndex.value]?.[fieldNames.value.children] || [];
|
||||||
|
|
||||||
const nextOption = nextOptions.find(option => !option.disabled);
|
const nextOption = nextOptions.find(option => !option.disabled);
|
||||||
|
|
||||||
if (nextOption) {
|
if (nextOption) {
|
||||||
const nextActiveCells = [...validActiveValueCells, nextOption[fieldNames.value]];
|
const nextActiveCells = [...validActiveValueCells.value, nextOption[fieldNames.value.value]];
|
||||||
internalSetActiveValueCells(nextActiveCells);
|
internalSetActiveValueCells(nextActiveCells);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
context.expose({
|
||||||
// scrollTo: treeRef.current?.scrollTo,
|
// scrollTo: treeRef.current?.scrollTo,
|
||||||
onKeyDown: event => {
|
onKeydown: event => {
|
||||||
const { which } = event;
|
const { which } = event;
|
||||||
|
|
||||||
switch (which) {
|
switch (which) {
|
||||||
|
@ -155,8 +165,11 @@ export default (
|
||||||
|
|
||||||
// >>> Select
|
// >>> Select
|
||||||
case KeyCode.ENTER: {
|
case KeyCode.ENTER: {
|
||||||
if (validActiveValueCells.length) {
|
if (validActiveValueCells.value.length) {
|
||||||
onKeyBoardSelect(validActiveValueCells, lastActiveOptions[lastActiveIndex]);
|
onKeyBoardSelect(
|
||||||
|
validActiveValueCells.value,
|
||||||
|
lastActiveOptions.value[lastActiveIndex.value],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -171,6 +184,6 @@ export default (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onKeyUp: () => {},
|
onKeyup: () => {},
|
||||||
}));
|
} as RefOptionListProps);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,14 +2,14 @@ import type { CSSProperties, InjectionKey, Ref } from 'vue';
|
||||||
import { inject, provide } from 'vue';
|
import { inject, provide } from 'vue';
|
||||||
import type { VueNode } from '../_util/type';
|
import type { VueNode } from '../_util/type';
|
||||||
import type {
|
import type {
|
||||||
CascaderProps,
|
BaseCascaderProps,
|
||||||
InternalFieldNames,
|
InternalFieldNames,
|
||||||
DefaultOptionType,
|
DefaultOptionType,
|
||||||
SingleValueType,
|
SingleValueType,
|
||||||
} from './Cascader';
|
} from './Cascader';
|
||||||
|
|
||||||
export interface CascaderContextProps {
|
export interface CascaderContextProps {
|
||||||
options: Ref<CascaderProps['options']>;
|
options: Ref<BaseCascaderProps['options']>;
|
||||||
fieldNames: Ref<InternalFieldNames>;
|
fieldNames: Ref<InternalFieldNames>;
|
||||||
values: Ref<SingleValueType[]>;
|
values: Ref<SingleValueType[]>;
|
||||||
halfValues: Ref<SingleValueType[]>;
|
halfValues: Ref<SingleValueType[]>;
|
||||||
|
@ -23,7 +23,7 @@ export interface CascaderContextProps {
|
||||||
expandIcon: Ref<VueNode>;
|
expandIcon: Ref<VueNode>;
|
||||||
loadingIcon: Ref<VueNode>;
|
loadingIcon: Ref<VueNode>;
|
||||||
dropdownMenuColumnStyle: Ref<CSSProperties>;
|
dropdownMenuColumnStyle: Ref<CSSProperties>;
|
||||||
slotsContext: Ref<Record<string, Function>>;
|
customSlots: Ref<Record<string, Function>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CascaderContextKey: InjectionKey<CascaderContextProps> = Symbol('CascaderContextKey');
|
const CascaderContextKey: InjectionKey<CascaderContextProps> = Symbol('CascaderContextKey');
|
||||||
|
|
|
@ -1,37 +1,38 @@
|
||||||
import { toPathOptions } from '../utils/treeUtil';
|
import { toPathOptions } from '../utils/treeUtil';
|
||||||
import * as React from 'react';
|
|
||||||
import type {
|
import type {
|
||||||
DefaultOptionType,
|
DefaultOptionType,
|
||||||
SingleValueType,
|
SingleValueType,
|
||||||
CascaderProps,
|
BaseCascaderProps,
|
||||||
InternalFieldNames,
|
InternalFieldNames,
|
||||||
} from '../Cascader';
|
} from '../Cascader';
|
||||||
import { toPathKey } from '../utils/commonUtil';
|
import { toPathKey } from '../utils/commonUtil';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { isValidElement } from '../../_util/props-util';
|
||||||
|
import { cloneElement } from '../../_util/vnode';
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
rawValues: SingleValueType[],
|
rawValues: Ref<SingleValueType[]>,
|
||||||
options: DefaultOptionType[],
|
options: Ref<DefaultOptionType[]>,
|
||||||
fieldNames: InternalFieldNames,
|
fieldNames: Ref<InternalFieldNames>,
|
||||||
multiple: boolean,
|
multiple: Ref<boolean>,
|
||||||
displayRender: CascaderProps['displayRender'],
|
displayRender: Ref<BaseCascaderProps['displayRender']>,
|
||||||
) => {
|
) => {
|
||||||
return React.useMemo(() => {
|
return computed(() => {
|
||||||
const mergedDisplayRender =
|
const mergedDisplayRender =
|
||||||
displayRender ||
|
displayRender.value ||
|
||||||
// Default displayRender
|
// Default displayRender
|
||||||
(labels => {
|
(({ labels }) => {
|
||||||
const mergedLabels = multiple ? labels.slice(-1) : labels;
|
const mergedLabels = multiple.value ? labels.slice(-1) : labels;
|
||||||
const SPLIT = ' / ';
|
const SPLIT = ' / ';
|
||||||
|
|
||||||
if (mergedLabels.every(label => ['string', 'number'].includes(typeof label))) {
|
if (mergedLabels.every(label => ['string', 'number'].includes(typeof label))) {
|
||||||
return mergedLabels.join(SPLIT);
|
return mergedLabels.join(SPLIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If exist non-string value, use ReactNode instead
|
// If exist non-string value, use VueNode instead
|
||||||
return mergedLabels.reduce((list, label, index) => {
|
return mergedLabels.reduce((list, label, index) => {
|
||||||
const keyedLabel = React.isValidElement(label)
|
const keyedLabel = isValidElement(label) ? cloneElement(label, { key: index }) : label;
|
||||||
? React.cloneElement(label, { key: index })
|
|
||||||
: label;
|
|
||||||
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return [keyedLabel];
|
return [keyedLabel];
|
||||||
|
@ -41,13 +42,13 @@ export default (
|
||||||
}, []);
|
}, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
return rawValues.map(valueCells => {
|
return rawValues.value.map(valueCells => {
|
||||||
const valueOptions = toPathOptions(valueCells, options, fieldNames);
|
const valueOptions = toPathOptions(valueCells, options.value, fieldNames.value);
|
||||||
|
|
||||||
const label = mergedDisplayRender(
|
const label = mergedDisplayRender({
|
||||||
valueOptions.map(({ option, value }) => option?.[fieldNames.label] ?? value),
|
labels: valueOptions.map(({ option, value }) => option?.[fieldNames.value.label] ?? value),
|
||||||
valueOptions.map(({ option }) => option),
|
selectedOptions: valueOptions.map(({ option }) => option),
|
||||||
);
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
|
@ -55,5 +56,5 @@ export default (
|
||||||
valueCells,
|
valueCells,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [rawValues, options, fieldNames, displayRender, multiple]);
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,13 +10,11 @@ export interface OptionsInfo {
|
||||||
pathKeyEntities: Record<string, DataEntity>;
|
pathKeyEntities: Record<string, DataEntity>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetEntities = () => OptionsInfo['pathKeyEntities'];
|
|
||||||
|
|
||||||
/** Lazy parse options data into conduct-able info to avoid perf issue in single mode */
|
/** Lazy parse options data into conduct-able info to avoid perf issue in single mode */
|
||||||
export default (options: Ref<DefaultOptionType[]>, fieldNames: Ref<InternalFieldNames>) => {
|
export default (options: Ref<DefaultOptionType[]>, fieldNames: Ref<InternalFieldNames>) => {
|
||||||
const entities = computed(() => {
|
const entities = computed(() => {
|
||||||
return (
|
return (
|
||||||
convertDataToEntities(options as any, {
|
convertDataToEntities(options.value as any, {
|
||||||
fieldNames: fieldNames.value,
|
fieldNames: fieldNames.value,
|
||||||
initWrapper: wrapper => ({
|
initWrapper: wrapper => ({
|
||||||
...wrapper,
|
...wrapper,
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import type { CascaderProps, ShowSearchType } from '../Cascader';
|
import type { BaseCascaderProps, ShowSearchType } from '../Cascader';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { computed } from 'vue';
|
import { ref, watchEffect } from 'vue';
|
||||||
import { warning } from '../../vc-util/warning';
|
import { warning } from '../../vc-util/warning';
|
||||||
|
|
||||||
// Convert `showSearch` to unique config
|
// Convert `showSearch` to unique config
|
||||||
export default function useSearchConfig(showSearch?: Ref<CascaderProps['showSearch']>) {
|
export default function useSearchConfig(showSearch?: Ref<BaseCascaderProps['showSearch']>) {
|
||||||
return computed(() => {
|
const mergedShowSearch = ref(false);
|
||||||
|
const mergedSearchConfig = ref<ShowSearchType>({});
|
||||||
|
watchEffect(() => {
|
||||||
if (!showSearch.value) {
|
if (!showSearch.value) {
|
||||||
return [false, {}];
|
mergedShowSearch.value = false;
|
||||||
|
mergedSearchConfig.value = {};
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchConfig: ShowSearchType = {
|
let searchConfig: ShowSearchType = {
|
||||||
|
@ -29,7 +33,9 @@ export default function useSearchConfig(showSearch?: Ref<CascaderProps['showSear
|
||||||
warning(false, "'limit' of showSearch should be positive number or false.");
|
warning(false, "'limit' of showSearch should be positive number or false.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mergedShowSearch.value = true;
|
||||||
return [true, searchConfig];
|
mergedSearchConfig.value = searchConfig;
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
|
return { showSearch: mergedShowSearch, searchConfig: mergedSearchConfig };
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { Key } from '../../_util/type';
|
||||||
import type { SingleValueType, DefaultOptionType, InternalFieldNames } from '../Cascader';
|
import type { SingleValueType, DefaultOptionType, InternalFieldNames } from '../Cascader';
|
||||||
import type { GetEntities } from '../hooks/useEntities';
|
import type { OptionsInfo } from '../hooks/useEntities';
|
||||||
|
|
||||||
export function formatStrategyValues(pathKeys: Key[], getKeyPathEntities: GetEntities) {
|
export function formatStrategyValues(
|
||||||
|
pathKeys: Key[],
|
||||||
|
keyPathEntities: OptionsInfo['pathKeyEntities'],
|
||||||
|
) {
|
||||||
const valueSet = new Set(pathKeys);
|
const valueSet = new Set(pathKeys);
|
||||||
const keyPathEntities = getKeyPathEntities();
|
|
||||||
|
|
||||||
return pathKeys.filter(key => {
|
return pathKeys.filter(key => {
|
||||||
const entity = keyPathEntities[key];
|
const entity = keyPathEntities[key];
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import type { CSSProperties, ExtractPropTypes, PropType, VNode } from 'vue';
|
import type { CSSProperties, ExtractPropTypes, PropType, VNode } from 'vue';
|
||||||
import PropTypes from '../_util/vue-types';
|
import PropTypes from '../_util/vue-types';
|
||||||
import { initDefaultProps } from '../_util/props-util';
|
import { initDefaultProps, isValidElement } from '../_util/props-util';
|
||||||
import isMobile from '../vc-util/isMobile';
|
import isMobile from '../vc-util/isMobile';
|
||||||
import KeyCode from '../_util/KeyCode';
|
import KeyCode from '../_util/KeyCode';
|
||||||
import { toReactive } from '../_util/toReactive';
|
import { toReactive } from '../_util/toReactive';
|
||||||
|
@ -38,6 +38,7 @@ import classNames from '../_util/classNames';
|
||||||
import createRef from '../_util/createRef';
|
import createRef from '../_util/createRef';
|
||||||
import type { BaseOptionType } from './Select';
|
import type { BaseOptionType } from './Select';
|
||||||
import useInjectLegacySelectContext from '../vc-tree-select/LegacyContext';
|
import useInjectLegacySelectContext from '../vc-tree-select/LegacyContext';
|
||||||
|
import { cloneElement } from '../_util/vnode';
|
||||||
|
|
||||||
const DEFAULT_OMIT_PROPS = [
|
const DEFAULT_OMIT_PROPS = [
|
||||||
'value',
|
'value',
|
||||||
|
@ -543,6 +544,8 @@ export default defineComponent({
|
||||||
focus: onContainerFocus,
|
focus: onContainerFocus,
|
||||||
blur: onContainerBlur,
|
blur: onContainerBlur,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Give focus back of Select
|
||||||
const activeTimeoutIds: any[] = [];
|
const activeTimeoutIds: any[] = [];
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -591,7 +594,7 @@ export default defineComponent({
|
||||||
triggerOpen,
|
triggerOpen,
|
||||||
() => {
|
() => {
|
||||||
if (triggerOpen.value) {
|
if (triggerOpen.value) {
|
||||||
const newWidth = Math.ceil(containerRef.value.offsetWidth);
|
const newWidth = Math.ceil(containerRef.value?.offsetWidth);
|
||||||
if (containerWidth.value !== newWidth && !Number.isNaN(newWidth)) {
|
if (containerWidth.value !== newWidth && !Number.isNaN(newWidth)) {
|
||||||
containerWidth.value = newWidth;
|
containerWidth.value = newWidth;
|
||||||
}
|
}
|
||||||
|
@ -740,7 +743,7 @@ export default defineComponent({
|
||||||
onInternalSearch('', false, false);
|
onInternalSearch('', false, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!disabled && allowClear && (displayValues.length || mergedSearchValue)) {
|
if (!disabled && allowClear && (displayValues.length || mergedSearchValue.value)) {
|
||||||
clearNode = (
|
clearNode = (
|
||||||
<TransBtn
|
<TransBtn
|
||||||
class={`${prefixCls}-clear`}
|
class={`${prefixCls}-clear`}
|
||||||
|
@ -800,7 +803,15 @@ export default defineComponent({
|
||||||
v-slots={{
|
v-slots={{
|
||||||
default: () => {
|
default: () => {
|
||||||
return customizeRawInputElement ? (
|
return customizeRawInputElement ? (
|
||||||
customizeRawInputElement
|
isValidElement(customizeRawInputElement) &&
|
||||||
|
cloneElement(
|
||||||
|
customizeRawInputElement,
|
||||||
|
{
|
||||||
|
ref: selectorDomRef,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Selector
|
<Selector
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -161,8 +161,8 @@ const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
|
||||||
return (
|
return (
|
||||||
<Trigger
|
<Trigger
|
||||||
{...props}
|
{...props}
|
||||||
showAction={[]}
|
showAction={onPopupVisibleChange ? ['click'] : []}
|
||||||
hideAction={[]}
|
hideAction={onPopupVisibleChange ? ['click'] : []}
|
||||||
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
|
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
|
||||||
builtinPlacements={builtInPlacements.value}
|
builtinPlacements={builtInPlacements.value}
|
||||||
prefixCls={dropdownPrefixCls}
|
prefixCls={dropdownPrefixCls}
|
||||||
|
|
|
@ -174,11 +174,11 @@ const Input = defineComponent({
|
||||||
this.VCSelectContainerEvent?.focus(args[0]);
|
this.VCSelectContainerEvent?.focus(args[0]);
|
||||||
},
|
},
|
||||||
onBlur: (...args: any[]) => {
|
onBlur: (...args: any[]) => {
|
||||||
this.blurTimeout = setTimeout(() => {
|
// this.blurTimeout = setTimeout(() => {
|
||||||
onOriginBlur && onOriginBlur(args[0]);
|
onOriginBlur && onOriginBlur(args[0]);
|
||||||
onBlur && onBlur(args[0]);
|
onBlur && onBlur(args[0]);
|
||||||
this.VCSelectContainerEvent?.blur(args[0]);
|
this.VCSelectContainerEvent?.blur(args[0]);
|
||||||
}, 200);
|
// }, 200);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputNode.type === 'textarea' ? {} : { type: 'search' },
|
inputNode.type === 'textarea' ? {} : { type: 'search' },
|
||||||
|
|
Loading…
Reference in New Issue