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 LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; import LeftOutlined from '@ant-design/icons-vue/LeftOutlined'; import getIcons from '../select/utils/iconUtil'; import type { VueNode } from '../_util/type'; import { withInstall } from '../_util/type'; import omit from '../_util/omit'; import { computed, defineComponent, ref, watchEffect } from 'vue'; import type { ExtractPropTypes, PropType } from 'vue'; import PropTypes from '../_util/vue-types'; import { initDefaultProps } from '../_util/props-util'; import useConfigInject from '../_util/hooks/useConfigInject'; import classNames from '../_util/classNames'; import type { SizeType } from '../config-provider'; import devWarning from '../vc-util/devWarning'; import { getTransitionName } from '../_util/transition'; import { useInjectFormItemContext } from '../form'; import type { ValueType } from '../vc-cascader/Cascader'; // Align the design since we use `rc-select` in root. This help: // - List search content will show all content // - Hover opacity style // - Search filter match case export type { BaseOptionType, DefaultOptionType, ShowSearchType }; export type FieldNamesType = FieldNames; export type FilledFieldNamesType = Required; 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 = ( {originWorld} ); } fillCells.push(originWorld); }); return fillCells; } const defaultSearchRender: ShowSearchType['render'] = ({ inputValue, path, prefixCls, fieldNames, }) => { const optionList: VueNode[] = []; // We do lower here to save perf const lower = inputValue.toLowerCase(); path.forEach((node, index) => { if (index !== 0) { optionList.push(' / '); } let label = (node as any)[fieldNames.label!]; const type = typeof label; if (type === 'string' || type === 'number') { label = highlightKeyword(String(label), lower, prefixCls); } optionList.push(label); }); return optionList; }; export interface CascaderOptionType extends DefaultOptionType { isLeaf?: boolean; loading?: boolean; children?: CascaderOptionType[]; [key: string]: any; } export function cascaderProps() { return { ...omit(vcCascaderProps(), ['customSlots', 'checkable', 'options']), multiple: { type: Boolean, default: undefined }, size: String as PropType, bordered: { type: Boolean, default: undefined }, suffixIcon: PropTypes.any, options: Array as PropType, 'onUpdate:value': Function as PropType<(value: ValueType) => void>, }; } export type CascaderProps = Partial>>; export interface CascaderRef { focus: () => void; blur: () => void; } const Cascader = defineComponent({ name: 'ACascader', inheritAttrs: false, props: initDefaultProps(cascaderProps(), { bordered: true, choiceTransitionName: '', allowClear: true, }), setup(props, { attrs, expose, slots, emit }) { const formItemContext = useInjectFormItemContext(); const { prefixCls: cascaderPrefixCls, rootPrefixCls, getPrefixCls, direction, getPopupContainer, renderEmpty, size, } = useConfigInject('cascader', props); const prefixCls = computed(() => getPrefixCls('select', props.prefixCls)); const isRtl = computed(() => direction.value === 'rtl'); // =================== Warning ===================== if (process.env.NODE_ENV !== 'production') { watchEffect(() => { devWarning( !props.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(); 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 ? : ; } const loadingIcon = ( ); // ===================== Icons ===================== const { suffixIcon, removeIcon, clearIcon } = getIcons( { ...props, multiple, prefixCls: prefixCls.value, }, slots, ); return ( , }} displayRender={props.displayRender || slots.displayRender} maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder} onChange={handleChange} onBlur={handleBlur} v-slots={slots} ref={selectRef} /> ); }; }, }); export default withInstall(Cascader);