refactor: tree-select
parent
956ed09885
commit
fdf7c5d4ce
|
@ -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<DefaultValueType>(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<RawValueType[]>([]);
|
||||
const rawHalfCheckedKeys = ref<RawValueType[]>([]);
|
||||
|
||||
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 (
|
||||
<SelectContext value={selectContext}>
|
||||
<RefSelect
|
||||
{...attrs}
|
||||
ref={selectRef}
|
||||
mode={mergedMultiple.value ? 'multiple' : null}
|
||||
{...props}
|
||||
{...selectProps}
|
||||
value={selectValues}
|
||||
// We will handle this ourself since we need calculate conduction
|
||||
labelInValue
|
||||
options={mergedTreeData.value}
|
||||
onChange={null}
|
||||
onSelect={null}
|
||||
onDeselect={null}
|
||||
onDropdownVisibleChange={onInternalDropdownVisibleChange}
|
||||
/>
|
||||
</SelectContext>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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<ValueType = DefaultValueType>() {
|
|||
type: [Boolean, Object] as PropType<boolean | SimpleModeConfig>,
|
||||
default: undefined,
|
||||
},
|
||||
treeExpandedKeys: { type: [String, Number] as PropType<Key> },
|
||||
treeDefaultExpandedKeys: { type: [String, Number] as PropType<Key> },
|
||||
treeLoadedKeys: { type: [String, Number] as PropType<Key> },
|
||||
treeExpandedKeys: { type: Array as PropType<Key[]> },
|
||||
treeDefaultExpandedKeys: { type: Array as PropType<Key[]> },
|
||||
treeLoadedKeys: { type: Array as PropType<Key[]> },
|
||||
treeCheckable: { type: Boolean, default: undefined },
|
||||
treeCheckStrictly: { type: Boolean, default: undefined },
|
||||
showCheckedStrategy: { type: String as PropType<CheckedStrategy> },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -43,7 +43,7 @@ export const treeNodeProps = {
|
|||
pos: String,
|
||||
|
||||
/** New added in Tree for easy data access */
|
||||
data: { type: Object as PropType<DataNode> },
|
||||
data: { type: Object as PropType<DataNode>, default: undefined as DataNode },
|
||||
isStart: { type: Array as PropType<boolean[]> },
|
||||
isEnd: { type: Array as PropType<boolean[]> },
|
||||
active: { type: Boolean, default: undefined },
|
||||
|
|
Loading…
Reference in New Issue