refactor: cascader

refactor-cascader
tangjinzhou 2022-01-20 21:29:50 +08:00
parent e08c6da9b5
commit 616478be1f
22 changed files with 16 additions and 1410 deletions

View File

@ -1,10 +1,5 @@
import type {
ShowSearchType,
FieldNames,
BaseOptionType,
DefaultOptionType,
} from '../vc-cascader2';
import VcCascader, { cascaderProps as vcCascaderProps } from '../vc-cascader2';
import type { ShowSearchType, FieldNames, BaseOptionType, DefaultOptionType } from '../vc-cascader';
import VcCascader, { cascaderProps as vcCascaderProps } from '../vc-cascader';
import RightOutlined from '@ant-design/icons-vue/RightOutlined';
import RedoOutlined from '@ant-design/icons-vue/RedoOutlined';
import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
@ -22,7 +17,7 @@ import type { SizeType } from '../config-provider';
import devWarning from '../vc-util/devWarning';
import { getTransitionName } from '../_util/transition';
import { useInjectFormItemContext } from '../form';
import type { ValueType } from '../vc-cascader2/Cascader';
import type { ValueType } from '../vc-cascader/Cascader';
// Align the design since we use `rc-select` in root. This help:
// - List search content will show all content

View File

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

View File

@ -1,388 +0,0 @@
import { getComponent, getSlot, hasProp, getEvents } from '../_util/props-util';
import PropTypes from '../_util/vue-types';
import Trigger from '../vc-trigger';
import Menus from './Menus';
import KeyCode from '../_util/KeyCode';
import arrayTreeFilter from 'array-tree-filter';
import shallowEqualArrays from 'shallow-equal/arrays';
import BaseMixin from '../_util/BaseMixin';
import { cloneElement } from '../_util/vnode';
import { defineComponent } from 'vue';
import isEqual from 'lodash-es/isEqual';
const BUILT_IN_PLACEMENTS = {
bottomLeft: {
points: ['tl', 'bl'],
offset: [0, 4],
overflow: {
adjustX: 1,
adjustY: 1,
},
},
topLeft: {
points: ['bl', 'tl'],
offset: [0, -4],
overflow: {
adjustX: 1,
adjustY: 1,
},
},
bottomRight: {
points: ['tr', 'br'],
offset: [0, 4],
overflow: {
adjustX: 1,
adjustY: 1,
},
},
topRight: {
points: ['br', 'tr'],
offset: [0, -4],
overflow: {
adjustX: 1,
adjustY: 1,
},
},
};
export default defineComponent({
name: 'Cascader',
mixins: [BaseMixin],
inheritAttrs: false,
// model: {
// prop: 'value',
// event: 'change',
// },
props: {
value: PropTypes.array,
defaultValue: PropTypes.array,
options: PropTypes.array,
// onChange: PropTypes.func,
// onPopupVisibleChange: PropTypes.func,
popupVisible: PropTypes.looseBool,
disabled: PropTypes.looseBool.def(false),
transitionName: PropTypes.string.def(''),
popupClassName: PropTypes.string.def(''),
popupStyle: PropTypes.object.def(() => ({})),
popupPlacement: PropTypes.string.def('bottomLeft'),
prefixCls: PropTypes.string.def('rc-cascader'),
dropdownMenuColumnStyle: PropTypes.object,
builtinPlacements: PropTypes.object.def(BUILT_IN_PLACEMENTS),
loadData: PropTypes.func,
changeOnSelect: PropTypes.looseBool,
// onKeyDown: PropTypes.func,
expandTrigger: PropTypes.string.def('click'),
fieldNames: PropTypes.object.def(() => ({
label: 'label',
value: 'value',
children: 'children',
})),
expandIcon: PropTypes.any,
loadingIcon: PropTypes.any,
getPopupContainer: PropTypes.func,
},
data() {
let initialValue = [];
const { value, defaultValue, popupVisible } = this;
if (hasProp(this, 'value')) {
initialValue = value || [];
} else if (hasProp(this, 'defaultValue')) {
initialValue = defaultValue || [];
}
this.children = undefined;
// warning(!('filedNames' in props),
// '`filedNames` of Cascader is a typo usage and deprecated, please use `fieldNames` instead.');
this.defaultFieldNames = { label: 'label', value: 'value', children: 'children' };
return {
sPopupVisible: popupVisible,
sActiveValue: initialValue,
sValue: initialValue,
};
},
watch: {
value(val, oldValue) {
if (!shallowEqualArrays(val, oldValue)) {
const newValues = {
sValue: val || [],
};
// allow activeValue diff from value
// https://github.com/ant-design/ant-design/issues/2767
if (!hasProp(this, 'loadData')) {
newValues.sActiveValue = val || [];
}
this.setState(newValues);
}
},
popupVisible(val) {
this.setState({
sPopupVisible: val,
});
},
},
methods: {
getPopupDOMNode() {
return this.trigger.getPopupDomNode();
},
getFieldName(name) {
const { defaultFieldNames, fieldNames } = this;
return fieldNames[name] || defaultFieldNames[name];
},
getFieldNames() {
return this.fieldNames;
},
getCurrentLevelOptions() {
const { options = [], sActiveValue = [] } = this;
const result = arrayTreeFilter(
options,
(o, level) => isEqual(o[this.getFieldName('value')], sActiveValue[level]),
{ childrenKeyName: this.getFieldName('children') },
);
if (result[result.length - 2]) {
return result[result.length - 2][this.getFieldName('children')];
}
return [...options].filter(o => !o.disabled);
},
getActiveOptions(activeValue) {
return arrayTreeFilter(
this.options || [],
(o, level) => isEqual(o[this.getFieldName('value')], activeValue[level]),
{ childrenKeyName: this.getFieldName('children') },
);
},
setPopupVisible(popupVisible) {
if (!hasProp(this, 'popupVisible')) {
this.setState({ sPopupVisible: popupVisible });
}
// sync activeValue with value when panel open
if (popupVisible && !this.sPopupVisible) {
this.setState({
sActiveValue: this.sValue,
});
}
this.__emit('popupVisibleChange', popupVisible);
},
handleChange(options, setProps, e) {
if (e.type !== 'keydown' || e.keyCode === KeyCode.ENTER) {
const value = options.map(o => o[this.getFieldName('value')]);
this.__emit('change', value, options);
this.setPopupVisible(setProps.visible);
}
},
handlePopupVisibleChange(popupVisible) {
this.setPopupVisible(popupVisible);
},
handleMenuSelect(targetOption, menuIndex, e) {
// Keep focused state for keyboard support
const triggerNode = this.trigger.getRootDomNode();
if (triggerNode && triggerNode.focus) {
triggerNode.focus();
}
const { changeOnSelect, loadData, expandTrigger } = this;
if (!targetOption || targetOption.disabled) {
return;
}
let { sActiveValue } = this;
sActiveValue = sActiveValue.slice(0, menuIndex + 1);
sActiveValue[menuIndex] = targetOption[this.getFieldName('value')];
const activeOptions = this.getActiveOptions(sActiveValue);
if (
targetOption.isLeaf === false &&
!targetOption[this.getFieldName('children')] &&
loadData
) {
if (changeOnSelect) {
this.handleChange(activeOptions, { visible: true }, e);
}
this.setState({ sActiveValue });
loadData(activeOptions);
return;
}
const newState = {};
if (
!targetOption[this.getFieldName('children')] ||
!targetOption[this.getFieldName('children')].length
) {
this.handleChange(activeOptions, { visible: false }, e);
// set value to activeValue when select leaf option
newState.sValue = sActiveValue;
// add e.type judgement to prevent `onChange` being triggered by mouseEnter
} else if (changeOnSelect && (e.type === 'click' || e.type === 'keydown')) {
if (expandTrigger === 'hover') {
this.handleChange(activeOptions, { visible: false }, e);
} else {
this.handleChange(activeOptions, { visible: true }, e);
}
// set value to activeValue on every select
newState.sValue = sActiveValue;
}
newState.sActiveValue = sActiveValue;
// not change the value by keyboard
if (hasProp(this, 'value') || (e.type === 'keydown' && e.keyCode !== KeyCode.ENTER)) {
delete newState.sValue;
}
this.setState(newState);
},
handleItemDoubleClick() {
const { changeOnSelect } = this.$props;
if (changeOnSelect) {
this.setPopupVisible(false);
}
},
handleKeyDown(e) {
const children = this.children;
// https://github.com/ant-design/ant-design/issues/6717
// Don't bind keyboard support when children specify the onKeyDown
if (children) {
const keydown = getEvents(children).onKeydown;
if (keydown) {
keydown(e);
return;
}
}
const activeValue = [...this.sActiveValue];
const currentLevel = activeValue.length - 1 < 0 ? 0 : activeValue.length - 1;
const currentOptions = this.getCurrentLevelOptions();
const currentIndex = currentOptions
.map(o => o[this.getFieldName('value')])
.findIndex(val => isEqual(activeValue[currentLevel], val));
if (
e.keyCode !== KeyCode.DOWN &&
e.keyCode !== KeyCode.UP &&
e.keyCode !== KeyCode.LEFT &&
e.keyCode !== KeyCode.RIGHT &&
e.keyCode !== KeyCode.ENTER &&
e.keyCode !== KeyCode.SPACE &&
e.keyCode !== KeyCode.BACKSPACE &&
e.keyCode !== KeyCode.ESC &&
e.keyCode !== KeyCode.TAB
) {
return;
}
// Press any keys above to reopen menu
if (
!this.sPopupVisible &&
e.keyCode !== KeyCode.BACKSPACE &&
e.keyCode !== KeyCode.LEFT &&
e.keyCode !== KeyCode.RIGHT &&
e.keyCode !== KeyCode.ESC &&
e.keyCode !== KeyCode.TAB
) {
this.setPopupVisible(true);
return;
}
if (e.keyCode === KeyCode.DOWN || e.keyCode === KeyCode.UP) {
e.preventDefault();
let nextIndex = currentIndex;
if (nextIndex !== -1) {
if (e.keyCode === KeyCode.DOWN) {
nextIndex += 1;
nextIndex = nextIndex >= currentOptions.length ? 0 : nextIndex;
} else {
nextIndex -= 1;
nextIndex = nextIndex < 0 ? currentOptions.length - 1 : nextIndex;
}
} else {
nextIndex = 0;
}
activeValue[currentLevel] = currentOptions[nextIndex][this.getFieldName('value')];
} else if (e.keyCode === KeyCode.LEFT || e.keyCode === KeyCode.BACKSPACE) {
e.preventDefault();
activeValue.splice(activeValue.length - 1, 1);
} else if (e.keyCode === KeyCode.RIGHT) {
e.preventDefault();
if (
currentOptions[currentIndex] &&
currentOptions[currentIndex][this.getFieldName('children')]
) {
activeValue.push(
currentOptions[currentIndex][this.getFieldName('children')][0][
this.getFieldName('value')
],
);
}
} else if (e.keyCode === KeyCode.ESC || e.keyCode === KeyCode.TAB) {
this.setPopupVisible(false);
return;
}
if (!activeValue || activeValue.length === 0) {
this.setPopupVisible(false);
}
const activeOptions = this.getActiveOptions(activeValue);
const targetOption = activeOptions[activeOptions.length - 1];
this.handleMenuSelect(targetOption, activeOptions.length - 1, e);
this.__emit('keydown', e);
},
saveTrigger(node) {
this.trigger = node;
},
},
render() {
const {
$props,
sActiveValue,
handleMenuSelect,
sPopupVisible,
handlePopupVisibleChange,
handleKeyDown,
} = this;
const {
prefixCls,
transitionName,
popupClassName,
options = [],
disabled,
builtinPlacements,
popupPlacement,
...restProps
} = $props;
// Did not show popup when there is no options
let menus = <div />;
let emptyMenuClassName = '';
if (options && options.length > 0) {
const loadingIcon = getComponent(this, 'loadingIcon');
const expandIcon = getComponent(this, 'expandIcon') || '>';
const menusProps = {
...$props,
...this.$attrs,
fieldNames: this.getFieldNames(),
defaultFieldNames: this.defaultFieldNames,
activeValue: sActiveValue,
visible: sPopupVisible,
loadingIcon,
expandIcon,
onSelect: handleMenuSelect,
onItemDoubleClick: this.handleItemDoubleClick,
};
menus = <Menus {...menusProps} />;
} else {
emptyMenuClassName = ` ${prefixCls}-menus-empty`;
}
const triggerProps = {
...restProps,
...this.$attrs,
disabled,
popupPlacement,
builtinPlacements,
popupTransitionName: transitionName,
action: disabled ? [] : ['click'],
popupVisible: disabled ? false : sPopupVisible,
prefixCls: `${prefixCls}-menus`,
popupClassName: popupClassName + emptyMenuClassName,
popup: menus,
onPopupVisibleChange: handlePopupVisibleChange,
ref: this.saveTrigger,
};
const children = getSlot(this);
this.children = children;
return (
<Trigger {...triggerProps}>
{children &&
cloneElement(children[0], {
onKeydown: handleKeyDown,
tabindex: disabled ? undefined : 0,
})}
</Trigger>
);
},
});

View File

@ -1,182 +0,0 @@
import { getComponent, findDOMNode } from '../_util/props-util';
import PropTypes from '../_util/vue-types';
import arrayTreeFilter from 'array-tree-filter';
import BaseMixin from '../_util/BaseMixin';
import isEqual from 'lodash-es/isEqual';
export default {
name: 'CascaderMenus',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
value: PropTypes.array.def([]),
activeValue: PropTypes.array.def([]),
options: PropTypes.array,
prefixCls: PropTypes.string.def('rc-cascader-menus'),
expandTrigger: PropTypes.string.def('click'),
// onSelect: PropTypes.func,
visible: PropTypes.looseBool.def(false),
dropdownMenuColumnStyle: PropTypes.object,
defaultFieldNames: PropTypes.object,
fieldNames: PropTypes.object,
expandIcon: PropTypes.any,
loadingIcon: PropTypes.any,
},
data() {
this.menuItems = {};
return {};
},
watch: {
visible(val) {
if (val) {
this.$nextTick(() => {
this.scrollActiveItemToView();
});
}
},
},
mounted() {
this.$nextTick(() => {
this.scrollActiveItemToView();
});
},
methods: {
getFieldName(name) {
const { fieldNames, defaultFieldNames } = this.$props;
//
return fieldNames[name] || defaultFieldNames[name];
},
getOption(option, menuIndex) {
const { prefixCls, expandTrigger } = this;
const loadingIcon = getComponent(this, 'loadingIcon');
const expandIcon = getComponent(this, 'expandIcon');
const onSelect = e => {
this.__emit('select', option, menuIndex, e);
};
const onItemDoubleClick = e => {
this.__emit('itemDoubleClick', option, menuIndex, e);
};
const key = option[this.getFieldName('value')];
let expandProps = {
onClick: onSelect,
onDblclick: onItemDoubleClick,
};
let menuItemCls = `${prefixCls}-menu-item`;
let expandIconNode = null;
const hasChildren =
option[this.getFieldName('children')] && option[this.getFieldName('children')].length > 0;
if (hasChildren || option.isLeaf === false) {
menuItemCls += ` ${prefixCls}-menu-item-expand`;
if (!option.loading) {
expandIconNode = <span class={`${prefixCls}-menu-item-expand-icon`}>{expandIcon}</span>;
}
}
if (expandTrigger === 'hover' && (hasChildren || option.isLeaf === false)) {
expandProps = {
onMouseenter: this.delayOnSelect.bind(this, onSelect),
onMouseleave: this.delayOnSelect.bind(this),
onClick: onSelect,
};
}
if (this.isActiveOption(option, menuIndex)) {
menuItemCls += ` ${prefixCls}-menu-item-active`;
expandProps.ref = this.saveMenuItem(menuIndex);
}
if (option.disabled) {
menuItemCls += ` ${prefixCls}-menu-item-disabled`;
}
let loadingIconNode = null;
if (option.loading) {
menuItemCls += ` ${prefixCls}-menu-item-loading`;
loadingIconNode = loadingIcon || null;
}
let title = '';
if (option.title) {
title = option.title;
} else if (typeof option[this.getFieldName('label')] === 'string') {
title = option[this.getFieldName('label')];
}
return (
<li
key={Array.isArray(key) ? key.join('__ant__') : key}
class={menuItemCls}
title={title}
{...expandProps}
role="menuitem"
onMousedown={e => e.preventDefault()}
>
{option[this.getFieldName('label')]}
{expandIconNode}
{loadingIconNode}
</li>
);
},
getActiveOptions(values) {
const activeValue = values || this.activeValue;
const options = this.options;
return arrayTreeFilter(
options,
(o, level) => isEqual(o[this.getFieldName('value')], activeValue[level]),
{ childrenKeyName: this.getFieldName('children') },
);
},
getShowOptions() {
const { options } = this;
const result = this.getActiveOptions()
.map(activeOption => activeOption[this.getFieldName('children')])
.filter(activeOption => !!activeOption);
result.unshift(options);
return result;
},
delayOnSelect(onSelect, ...args) {
if (this.delayTimer) {
clearTimeout(this.delayTimer);
this.delayTimer = null;
}
if (typeof onSelect === 'function') {
this.delayTimer = setTimeout(() => {
onSelect(args);
this.delayTimer = null;
}, 150);
}
},
scrollActiveItemToView() {
// scroll into view
const optionsLength = this.getShowOptions().length;
for (let i = 0; i < optionsLength; i++) {
const itemComponent = this.menuItems[i];
if (itemComponent) {
const target = findDOMNode(itemComponent);
target.parentNode.scrollTop = target.offsetTop;
}
}
},
isActiveOption(option, menuIndex) {
const { activeValue = [] } = this;
return isEqual(activeValue[menuIndex], option[this.getFieldName('value')]);
},
saveMenuItem(index) {
return node => {
this.menuItems[index] = node;
};
},
},
render() {
const { prefixCls, dropdownMenuColumnStyle } = this;
return (
<div>
{this.getShowOptions().map((options, menuIndex) => (
<ul class={`${prefixCls}-menu`} key={menuIndex} style={dropdownMenuColumnStyle}>
{options.map(option => this.getOption(option, menuIndex))}
</ul>
))}
</div>
);
},
};

View File

@ -1,7 +1,7 @@
import type { RefOptionListProps } from '../../vc-select/OptionList';
import type { Key } from 'ant-design-vue/es/_util/type';
import type { Ref, SetupContext } from 'vue';
import { ref, watchEffect } from 'vue';
import { computed, ref, watchEffect } from 'vue';
import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader';
import { toPathKey } from '../utils/commonUtil';
import { useBaseProps } from '../../vc-select';
@ -16,8 +16,8 @@ export default (
containerRef: Ref<HTMLElement>,
onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void,
) => {
const { direction, searchValue, toggleOpen, open } = useBaseProps();
const rtl = direction === 'rtl';
const baseProps = useBaseProps();
const rtl = computed(() => baseProps.direction === 'rtl');
const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = [
ref<Key[]>([]),
ref<number>(),
@ -31,7 +31,6 @@ export default (
const mergedActiveValueCells: Key[] = [];
const len = activeValueCells.value.length;
// Fill validate active value cells and index
for (let i = 0; i < len; i += 1) {
// Mark the active index for current options
@ -99,7 +98,7 @@ export default (
const nextActiveCells = validActiveValueCells.value.slice(0, -1);
internalSetActiveValueCells(nextActiveCells);
} else {
toggleOpen(false);
baseProps.toggleOpen(false);
}
};
@ -139,7 +138,7 @@ export default (
}
case KeyCode.LEFT: {
if (rtl) {
if (rtl.value) {
nextColumn();
} else {
prevColumn();
@ -148,7 +147,7 @@ export default (
}
case KeyCode.RIGHT: {
if (rtl) {
if (rtl.value) {
prevColumn();
} else {
nextColumn();
@ -157,7 +156,7 @@ export default (
}
case KeyCode.BACKSPACE: {
if (!searchValue) {
if (!baseProps.searchValue) {
prevColumn();
}
break;
@ -176,7 +175,7 @@ export default (
// >>> Close
case KeyCode.ESC: {
toggleOpen(false);
baseProps.toggleOpen(false);
if (open) {
event.stopPropagation();

View File

@ -1,168 +0,0 @@
.effect() {
animation-duration: 0.3s;
animation-fill-mode: both;
transform-origin: 0 0;
}
.rc-cascader {
font-size: 12px;
&-menus {
font-size: 12px;
overflow: hidden;
background: #fff;
position: absolute;
border: 1px solid #d9d9d9;
border-radius: 6px;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.17);
white-space: nowrap;
&-hidden {
display: none;
}
&.slide-up-enter,
&.slide-up-appear {
.effect();
opacity: 0;
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
animation-play-state: paused;
}
&.slide-up-leave {
.effect();
opacity: 1;
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
animation-play-state: paused;
}
&.slide-up-enter.slide-up-enter-active&-placement-bottomLeft,
&.slide-up-appear.slide-up-appear-active&-placement-bottomLeft {
animation-name: SlideUpIn;
animation-play-state: running;
}
&.slide-up-enter.slide-up-enter-active&-placement-topLeft,
&.slide-up-appear.slide-up-appear-active&-placement-topLeft {
animation-name: SlideDownIn;
animation-play-state: running;
}
&.slide-up-leave.slide-up-leave-active&-placement-bottomLeft {
animation-name: SlideUpOut;
animation-play-state: running;
}
&.slide-up-leave.slide-up-leave-active&-placement-topLeft {
animation-name: SlideDownOut;
animation-play-state: running;
}
}
&-menu {
display: inline-block;
width: 100px;
height: 192px;
list-style: none;
margin: 0;
padding: 0;
border-right: 1px solid #e9e9e9;
overflow: auto;
&:last-child {
border-right: 0;
}
}
&-menu-item {
height: 32px;
line-height: 32px;
padding: 0 16px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.3s ease;
position: relative;
&:hover {
background: tint(#2db7f5, 90%);
}
&-disabled {
cursor: not-allowed;
color: #ccc;
&:hover {
background: transparent;
}
}
&-loading:after {
position: absolute;
right: 12px;
content: 'loading';
color: #aaa;
font-style: italic;
}
&-active {
background: tint(#2db7f5, 80%);
&:hover {
background: tint(#2db7f5, 80%);
}
}
&-expand {
position: relative;
&:after {
content: '>';
font-size: 12px;
color: #999;
position: absolute;
right: 16px;
line-height: 32px;
}
}
}
}
@keyframes SlideUpIn {
0% {
opacity: 0;
transform-origin: 0% 0%;
transform: scaleY(0.8);
}
100% {
opacity: 1;
transform-origin: 0% 0%;
transform: scaleY(1);
}
}
@keyframes SlideUpOut {
0% {
opacity: 1;
transform-origin: 0% 0%;
transform: scaleY(1);
}
100% {
opacity: 0;
transform-origin: 0% 0%;
transform: scaleY(0.8);
}
}
@keyframes SlideDownIn {
0% {
opacity: 0;
transform-origin: 0% 100%;
transform: scaleY(0.8);
}
100% {
opacity: 1;
transform-origin: 0% 100%;
transform: scaleY(1);
}
}
@keyframes SlideDownOut {
0% {
opacity: 1;
transform-origin: 0% 100%;
transform: scaleY(1);
}
100% {
opacity: 0;
transform-origin: 0% 100%;
transform: scaleY(0.8);
}
}

View File

@ -1,3 +0,0 @@
// based on rc-cascader 0.17.4
import Cascader from './Cascader';
export default Cascader;

View File

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