import type { GenerateConfig } from '../vc-select/generate'; import generateSelector from '../vc-select/generate'; import TreeNode from './TreeNode'; import type { DefaultValueType, DataNode, LabelValueType, RawValueType, ChangeEventExtra, SelectSource, FlattenDataNode, } from './interface'; import { flattenOptions, filterOptions, isValueDisabled, findValueOption, addValue, removeValue, getRawValueLabeled, toArray, fillFieldNames, } from './utils/valueUtil'; import warningProps from './utils/warningPropsUtil'; import { SelectContext } from './Context'; import useTreeData from './hooks/useTreeData'; import useKeyValueMap from './hooks/useKeyValueMap'; import useKeyValueMapping from './hooks/useKeyValueMapping'; import { formatStrategyKeys, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; import { fillAdditionalInfo } from './utils/legacyUtil'; import useSelectValues from './hooks/useSelectValues'; import type { TreeSelectProps } from './props'; import { treeSelectProps } from './props'; import { getLabeledValue } from '../vc-select/utils/valueUtil'; import omit from '../_util/omit'; import { computed, defineComponent, ref, shallowRef, toRef, watch, watchEffect } from 'vue'; import { convertDataToEntities } from '../vc-tree/utils/treeUtil'; import { conductCheck } from '../vc-tree/utils/conductUtil'; import { warning } from '../vc-util/warning'; import { INTERNAL_PROPS_MARK } from '../vc-select/interface/generator'; const OMIT_PROPS: (keyof TreeSelectProps)[] = [ 'expandedKeys' as any, 'treeData', 'treeCheckable', 'showCheckedStrategy', 'searchPlaceholder', 'treeLine', 'treeIcon', 'showTreeIcon', 'switcherIcon', 'treeNodeFilterProp', 'filterTreeNode', 'dropdownPopupAlign', 'treeDefaultExpandAll', 'treeCheckStrictly', 'treeExpandedKeys', 'treeLoadedKeys', 'treeMotion', 'onTreeExpand', 'onTreeLoad', 'labelRender', 'loadData', 'treeDataSimpleMode', 'treeNodeLabelProp', 'treeDefaultExpandedKeys', 'bordered', ]; export default function generate(config: { prefixCls: string; optionList: GenerateConfig['components']['optionList']; }) { const { prefixCls, optionList } = config; const RefSelect = generateSelector({ prefixCls, components: { optionList, }, // Not use generate since we will handle ourself convertChildrenToData: () => null, flattenOptions, // Handle `optionLabelProp` in TreeSelect component getLabeledValue: getLabeledValue as any, filterOptions, isValueDisabled, findValueOption, omitDOMProps: (props: TreeSelectProps) => omit(props, OMIT_PROPS), }); return defineComponent({ name: 'TreeSelect', props: treeSelectProps(), slots: [ 'title', 'placeholder', 'maxTagPlaceholder', 'treeIcon', 'switcherIcon', 'notFoundContent', 'treeCheckable', ], TreeNode, SHOW_ALL, SHOW_PARENT, SHOW_CHILD, setup(props, { expose, slots, attrs }) { const mergedCheckable = computed(() => props.treeCheckable || props.treeCheckStrictly); const mergedMultiple = computed(() => props.multiple || mergedCheckable.value); const treeConduction = computed(() => props.treeCheckable && !props.treeCheckStrictly); const mergedLabelInValue = computed(() => props.treeCheckStrictly || props.labelInValue); // ======================= Tree Data ======================= // FieldNames const mergedFieldNames = computed(() => fillFieldNames(props.fieldNames, true)); // Legacy both support `label` or `title` if not set. // We have to fallback to function to handle this const getTreeNodeTitle = (node: DataNode) => { if (!props.treeData) { return node.title; } if (mergedFieldNames.value?.label) { return node[mergedFieldNames.value.label]; } return node.label || node.title; }; const getTreeNodeLabelProp = (entity: FlattenDataNode) => { const { labelRender, treeNodeLabelProp } = props; const { node } = entity.data; if (labelRender) { return labelRender(entity); } if (treeNodeLabelProp) { return node[treeNodeLabelProp]; } return getTreeNodeTitle(node); }; const mergedTreeData = useTreeData(toRef(props, 'treeData'), toRef(props, 'children'), { getLabelProp: getTreeNodeTitle, simpleMode: toRef(props, 'treeDataSimpleMode'), fieldNames: mergedFieldNames, }); const flattedOptions = computed(() => flattenOptions(mergedTreeData.value)); const [cacheKeyMap, cacheValueMap] = useKeyValueMap(flattedOptions); const [getEntityByKey, getEntityByValue] = useKeyValueMapping(cacheKeyMap, cacheValueMap); // Only generate keyEntities for check conduction when is `treeCheckable` const conductKeyEntities = computed(() => { if (treeConduction.value) { return convertDataToEntities(mergedTreeData.value).keyEntities; } return null; }); // ========================== Ref ========================== const selectRef = ref(); expose({ scrollTo: (...args: any[]) => selectRef.value.scrollTo?.(...args), focus: () => selectRef.value.focus?.(), blur: () => selectRef.value?.blur(), /** @private Internal usage. It's save to remove if `rc-cascader` not use it any longer */ getEntityByValue, }); const valueRef = ref(props.defaultValue); watch( () => props.value, () => { if (props.value !== undefined) { valueRef.value = props.value; } }, { immediate: true }, ); /** Get `missingRawValues` which not exist in the tree yet */ const splitRawValues = (newRawValues: RawValueType[]) => { const missingRawValues = []; const existRawValues = []; // Keep missing value in the cache newRawValues.forEach(val => { if (getEntityByValue(val)) { existRawValues.push(val); } else { missingRawValues.push(val); } }); return { missingRawValues, existRawValues }; }; const rawValues = shallowRef([]); const rawHalfCheckedKeys = shallowRef([]); watchEffect(() => { const valueHalfCheckedKeys: RawValueType[] = []; const newRawValues: RawValueType[] = []; toArray(valueRef.value).forEach(item => { if (item && typeof item === 'object' && 'value' in item) { if (item.halfChecked && props.treeCheckStrictly) { const entity = getEntityByValue(item.value); valueHalfCheckedKeys.push(entity ? entity.key : item.value); } else { newRawValues.push(item.value); } } else { newRawValues.push(item as RawValueType); } }); // We need do conduction of values if (treeConduction.value) { const { missingRawValues, existRawValues } = splitRawValues(newRawValues); const keyList = existRawValues.map(val => getEntityByValue(val).key); const { checkedKeys, halfCheckedKeys } = conductCheck( keyList, true, conductKeyEntities.value, ); rawValues.value = [ ...missingRawValues, ...checkedKeys.map(key => getEntityByKey(key).data.value), ]; rawHalfCheckedKeys.value = halfCheckedKeys; } else { [rawValues.value, rawHalfCheckedKeys.value] = [newRawValues, valueHalfCheckedKeys]; } }); const selectValues = useSelectValues(rawValues, { treeConduction, value: valueRef, showCheckedStrategy: toRef(props, 'showCheckedStrategy'), conductKeyEntities, getEntityByValue, getEntityByKey, getLabelProp: getTreeNodeLabelProp, }); const triggerChange = ( newRawValues: RawValueType[], extra: { triggerValue: RawValueType; selected: boolean }, source: SelectSource, ) => { const { onChange, showCheckedStrategy, treeCheckStrictly } = props; const preValue = valueRef.value; valueRef.value = mergedMultiple.value ? newRawValues : newRawValues[0]; if (onChange) { let eventValues: RawValueType[] = newRawValues; if (treeConduction.value && showCheckedStrategy !== 'SHOW_ALL') { const keyList = newRawValues.map(val => { const entity = getEntityByValue(val); return entity ? entity.key : val; }); const formattedKeyList = formatStrategyKeys( keyList, showCheckedStrategy, conductKeyEntities.value, ); eventValues = formattedKeyList.map(key => { const entity = getEntityByKey(key); return entity ? entity.data.value : key; }); } const { triggerValue, selected } = extra || { triggerValue: undefined, selected: undefined, }; let returnValues = mergedLabelInValue.value ? getRawValueLabeled(eventValues, preValue, getEntityByValue, getTreeNodeLabelProp) : eventValues; // We need fill half check back if (treeCheckStrictly) { const halfValues = rawHalfCheckedKeys.value .map(key => { const entity = getEntityByKey(key); return entity ? entity.data.value : key; }) .filter(val => !eventValues.includes(val)); returnValues = [ ...(returnValues as LabelValueType[]), ...getRawValueLabeled(halfValues, preValue, getEntityByValue, getTreeNodeLabelProp), ]; } const additionalInfo = { // [Legacy] Always return as array contains label & value preValue: selectValues.value, triggerValue, } as ChangeEventExtra; // [Legacy] Fill legacy data if user query. // This is expansive that we only fill when user query // https://github.com/react-component/tree-select/blob/fe33eb7c27830c9ac70cd1fdb1ebbe7bc679c16a/src/Select.jsx let showPosition = true; if (treeCheckStrictly || (source === 'selection' && !selected)) { showPosition = false; } fillAdditionalInfo( additionalInfo, triggerValue, newRawValues, mergedTreeData.value, showPosition, ); if (mergedCheckable.value) { additionalInfo.checked = selected; } else { additionalInfo.selected = selected; } onChange( mergedMultiple.value ? returnValues : returnValues[0], mergedLabelInValue.value ? null : eventValues.map(val => { const entity = getEntityByValue(val); return entity ? entity.data.title : null; }), additionalInfo, ); } }; const onInternalSelect = ( selectValue: RawValueType, option: DataNode, source: SelectSource, ) => { const eventValue = mergedLabelInValue.value ? selectValue : selectValue; if (!mergedMultiple.value) { // Single mode always set value triggerChange([selectValue], { selected: true, triggerValue: selectValue }, source); } else { let newRawValues = addValue(rawValues.value, selectValue); // Add keys if tree conduction if (treeConduction.value) { // Should keep missing values const { missingRawValues, existRawValues } = splitRawValues(newRawValues); const keyList = existRawValues.map(val => getEntityByValue(val).key); const { checkedKeys } = conductCheck(keyList, true, conductKeyEntities.value); newRawValues = [ ...missingRawValues, ...checkedKeys.map(key => getEntityByKey(key).data.value), ]; } triggerChange(newRawValues, { selected: true, triggerValue: selectValue }, source); } props.onSelect?.(eventValue, option); }; const onInternalDeselect = ( selectValue: RawValueType, option: DataNode, source: SelectSource, ) => { const eventValue = selectValue; let newRawValues = removeValue(rawValues.value, selectValue); // Remove keys if tree conduction if (treeConduction.value) { const { missingRawValues, existRawValues } = splitRawValues(newRawValues); const keyList = existRawValues.map(val => getEntityByValue(val).key); const { checkedKeys } = conductCheck( keyList, { checked: false, halfCheckedKeys: rawHalfCheckedKeys.value }, conductKeyEntities.value, ); newRawValues = [ ...missingRawValues, ...checkedKeys.map(key => getEntityByKey(key).data.value), ]; } triggerChange(newRawValues, { selected: false, triggerValue: selectValue }, source); props.onDeselect?.(eventValue, option); }; const onInternalClear = () => { triggerChange([], null, 'clear'); }; // ========================= Open ========================== const onInternalDropdownVisibleChange = (open: boolean) => { if (props.onDropdownVisibleChange) { const legacyParam = {}; Object.defineProperty(legacyParam, 'documentClickClose', { get() { warning(false, 'Second param of `onDropdownVisibleChange` has been removed.'); return false; }, }); (props.onDropdownVisibleChange as any)(open, legacyParam); } }; // ======================== Warning ======================== if (process.env.NODE_ENV !== 'production') { warningProps(props); } return () => { const { treeNodeFilterProp, dropdownPopupAlign, filterTreeNode, treeDefaultExpandAll, treeExpandedKeys, treeDefaultExpandedKeys, onTreeExpand, treeIcon, treeMotion, showTreeIcon, switcherIcon, treeLine, loadData, treeLoadedKeys, onTreeLoad, } = props; // ======================== Render ========================= // We pass some props into select props style const selectProps = { optionLabelProp: null, optionFilterProp: treeNodeFilterProp, dropdownAlign: dropdownPopupAlign, internalProps: { mark: INTERNAL_PROPS_MARK, onClear: onInternalClear, skipTriggerChange: true, skipTriggerSelect: true, onRawSelect: onInternalSelect, onRawDeselect: onInternalDeselect, }, filterOption: filterTreeNode, }; if (props.filterTreeNode === undefined) { delete selectProps.filterOption; } const selectContext = { checkable: mergedCheckable.value, loadData, treeLoadedKeys, onTreeLoad, checkedKeys: rawValues.value, halfCheckedKeys: rawHalfCheckedKeys.value, treeDefaultExpandAll, treeExpandedKeys, treeDefaultExpandedKeys, onTreeExpand, treeIcon, treeMotion, showTreeIcon, switcherIcon, treeLine, treeNodeFilterProp, getEntityByKey, getEntityByValue, customCheckable: slots.treeCheckable, slots, }; return ( ); }; }, }); }