diff --git a/components/vc-tree-select/generate.tsx b/components/vc-tree-select/generate.tsx index fe6bba0d2..a6a8fcec9 100644 --- a/components/vc-tree-select/generate.tsx +++ b/components/vc-tree-select/generate.tsx @@ -29,14 +29,17 @@ import { SelectContext } from './Context'; import useTreeData from './hooks/useTreeData'; import useKeyValueMap from './hooks/useKeyValueMap'; import useKeyValueMapping from './hooks/useKeyValueMapping'; -import type { CheckedStrategy } from './utils/strategyUtil'; import { formatStrategyKeys, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil'; import { fillAdditionalInfo } from './utils/legacyUtil'; import useSelectValues from './hooks/useSelectValues'; import { treeSelectProps, TreeSelectProps } from './props'; import { getLabeledValue } from '../vc-select/utils/valueUtil'; import omit from '../_util/omit'; -import { defineComponent } from 'vue'; +import { computed, defineComponent, ref, 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, @@ -91,9 +94,413 @@ export default function generate(config: { props: treeSelectProps(), slots: [], name: 'TreeSelect', - setup(props) { - return () => { + TreeNode: TreeNode, + SHOW_ALL: SHOW_ALL, + SHOW_PARENT: SHOW_PARENT, + SHOW_CHILD: 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(null); + + expose({ + scrollTo: selectRef.value.scrollTo, + 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 = ref([]); + const rawHalfCheckedKeys = ref([]); + + 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; + } + [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 = mergedLabelInValue.value ? selectValue : 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.checkable, + slots, + }; + return ( + + + + ); }; }, }); diff --git a/components/vc-tree-select/props.ts b/components/vc-tree-select/props.ts index eee712a9c..b13d548ea 100644 --- a/components/vc-tree-select/props.ts +++ b/components/vc-tree-select/props.ts @@ -1,5 +1,5 @@ import type { ExtractPropTypes, PropType } from 'vue'; -import type { DataNode } from '../tree'; +import type { DataNode } from './interface'; import { selectBaseProps } from '../vc-select'; import type { FilterFunc } from '../vc-select/interface/generator'; import omit from '../_util/omit'; @@ -87,9 +87,9 @@ export function treeSelectProps() { type: [Boolean, Object] as PropType, default: undefined, }, - treeExpandedKeys: { type: [String, Number] as PropType }, - treeDefaultExpandedKeys: { type: [String, Number] as PropType }, - treeLoadedKeys: { type: [String, Number] as PropType }, + treeExpandedKeys: { type: Array as PropType }, + treeDefaultExpandedKeys: { type: Array as PropType }, + treeLoadedKeys: { type: Array as PropType }, treeCheckable: { type: Boolean, default: undefined }, treeCheckStrictly: { type: Boolean, default: undefined }, showCheckedStrategy: { type: String as PropType }, diff --git a/components/vc-tree/interface.tsx b/components/vc-tree/interface.tsx index a84e92e00..40aeda5ed 100644 --- a/components/vc-tree/interface.tsx +++ b/components/vc-tree/interface.tsx @@ -21,19 +21,19 @@ export interface DataNode { } export interface EventDataNode extends DataNode { - expanded: boolean; - selected: boolean; + expanded?: boolean; + selected?: boolean; checked: boolean; - loaded: boolean; - loading: boolean; - halfChecked: boolean; - dragOver: boolean; - dragOverGapTop: boolean; - dragOverGapBottom: boolean; - pos: string; - active: boolean; - dataRef: DataNode; - eventKey: Key; // 兼容 v2, 推荐直接用 key + loaded?: boolean; + loading?: boolean; + halfChecked?: boolean; + dragOver?: boolean; + dragOverGapTop?: boolean; + dragOverGapBottom?: boolean; + pos?: string; + active?: boolean; + dataRef?: DataNode; + eventKey?: Key; // 兼容 v2, 推荐直接用 key } export type IconType = any; diff --git a/components/vc-tree/props.ts b/components/vc-tree/props.ts index cacaa58e3..ecce7f459 100644 --- a/components/vc-tree/props.ts +++ b/components/vc-tree/props.ts @@ -43,7 +43,7 @@ export const treeNodeProps = { pos: String, /** New added in Tree for easy data access */ - data: { type: Object as PropType }, + data: { type: Object as PropType, default: undefined as DataNode }, isStart: { type: Array as PropType }, isEnd: { type: Array as PropType }, active: { type: Boolean, default: undefined },