refactor: vc-tree-select
parent
bb91ce7592
commit
63c5df9e73
|
@ -0,0 +1,68 @@
|
|||
import type {
|
||||
FlattenDataNode,
|
||||
InternalDataEntity,
|
||||
Key,
|
||||
LegacyDataNode,
|
||||
RawValueType,
|
||||
} from './interface';
|
||||
import type { SkipType } from './hooks/useKeyValueMapping';
|
||||
import type { ComputedRef, InjectionKey, PropType } from 'vue';
|
||||
import { computed, defineComponent, inject, provide } from 'vue';
|
||||
|
||||
interface ContextProps {
|
||||
checkable: boolean;
|
||||
customCheckable: () => any;
|
||||
checkedKeys: Key[];
|
||||
halfCheckedKeys: Key[];
|
||||
treeExpandedKeys: Key[];
|
||||
treeDefaultExpandedKeys: Key[];
|
||||
onTreeExpand: (keys: Key[]) => void;
|
||||
treeDefaultExpandAll: boolean;
|
||||
treeIcon: any;
|
||||
showTreeIcon: boolean;
|
||||
switcherIcon: any;
|
||||
treeLine: boolean;
|
||||
treeNodeFilterProp: string;
|
||||
treeLoadedKeys: Key[];
|
||||
treeMotion: any;
|
||||
loadData: (treeNode: LegacyDataNode) => Promise<unknown>;
|
||||
onTreeLoad: (loadedKeys: Key[]) => void;
|
||||
|
||||
// Cache help content. These can be generated by parent component.
|
||||
// Let's reuse this.
|
||||
getEntityByKey: (key: Key, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode;
|
||||
getEntityByValue: (
|
||||
value: RawValueType,
|
||||
skipType?: SkipType,
|
||||
ignoreDisabledCheck?: boolean,
|
||||
) => FlattenDataNode;
|
||||
|
||||
slots: {
|
||||
title?: (data: InternalDataEntity) => any;
|
||||
titleRender?: (data: InternalDataEntity) => any;
|
||||
[key: string]: ((...args: any[]) => any) | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const SelectContextKey: InjectionKey<ComputedRef<ContextProps>> = Symbol('SelectContextKey');
|
||||
|
||||
export const SelectContext = defineComponent({
|
||||
name: 'SelectContext',
|
||||
props: {
|
||||
value: { type: Object as PropType<ContextProps> },
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
provide(
|
||||
SelectContextKey,
|
||||
computed(() => props.value),
|
||||
);
|
||||
return () => slots.default?.();
|
||||
},
|
||||
});
|
||||
|
||||
export const useInjectTreeSelectContext = () => {
|
||||
return inject(
|
||||
SelectContextKey,
|
||||
computed(() => ({} as ContextProps)),
|
||||
);
|
||||
};
|
|
@ -0,0 +1,262 @@
|
|||
import type { DataNode, TreeDataNode, Key } from './interface';
|
||||
import { useInjectTreeSelectContext } from './Context';
|
||||
import type { RefOptionListProps } from '../vc-select/OptionList';
|
||||
import type { ScrollTo } from '../vc-virtual-list/List';
|
||||
import { computed, defineComponent, nextTick, ref, shallowRef, watch } from 'vue';
|
||||
import { optionListProps } from './props';
|
||||
import useMemo from '../_util/hooks/useMemo';
|
||||
import type { EventDataNode } from '../tree';
|
||||
import KeyCode from '../_util/KeyCode';
|
||||
import Tree from '../vc-tree/Tree';
|
||||
import type { TreeProps } from '../vc-tree/props';
|
||||
|
||||
const HIDDEN_STYLE = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
opacity: 0,
|
||||
border: 0,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
interface TreeEventInfo {
|
||||
node: { key: Key };
|
||||
selected?: boolean;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
type ReviseRefOptionListProps = Omit<RefOptionListProps, 'scrollTo'> & { scrollTo: ScrollTo };
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OptionList',
|
||||
inheritAttrs: false,
|
||||
props: optionListProps<DataNode>(),
|
||||
slots: ['notFoundContent', 'menuItemSelectedIcon'],
|
||||
setup(props, { slots, expose }) {
|
||||
const context = useInjectTreeSelectContext();
|
||||
|
||||
const treeRef = ref();
|
||||
const memoOptions = useMemo(
|
||||
() => props.options,
|
||||
[() => props.open, () => props.options],
|
||||
next => next[0],
|
||||
);
|
||||
|
||||
const valueKeys = computed(() => {
|
||||
const { checkedKeys, getEntityByValue } = context.value;
|
||||
return checkedKeys.map(val => {
|
||||
const entity = getEntityByValue(val);
|
||||
return entity ? entity.key : null;
|
||||
});
|
||||
});
|
||||
|
||||
const mergedCheckedKeys = computed(() => {
|
||||
const { checkable, halfCheckedKeys } = context.value;
|
||||
if (!checkable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
checked: valueKeys.value,
|
||||
halfChecked: halfCheckedKeys,
|
||||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
if (props.open && !props.multiple && valueKeys.value.length) {
|
||||
treeRef.value?.scrollTo({ key: valueKeys.value[0] });
|
||||
}
|
||||
});
|
||||
},
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
// ========================== Search ==========================
|
||||
const lowerSearchValue = computed(() => String(props.searchValue).toLowerCase());
|
||||
const filterTreeNode = (treeNode: EventDataNode) => {
|
||||
if (!lowerSearchValue.value) {
|
||||
return false;
|
||||
}
|
||||
return String(treeNode[context.value.treeNodeFilterProp])
|
||||
.toLowerCase()
|
||||
.includes(lowerSearchValue.value);
|
||||
};
|
||||
|
||||
// =========================== Keys ===========================
|
||||
const expandedKeys = shallowRef<Key[]>(context.value.treeDefaultExpandedKeys);
|
||||
const searchExpandedKeys = shallowRef<Key[]>(null);
|
||||
|
||||
watch(
|
||||
() => props.searchValue,
|
||||
() => {
|
||||
if (props.searchValue) {
|
||||
searchExpandedKeys.value = props.flattenOptions.map(o => o.key);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
const mergedExpandedKeys = computed(() => {
|
||||
if (context.value.treeExpandedKeys) {
|
||||
return [...context.value.treeExpandedKeys];
|
||||
}
|
||||
return props.searchValue ? searchExpandedKeys.value : expandedKeys.value;
|
||||
});
|
||||
|
||||
const onInternalExpand = (keys: Key[]) => {
|
||||
expandedKeys.value = keys;
|
||||
searchExpandedKeys.value = keys;
|
||||
|
||||
context.value.onTreeExpand?.(keys);
|
||||
};
|
||||
|
||||
// ========================== Events ==========================
|
||||
const onListMouseDown = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => {
|
||||
const { getEntityByKey, checkable, checkedKeys } = context.value;
|
||||
const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select');
|
||||
if (entity !== null) {
|
||||
props.onSelect?.(entity.data.value, {
|
||||
selected: !checkedKeys.includes(entity.data.value),
|
||||
});
|
||||
}
|
||||
|
||||
if (!props.multiple) {
|
||||
props.onToggleOpen?.(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ========================= Keyboard =========================
|
||||
const activeKey = ref<Key>(null);
|
||||
const activeEntity = computed(() => context.value.getEntityByKey(activeKey.value));
|
||||
|
||||
const setActiveKey = (key: Key) => {
|
||||
activeKey.value = key;
|
||||
};
|
||||
expose({
|
||||
scrollTo: (...args: any[]) => treeRef.value?.scrollTo?.(...args),
|
||||
onKeydown: (event: KeyboardEvent) => {
|
||||
const { which } = event;
|
||||
switch (which) {
|
||||
// >>> Arrow keys
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
case KeyCode.LEFT:
|
||||
case KeyCode.RIGHT:
|
||||
treeRef.value?.onKeydown(event);
|
||||
break;
|
||||
|
||||
// >>> Select item
|
||||
case KeyCode.ENTER: {
|
||||
const { selectable, value } = activeEntity.value?.data.node || {};
|
||||
if (selectable !== false) {
|
||||
onInternalSelect(null, {
|
||||
node: { key: activeKey.value },
|
||||
selected: !context.value.checkedKeys.includes(value),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// >>> Close
|
||||
case KeyCode.ESC: {
|
||||
props.onToggleOpen(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyup: () => {},
|
||||
} as ReviseRefOptionListProps);
|
||||
|
||||
return () => {
|
||||
const {
|
||||
prefixCls,
|
||||
height,
|
||||
itemHeight,
|
||||
virtual,
|
||||
multiple,
|
||||
searchValue,
|
||||
open,
|
||||
notFoundContent = slots.notFoundContent?.(),
|
||||
onMouseenter,
|
||||
} = props;
|
||||
const {
|
||||
checkable,
|
||||
treeDefaultExpandAll,
|
||||
treeIcon,
|
||||
showTreeIcon,
|
||||
switcherIcon,
|
||||
treeLine,
|
||||
loadData,
|
||||
treeLoadedKeys,
|
||||
treeMotion,
|
||||
onTreeLoad,
|
||||
} = context.value;
|
||||
// ========================== Render ==========================
|
||||
if (memoOptions.value.length === 0) {
|
||||
return (
|
||||
<div role="listbox" class={`${prefixCls}-empty`} onMousedown={onListMouseDown}>
|
||||
{notFoundContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const treeProps: Partial<TreeProps> = {};
|
||||
if (treeLoadedKeys) {
|
||||
treeProps.loadedKeys = treeLoadedKeys;
|
||||
}
|
||||
if (mergedExpandedKeys.value) {
|
||||
treeProps.expandedKeys = mergedExpandedKeys.value;
|
||||
}
|
||||
return (
|
||||
<div onMousedown={onListMouseDown} onMouseenter={onMouseenter}>
|
||||
{activeEntity.value && open && (
|
||||
<span style={HIDDEN_STYLE} aria-live="assertive">
|
||||
{activeEntity.value.data.value}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Tree
|
||||
ref={treeRef}
|
||||
focusable={false}
|
||||
prefixCls={`${prefixCls}-tree`}
|
||||
treeData={memoOptions.value as TreeDataNode[]}
|
||||
height={height}
|
||||
itemHeight={itemHeight}
|
||||
virtual={virtual}
|
||||
multiple={multiple}
|
||||
icon={treeIcon}
|
||||
showIcon={showTreeIcon}
|
||||
switcherIcon={switcherIcon}
|
||||
showLine={treeLine}
|
||||
loadData={searchValue ? null : (loadData as any)}
|
||||
motion={treeMotion}
|
||||
// We handle keys by out instead tree self
|
||||
checkable={checkable}
|
||||
checkStrictly
|
||||
checkedKeys={mergedCheckedKeys.value}
|
||||
selectedKeys={!checkable ? valueKeys.value : []}
|
||||
defaultExpandAll={treeDefaultExpandAll}
|
||||
{...treeProps}
|
||||
// Proxy event out
|
||||
onActiveChange={setActiveKey}
|
||||
onSelect={onInternalSelect}
|
||||
onCheck={onInternalSelect as any}
|
||||
onExpand={onInternalExpand}
|
||||
onLoad={onTreeLoad}
|
||||
filterTreeNode={filterTreeNode}
|
||||
v-slots={{ ...slots, checkable: context.value.customCheckable }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
/* istanbul ignore file */
|
||||
|
||||
import type { FunctionalComponent } from 'vue';
|
||||
import type { DataNode, Key } from './interface';
|
||||
|
||||
export interface TreeNodeProps extends Omit<DataNode, 'children'> {
|
||||
value: Key;
|
||||
}
|
||||
|
||||
/** This is a placeholder, not real render in dom */
|
||||
const TreeNode: FunctionalComponent<TreeNodeProps> & { isTreeSelectNode: boolean } = () => null;
|
||||
TreeNode.inheritAttrs = false;
|
||||
TreeNode.displayName = 'ATreeSelectNode';
|
||||
TreeNode.isTreeSelectNode = true;
|
||||
export default TreeNode;
|
|
@ -0,0 +1,6 @@
|
|||
import generate from './generate';
|
||||
import OptionList from './OptionList';
|
||||
|
||||
const TreeSelect = generate({ prefixCls: 'vc-tree-select', optionList: OptionList as any });
|
||||
|
||||
export default TreeSelect;
|
|
@ -0,0 +1,514 @@
|
|||
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<DataNode>['components']['optionList'];
|
||||
}) {
|
||||
const { prefixCls, optionList } = config;
|
||||
|
||||
const RefSelect = generateSelector<DataNode>({
|
||||
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<any>) => 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<DefaultValueType>(
|
||||
props.value === undefined ? props.defaultValue : props.value,
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
valueRef.value = props.value;
|
||||
},
|
||||
);
|
||||
|
||||
/** 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<RawValueType[]>([]);
|
||||
const rawHalfCheckedKeys = shallowRef<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;
|
||||
} 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 (
|
||||
<SelectContext value={selectContext}>
|
||||
<RefSelect
|
||||
{...attrs}
|
||||
ref={selectRef}
|
||||
mode={mergedMultiple.value ? 'multiple' : null}
|
||||
{...props}
|
||||
{...selectProps}
|
||||
value={selectValues.value}
|
||||
// We will handle this ourself since we need calculate conduction
|
||||
labelInValue
|
||||
options={mergedTreeData.value}
|
||||
onChange={null}
|
||||
onSelect={null}
|
||||
onDeselect={null}
|
||||
onDropdownVisibleChange={onInternalDropdownVisibleChange}
|
||||
v-slots={slots}
|
||||
/>
|
||||
</SelectContext>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import type { LabeledValueType, RawValueType } from '../TreeSelect';
|
||||
|
||||
/**
|
||||
* This function will try to call requestIdleCallback if available to save performance.
|
||||
* No need `getLabel` here since already fetch on `rawLabeledValue`.
|
||||
*/
|
||||
export default (values: Ref<LabeledValueType[]>): [Ref<LabeledValueType[]>] => {
|
||||
const cacheRef = shallowRef({
|
||||
valueLabels: new Map<RawValueType, any>(),
|
||||
});
|
||||
|
||||
const newFilledValues = computed(() => {
|
||||
const { valueLabels } = cacheRef.value;
|
||||
const valueLabelsCache = new Map<RawValueType, any>();
|
||||
|
||||
const filledValues = values.value.map(item => {
|
||||
const { value } = item;
|
||||
const mergedLabel = item.label ?? valueLabels.get(value);
|
||||
|
||||
// Save in cache
|
||||
valueLabelsCache.set(value, mergedLabel);
|
||||
|
||||
return {
|
||||
...item,
|
||||
label: mergedLabel,
|
||||
};
|
||||
});
|
||||
|
||||
cacheRef.value.valueLabels = valueLabelsCache;
|
||||
|
||||
return filledValues;
|
||||
});
|
||||
return [newFilledValues];
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import type { Key } from '../../_util/type';
|
||||
import type { DataEntity } from '../../vc-tree/interface';
|
||||
import { conductCheck } from '../../vc-tree/utils/conductUtil';
|
||||
import type { LabeledValueType, RawValueType } from '../TreeSelect';
|
||||
import type { Ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export default (
|
||||
rawLabeledValues: Ref<LabeledValueType[]>,
|
||||
rawHalfCheckedValues: Ref<LabeledValueType[]>,
|
||||
treeConduction: Ref<boolean>,
|
||||
keyEntities: Ref<Record<Key, DataEntity>>,
|
||||
) =>
|
||||
computed(() => {
|
||||
let checkedKeys: RawValueType[] = rawLabeledValues.value.map(({ value }) => value);
|
||||
let halfCheckedKeys: RawValueType[] = rawHalfCheckedValues.value.map(({ value }) => value);
|
||||
|
||||
const missingValues = checkedKeys.filter(key => !keyEntities[key]);
|
||||
|
||||
if (treeConduction.value) {
|
||||
({ checkedKeys, halfCheckedKeys } = conductCheck(checkedKeys, true, keyEntities.value));
|
||||
}
|
||||
|
||||
return [
|
||||
// Checked keys should fill with missing keys which should de-duplicated
|
||||
Array.from(new Set([...missingValues, ...checkedKeys])),
|
||||
// Half checked keys
|
||||
halfCheckedKeys,
|
||||
];
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import { convertDataToEntities } from '../../vc-tree/utils/treeUtil';
|
||||
import type { DataEntity } from '../../vc-tree/interface';
|
||||
import type { FieldNames, RawValueType } from '../TreeSelect';
|
||||
|
||||
import { isNil } from '../utils/valueUtil';
|
||||
import type { Ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { warning } from '../../vc-util/warning';
|
||||
|
||||
export default (treeData: Ref<any>, fieldNames: Ref<FieldNames>) =>
|
||||
computed<{
|
||||
valueEntities: Map<RawValueType, DataEntity>;
|
||||
keyEntities: Record<string, DataEntity>;
|
||||
}>(() => {
|
||||
const collection = convertDataToEntities(treeData.value, {
|
||||
fieldNames: fieldNames.value,
|
||||
initWrapper: wrapper => ({
|
||||
...wrapper,
|
||||
valueEntities: new Map(),
|
||||
}),
|
||||
processEntity: (entity, wrapper: any) => {
|
||||
const val = entity.node[fieldNames.value.value];
|
||||
|
||||
// Check if exist same value
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const key = entity.node.key;
|
||||
|
||||
warning(!isNil(val), 'TreeNode `value` is invalidate: undefined');
|
||||
warning(!wrapper.valueEntities.has(val), `Same \`value\` exist in the tree: ${val}`);
|
||||
warning(
|
||||
!key || String(key) === String(val),
|
||||
`\`key\` or \`value\` with TreeNode must be the same or you can remove one of them. key: ${key}, value: ${val}.`,
|
||||
);
|
||||
}
|
||||
wrapper.valueEntities.set(val, entity);
|
||||
},
|
||||
});
|
||||
|
||||
return collection as any;
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import type { DefaultOptionType, InternalFieldName, TreeSelectProps } from '../TreeSelect';
|
||||
import { fillLegacyProps } from '../utils/legacyUtil';
|
||||
|
||||
type GetFuncType<T> = T extends boolean ? never : T;
|
||||
type FilterFn = GetFuncType<TreeSelectProps['filterTreeNode']>;
|
||||
|
||||
export default (
|
||||
treeData: Ref<DefaultOptionType[]>,
|
||||
searchValue: Ref<string>,
|
||||
{
|
||||
treeNodeFilterProp,
|
||||
filterTreeNode,
|
||||
fieldNames,
|
||||
}: {
|
||||
fieldNames: Ref<InternalFieldName>;
|
||||
treeNodeFilterProp: Ref<string>;
|
||||
filterTreeNode: Ref<TreeSelectProps['filterTreeNode']>;
|
||||
},
|
||||
) => {
|
||||
return computed(() => {
|
||||
const { children: fieldChildren } = fieldNames.value;
|
||||
if (!searchValue.value || filterTreeNode.value === false) {
|
||||
return treeData.value;
|
||||
}
|
||||
|
||||
let filterOptionFunc: FilterFn;
|
||||
if (typeof filterTreeNode.value === 'function') {
|
||||
filterOptionFunc = filterTreeNode.value;
|
||||
} else {
|
||||
const upperStr = searchValue.value.toUpperCase();
|
||||
filterOptionFunc = (_, dataNode) => {
|
||||
const value = dataNode[treeNodeFilterProp.value];
|
||||
|
||||
return String(value).toUpperCase().includes(upperStr);
|
||||
};
|
||||
}
|
||||
|
||||
function dig(list: DefaultOptionType[], keepAll = false) {
|
||||
return list
|
||||
.map(dataNode => {
|
||||
const children = dataNode[fieldChildren];
|
||||
|
||||
const match = keepAll || filterOptionFunc(searchValue.value, fillLegacyProps(dataNode));
|
||||
const childList = dig(children || [], match);
|
||||
|
||||
if (match || childList.length) {
|
||||
return {
|
||||
...dataNode,
|
||||
[fieldChildren]: childList,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(node => node);
|
||||
}
|
||||
|
||||
return dig(treeData.value);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import type { ComputedRef, Ref } from 'vue';
|
||||
import { shallowRef, watchEffect } from 'vue';
|
||||
import type { FlattenDataNode, Key, RawValueType } from '../interface';
|
||||
|
||||
/**
|
||||
* Return cached Key Value map with DataNode.
|
||||
* Only re-calculate when `flattenOptions` changed.
|
||||
*/
|
||||
export default function useKeyValueMap(flattenOptions: ComputedRef<FlattenDataNode[]>) {
|
||||
const cacheKeyMap: Ref<Map<Key, FlattenDataNode>> = shallowRef(new Map());
|
||||
const cacheValueMap: Ref<Map<RawValueType, FlattenDataNode>> = shallowRef(new Map());
|
||||
|
||||
watchEffect(() => {
|
||||
const newCacheKeyMap = new Map();
|
||||
const newCacheValueMap = new Map();
|
||||
// Cache options by key
|
||||
flattenOptions.value.forEach((dataNode: FlattenDataNode) => {
|
||||
newCacheKeyMap.set(dataNode.key, dataNode);
|
||||
newCacheValueMap.set(dataNode.data.value, dataNode);
|
||||
});
|
||||
cacheKeyMap.value = newCacheKeyMap;
|
||||
cacheValueMap.value = newCacheValueMap;
|
||||
});
|
||||
return [cacheKeyMap, cacheValueMap];
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import type { Ref } from 'vue';
|
||||
import type { FlattenDataNode, Key, RawValueType } from '../interface';
|
||||
|
||||
export type SkipType = null | 'select' | 'checkbox';
|
||||
|
||||
export function isDisabled(dataNode: FlattenDataNode, skipType: SkipType): boolean {
|
||||
if (!dataNode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { disabled, disableCheckbox } = dataNode.data.node;
|
||||
|
||||
switch (skipType) {
|
||||
case 'checkbox':
|
||||
return disabled || disableCheckbox;
|
||||
|
||||
default:
|
||||
return disabled;
|
||||
}
|
||||
}
|
||||
|
||||
export default function useKeyValueMapping(
|
||||
cacheKeyMap: Ref<Map<Key, FlattenDataNode>>,
|
||||
cacheValueMap: Ref<Map<RawValueType, FlattenDataNode>>,
|
||||
): [
|
||||
(key: Key, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode,
|
||||
(value: RawValueType, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode,
|
||||
] {
|
||||
const getEntityByKey = (
|
||||
key: Key,
|
||||
skipType: SkipType = 'select',
|
||||
ignoreDisabledCheck?: boolean,
|
||||
) => {
|
||||
const dataNode = cacheKeyMap.value.get(key);
|
||||
|
||||
if (!ignoreDisabledCheck && isDisabled(dataNode, skipType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dataNode;
|
||||
};
|
||||
|
||||
const getEntityByValue = (
|
||||
value: RawValueType,
|
||||
skipType: SkipType = 'select',
|
||||
ignoreDisabledCheck?: boolean,
|
||||
) => {
|
||||
const dataNode = cacheValueMap.value.get(value);
|
||||
|
||||
if (!ignoreDisabledCheck && isDisabled(dataNode, skipType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dataNode;
|
||||
};
|
||||
|
||||
return [getEntityByKey, getEntityByValue];
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import type { RawValueType, FlattenDataNode, Key, LabelValueType } from '../interface';
|
||||
import type { SkipType } from './useKeyValueMapping';
|
||||
import { getRawValueLabeled } from '../utils/valueUtil';
|
||||
import type { CheckedStrategy } from '../utils/strategyUtil';
|
||||
import { formatStrategyKeys } from '../utils/strategyUtil';
|
||||
import type { DefaultValueType } from '../../vc-select/interface/generator';
|
||||
import type { DataEntity } from '../../vc-tree/interface';
|
||||
import type { Ref } from 'vue';
|
||||
import { shallowRef, watchEffect } from 'vue';
|
||||
|
||||
interface Config {
|
||||
treeConduction: Ref<boolean>;
|
||||
/** Current `value` of TreeSelect */
|
||||
value: Ref<DefaultValueType>;
|
||||
showCheckedStrategy: Ref<CheckedStrategy>;
|
||||
conductKeyEntities: Ref<Record<Key, DataEntity>>;
|
||||
getEntityByKey: (key: Key, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode;
|
||||
getEntityByValue: (
|
||||
value: RawValueType,
|
||||
skipType?: SkipType,
|
||||
ignoreDisabledCheck?: boolean,
|
||||
) => FlattenDataNode;
|
||||
getLabelProp: (entity: FlattenDataNode) => any;
|
||||
}
|
||||
|
||||
/** Return */
|
||||
export default function useSelectValues(
|
||||
rawValues: Ref<RawValueType[]>,
|
||||
{
|
||||
value,
|
||||
getEntityByValue,
|
||||
getEntityByKey,
|
||||
treeConduction,
|
||||
showCheckedStrategy,
|
||||
conductKeyEntities,
|
||||
getLabelProp,
|
||||
}: Config,
|
||||
): Ref<LabelValueType[]> {
|
||||
const rawValueLabeled = shallowRef([]);
|
||||
watchEffect(() => {
|
||||
let mergedRawValues = rawValues.value;
|
||||
|
||||
if (treeConduction.value) {
|
||||
const rawKeys = formatStrategyKeys(
|
||||
rawValues.value.map(val => {
|
||||
const entity = getEntityByValue(val);
|
||||
return entity ? entity.key : val;
|
||||
}),
|
||||
showCheckedStrategy.value,
|
||||
conductKeyEntities.value,
|
||||
);
|
||||
|
||||
mergedRawValues = rawKeys.map(key => {
|
||||
const entity = getEntityByKey(key);
|
||||
return entity ? entity.data.value : key;
|
||||
});
|
||||
}
|
||||
|
||||
rawValueLabeled.value = getRawValueLabeled(
|
||||
mergedRawValues,
|
||||
value.value,
|
||||
getEntityByValue,
|
||||
getLabelProp,
|
||||
);
|
||||
});
|
||||
return rawValueLabeled;
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import type { DataNode, SimpleModeConfig } from '../interface';
|
||||
import { convertChildrenToData } from '../utils/legacyUtil';
|
||||
import type { DefaultOptionType } from '../TreeSelect';
|
||||
import type { VueNode } from 'ant-design-vue/es/_util/type';
|
||||
|
||||
function parseSimpleTreeData(
|
||||
treeData: DataNode[],
|
||||
{ id, pId, rootPId }: SimpleModeConfig,
|
||||
): DataNode[] {
|
||||
const keyNodes = {};
|
||||
const rootNodeList = [];
|
||||
|
||||
// Fill in the map
|
||||
const nodeList = treeData.map(node => {
|
||||
const clone = { ...node };
|
||||
const key = clone[id];
|
||||
keyNodes[key] = clone;
|
||||
clone.key = clone.key || key;
|
||||
return clone;
|
||||
});
|
||||
|
||||
// Connect tree
|
||||
nodeList.forEach(node => {
|
||||
const parentKey = node[pId];
|
||||
const parent = keyNodes[parentKey];
|
||||
|
||||
// Fill parent
|
||||
if (parent) {
|
||||
parent.children = parent.children || [];
|
||||
parent.children.push(node);
|
||||
}
|
||||
|
||||
// Fill root tree node
|
||||
if (parentKey === rootPId || (!parent && rootPId === null)) {
|
||||
rootNodeList.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return rootNodeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `treeData` or `children` into formatted `treeData`.
|
||||
* Will not re-calculate if `treeData` or `children` not change.
|
||||
*/
|
||||
export default function useTreeData(
|
||||
treeData: Ref<DataNode[]>,
|
||||
children: Ref<VueNode[]>,
|
||||
simpleMode: Ref<boolean | SimpleModeConfig>,
|
||||
): Ref<DefaultOptionType[]> {
|
||||
return computed(() => {
|
||||
if (treeData.value) {
|
||||
return simpleMode.value
|
||||
? parseSimpleTreeData(treeData.value, {
|
||||
id: 'id',
|
||||
pId: 'pId',
|
||||
rootPId: null,
|
||||
...(simpleMode.value !== true ? simpleMode.value : {}),
|
||||
})
|
||||
: treeData.value;
|
||||
}
|
||||
|
||||
return convertChildrenToData(children.value);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// base rc-tree-select@4.6.1
|
||||
import TreeSelect from './TreeSelect';
|
||||
import TreeNode from './TreeNode';
|
||||
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';
|
||||
import type { TreeSelectProps } from './props';
|
||||
import { treeSelectProps } from './props';
|
||||
|
||||
export { TreeNode, SHOW_ALL, SHOW_CHILD, SHOW_PARENT, treeSelectProps };
|
||||
export type { TreeSelectProps };
|
||||
|
||||
export default TreeSelect;
|
|
@ -0,0 +1,101 @@
|
|||
export type SelectSource = 'option' | 'selection' | 'input' | 'clear';
|
||||
|
||||
export type Key = string | number;
|
||||
|
||||
export type RawValueType = string | number;
|
||||
|
||||
export interface LabelValueType {
|
||||
key?: Key;
|
||||
value?: RawValueType;
|
||||
label?: any;
|
||||
/** Only works on `treeCheckStrictly` */
|
||||
halfChecked?: boolean;
|
||||
}
|
||||
|
||||
export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[];
|
||||
|
||||
export interface DataNode {
|
||||
value?: RawValueType;
|
||||
title?: any;
|
||||
label?: any;
|
||||
key?: Key;
|
||||
disabled?: boolean;
|
||||
disableCheckbox?: boolean;
|
||||
checkable?: boolean;
|
||||
selectable?: boolean;
|
||||
children?: DataNode[];
|
||||
|
||||
/** Customize data info */
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
export interface InternalDataEntity {
|
||||
key: Key;
|
||||
value: RawValueType;
|
||||
title?: any;
|
||||
checkable: boolean;
|
||||
disableCheckbox: boolean;
|
||||
disabled: boolean;
|
||||
selectable: boolean;
|
||||
isLeaf: boolean;
|
||||
children?: InternalDataEntity[];
|
||||
|
||||
/** Origin DataNode */
|
||||
node: DataNode;
|
||||
|
||||
dataRef: DataNode;
|
||||
|
||||
slots?: Record<string, string>; // 兼容 V2
|
||||
}
|
||||
|
||||
export interface LegacyDataNode extends DataNode {
|
||||
props: any;
|
||||
}
|
||||
|
||||
export interface TreeDataNode extends DataNode {
|
||||
key: Key;
|
||||
children?: TreeDataNode[];
|
||||
}
|
||||
|
||||
export interface FlattenDataNode {
|
||||
data: InternalDataEntity;
|
||||
key: Key;
|
||||
value: RawValueType;
|
||||
level: number;
|
||||
parent?: FlattenDataNode;
|
||||
}
|
||||
|
||||
export interface SimpleModeConfig {
|
||||
id?: Key;
|
||||
pId?: Key;
|
||||
rootPId?: Key;
|
||||
}
|
||||
|
||||
/** @deprecated This is only used for legacy compatible. Not works on new code. */
|
||||
export interface LegacyCheckedNode {
|
||||
pos: string;
|
||||
node: any;
|
||||
children?: LegacyCheckedNode[];
|
||||
}
|
||||
|
||||
export interface ChangeEventExtra {
|
||||
/** @deprecated Please save prev value by control logic instead */
|
||||
preValue: LabelValueType[];
|
||||
triggerValue: RawValueType;
|
||||
/** @deprecated Use `onSelect` or `onDeselect` instead. */
|
||||
selected?: boolean;
|
||||
/** @deprecated Use `onSelect` or `onDeselect` instead. */
|
||||
checked?: boolean;
|
||||
|
||||
// Not sure if exist user still use this. We have to keep but not recommend user to use
|
||||
/** @deprecated This prop not work as react node anymore. */
|
||||
triggerNode: any;
|
||||
/** @deprecated This prop not work as react node anymore. */
|
||||
allCheckedNodes: LegacyCheckedNode[];
|
||||
}
|
||||
|
||||
export interface FieldNames {
|
||||
value?: string;
|
||||
label?: string;
|
||||
children?: string;
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type {
|
||||
DataNode,
|
||||
ChangeEventExtra,
|
||||
DefaultValueType,
|
||||
FieldNames,
|
||||
FlattenDataNode,
|
||||
LabelValueType,
|
||||
LegacyDataNode,
|
||||
RawValueType,
|
||||
SimpleModeConfig,
|
||||
} from './interface';
|
||||
import { selectBaseProps } from '../vc-select';
|
||||
import type { FilterFunc } from '../vc-select/interface/generator';
|
||||
import omit from '../_util/omit';
|
||||
import type { Key } from '../_util/type';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import type { CheckedStrategy } from './utils/strategyUtil';
|
||||
|
||||
export function optionListProps<OptionsType>() {
|
||||
return {
|
||||
prefixCls: String,
|
||||
id: String,
|
||||
options: { type: Array as PropType<OptionsType[]> },
|
||||
flattenOptions: { type: Array as PropType<FlattenDataNode[]> },
|
||||
height: Number,
|
||||
itemHeight: Number,
|
||||
virtual: { type: Boolean, default: undefined },
|
||||
values: { type: Set as PropType<Set<RawValueType>> },
|
||||
multiple: { type: Boolean, default: undefined },
|
||||
open: { type: Boolean, default: undefined },
|
||||
defaultActiveFirstOption: { type: Boolean, default: undefined },
|
||||
notFoundContent: PropTypes.any,
|
||||
menuItemSelectedIcon: PropTypes.any,
|
||||
childrenAsData: { type: Boolean, default: undefined },
|
||||
searchValue: String,
|
||||
|
||||
onSelect: {
|
||||
type: Function as PropType<(value: RawValueType, option: { selected: boolean }) => void>,
|
||||
},
|
||||
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
||||
/** Tell Select that some value is now active to make accessibility work */
|
||||
onActiveValue: { type: Function as PropType<(value: RawValueType, index: number) => void> },
|
||||
onScroll: { type: Function as PropType<(e: UIEvent) => void> },
|
||||
|
||||
onMouseenter: { type: Function as PropType<() => void> },
|
||||
};
|
||||
}
|
||||
|
||||
export function treeSelectProps<ValueType = DefaultValueType>() {
|
||||
const selectProps = omit(selectBaseProps<DataNode, ValueType>(), [
|
||||
'onChange',
|
||||
'mode',
|
||||
'menuItemSelectedIcon',
|
||||
'dropdownAlign',
|
||||
'backfill',
|
||||
'getInputElement',
|
||||
'optionLabelProp',
|
||||
'tokenSeparators',
|
||||
'filterOption',
|
||||
]);
|
||||
return {
|
||||
...selectProps,
|
||||
|
||||
multiple: { type: Boolean, default: undefined },
|
||||
showArrow: { type: Boolean, default: undefined },
|
||||
showSearch: { type: Boolean, default: undefined },
|
||||
open: { type: Boolean, default: undefined },
|
||||
defaultOpen: { type: Boolean, default: undefined },
|
||||
value: { type: [String, Number, Object, Array] as PropType<ValueType> },
|
||||
defaultValue: { type: [String, Number, Object, Array] as PropType<ValueType> },
|
||||
disabled: { type: Boolean, default: undefined },
|
||||
|
||||
placeholder: PropTypes.any,
|
||||
/** @deprecated Use `searchValue` instead */
|
||||
inputValue: String,
|
||||
searchValue: String,
|
||||
autoClearSearchValue: { type: Boolean, default: undefined },
|
||||
|
||||
maxTagPlaceholder: { type: Function as PropType<(omittedValues: LabelValueType[]) => any> },
|
||||
|
||||
fieldNames: { type: Object as PropType<FieldNames> },
|
||||
loadData: { type: Function as PropType<(dataNode: LegacyDataNode) => Promise<unknown>> },
|
||||
treeNodeFilterProp: String,
|
||||
treeNodeLabelProp: String,
|
||||
treeDataSimpleMode: {
|
||||
type: [Boolean, Object] as PropType<boolean | SimpleModeConfig>,
|
||||
default: undefined,
|
||||
},
|
||||
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> },
|
||||
treeDefaultExpandAll: { type: Boolean, default: undefined },
|
||||
treeData: { type: Array as PropType<DataNode[]> },
|
||||
treeLine: { type: Boolean, default: undefined },
|
||||
treeIcon: PropTypes.any,
|
||||
showTreeIcon: { type: Boolean, default: undefined },
|
||||
switcherIcon: PropTypes.any,
|
||||
treeMotion: PropTypes.any,
|
||||
children: Array,
|
||||
|
||||
filterTreeNode: {
|
||||
type: [Boolean, Function] as PropType<boolean | FilterFunc<LegacyDataNode>>,
|
||||
default: undefined,
|
||||
},
|
||||
dropdownPopupAlign: PropTypes.any,
|
||||
|
||||
// Event
|
||||
onSearch: { type: Function as PropType<(value: string) => void> },
|
||||
onChange: {
|
||||
type: Function as PropType<
|
||||
(value: ValueType, labelList: any[], extra: ChangeEventExtra) => void
|
||||
>,
|
||||
},
|
||||
onTreeExpand: { type: Function as PropType<(expandedKeys: Key[]) => void> },
|
||||
onTreeLoad: { type: Function as PropType<(loadedKeys: Key[]) => void> },
|
||||
onDropdownVisibleChange: { type: Function as PropType<(open: boolean) => void> },
|
||||
|
||||
// Legacy
|
||||
/** `searchPlaceholder` has been removed since search box has been merged into input box */
|
||||
searchPlaceholder: PropTypes.any,
|
||||
|
||||
/** @private This is not standard API since we only used in `rc-cascader`. Do not use in your production */
|
||||
labelRender: { type: Function as PropType<(entity: FlattenDataNode) => any> },
|
||||
};
|
||||
}
|
||||
|
||||
class Helper<T> {
|
||||
ReturnOptionListProps = optionListProps<T>();
|
||||
ReturnTreeSelectProps = treeSelectProps<T>();
|
||||
}
|
||||
|
||||
export type OptionListProps = Partial<ExtractPropTypes<Helper<DataNode>['ReturnOptionListProps']>>;
|
||||
|
||||
export type TreeSelectProps<T = DefaultValueType> = Partial<
|
||||
ExtractPropTypes<Helper<T>['ReturnTreeSelectProps']>
|
||||
>;
|
|
@ -0,0 +1,181 @@
|
|||
import { filterEmpty } from '../../_util/props-util';
|
||||
import { camelize } from 'vue';
|
||||
import { warning } from '../../vc-util/warning';
|
||||
import type {
|
||||
DataNode,
|
||||
LegacyDataNode,
|
||||
ChangeEventExtra,
|
||||
InternalDataEntity,
|
||||
RawValueType,
|
||||
LegacyCheckedNode,
|
||||
} from '../interface';
|
||||
import TreeNode from '../TreeNode';
|
||||
import type { VueNode } from '../../_util/type';
|
||||
|
||||
function isTreeSelectNode(node: any) {
|
||||
return node && node.type && (node.type as any).isTreeSelectNode;
|
||||
}
|
||||
export function convertChildrenToData(rootNodes: VueNode[]): DataNode[] {
|
||||
function dig(treeNodes: any[] = []): DataNode[] {
|
||||
return filterEmpty(treeNodes).map(treeNode => {
|
||||
// Filter invalidate node
|
||||
if (!isTreeSelectNode(treeNode)) {
|
||||
warning(!treeNode, 'TreeSelect/TreeSelectNode can only accept TreeSelectNode as children.');
|
||||
return null;
|
||||
}
|
||||
const slots = (treeNode.children as any) || {};
|
||||
const key = treeNode.key as string | number;
|
||||
const props: any = {};
|
||||
for (const [k, v] of Object.entries(treeNode.props)) {
|
||||
props[camelize(k)] = v;
|
||||
}
|
||||
const { isLeaf, checkable, selectable, disabled, disableCheckbox } = props;
|
||||
// 默认值为 undefined
|
||||
const newProps = {
|
||||
isLeaf: isLeaf || isLeaf === '' || undefined,
|
||||
checkable: checkable || checkable === '' || undefined,
|
||||
selectable: selectable || selectable === '' || undefined,
|
||||
disabled: disabled || disabled === '' || undefined,
|
||||
disableCheckbox: disableCheckbox || disableCheckbox === '' || undefined,
|
||||
};
|
||||
const slotsProps = { ...props, ...newProps };
|
||||
const {
|
||||
title = slots.title?.(slotsProps),
|
||||
switcherIcon = slots.switcherIcon?.(slotsProps),
|
||||
...rest
|
||||
} = props;
|
||||
const children = slots.default?.();
|
||||
const dataNode: DataNode = {
|
||||
...rest,
|
||||
title,
|
||||
switcherIcon,
|
||||
key,
|
||||
isLeaf,
|
||||
...newProps,
|
||||
};
|
||||
|
||||
const parsedChildren = dig(children);
|
||||
if (parsedChildren.length) {
|
||||
dataNode.children = parsedChildren;
|
||||
}
|
||||
|
||||
return dataNode;
|
||||
});
|
||||
}
|
||||
|
||||
return dig(rootNodes as any[]);
|
||||
}
|
||||
|
||||
export function fillLegacyProps(dataNode: DataNode): LegacyDataNode {
|
||||
// Skip if not dataNode exist
|
||||
if (!dataNode) {
|
||||
return dataNode as LegacyDataNode;
|
||||
}
|
||||
|
||||
const cloneNode = { ...dataNode };
|
||||
|
||||
if (!('props' in cloneNode)) {
|
||||
Object.defineProperty(cloneNode, 'props', {
|
||||
get() {
|
||||
warning(
|
||||
false,
|
||||
'New `rc-tree-select` not support return node instance as argument anymore. Please consider to remove `props` access.',
|
||||
);
|
||||
return cloneNode;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return cloneNode as LegacyDataNode;
|
||||
}
|
||||
|
||||
export function fillAdditionalInfo(
|
||||
extra: ChangeEventExtra,
|
||||
triggerValue: RawValueType,
|
||||
checkedValues: RawValueType[],
|
||||
treeData: InternalDataEntity[],
|
||||
showPosition: boolean,
|
||||
) {
|
||||
let triggerNode = null;
|
||||
let nodeList: LegacyCheckedNode[] = null;
|
||||
|
||||
function generateMap() {
|
||||
function dig(list: InternalDataEntity[], level = '0', parentIncluded = false) {
|
||||
return list
|
||||
.map((dataNode, index) => {
|
||||
const pos = `${level}-${index}`;
|
||||
const included = checkedValues.includes(dataNode.value);
|
||||
const children = dig(dataNode.children || [], pos, included);
|
||||
const node = <TreeNode {...dataNode}>{children.map(child => child.node)}</TreeNode>;
|
||||
|
||||
// Link with trigger node
|
||||
if (triggerValue === dataNode.value) {
|
||||
triggerNode = node;
|
||||
}
|
||||
|
||||
if (included) {
|
||||
const checkedNode: LegacyCheckedNode = {
|
||||
pos,
|
||||
node,
|
||||
children,
|
||||
};
|
||||
|
||||
if (!parentIncluded) {
|
||||
nodeList.push(checkedNode);
|
||||
}
|
||||
|
||||
return checkedNode;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(node => node);
|
||||
}
|
||||
|
||||
if (!nodeList) {
|
||||
nodeList = [];
|
||||
|
||||
dig(treeData);
|
||||
|
||||
// Sort to keep the checked node length
|
||||
nodeList.sort(
|
||||
(
|
||||
{
|
||||
node: {
|
||||
props: { value: val1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
props: { value: val2 },
|
||||
},
|
||||
},
|
||||
) => {
|
||||
const index1 = checkedValues.indexOf(val1);
|
||||
const index2 = checkedValues.indexOf(val2);
|
||||
return index1 - index2;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(extra, 'triggerNode', {
|
||||
get() {
|
||||
warning(false, '`triggerNode` is deprecated. Please consider decoupling data with node.');
|
||||
generateMap();
|
||||
|
||||
return triggerNode;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(extra, 'allCheckedNodes', {
|
||||
get() {
|
||||
warning(false, '`allCheckedNodes` is deprecated. Please consider decoupling data with node.');
|
||||
generateMap();
|
||||
|
||||
if (showPosition) {
|
||||
return nodeList;
|
||||
}
|
||||
|
||||
return nodeList.map(({ node }) => node);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import type { DataEntity } from '../../vc-tree/interface';
|
||||
import type { RawValueType, Key, DataNode } from '../interface';
|
||||
import { isCheckDisabled } from './valueUtil';
|
||||
|
||||
export const SHOW_ALL = 'SHOW_ALL';
|
||||
export const SHOW_PARENT = 'SHOW_PARENT';
|
||||
export const SHOW_CHILD = 'SHOW_CHILD';
|
||||
|
||||
export type CheckedStrategy = typeof SHOW_ALL | typeof SHOW_PARENT | typeof SHOW_CHILD;
|
||||
|
||||
export function formatStrategyKeys(
|
||||
keys: Key[],
|
||||
strategy: CheckedStrategy,
|
||||
keyEntities: Record<Key, DataEntity>,
|
||||
): RawValueType[] {
|
||||
const keySet = new Set(keys);
|
||||
|
||||
if (strategy === SHOW_CHILD) {
|
||||
return keys.filter(key => {
|
||||
const entity = keyEntities[key];
|
||||
|
||||
if (
|
||||
entity &&
|
||||
entity.children &&
|
||||
entity.children.every(
|
||||
({ node }) => isCheckDisabled(node) || keySet.has((node as DataNode).key),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (strategy === SHOW_PARENT) {
|
||||
return keys.filter(key => {
|
||||
const entity = keyEntities[key];
|
||||
const parent = entity ? entity.parent : null;
|
||||
|
||||
if (parent && !isCheckDisabled(parent.node) && keySet.has((parent.node as DataNode).key)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return keys;
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
import type {
|
||||
FlattenDataNode,
|
||||
Key,
|
||||
RawValueType,
|
||||
DataNode,
|
||||
DefaultValueType,
|
||||
LabelValueType,
|
||||
LegacyDataNode,
|
||||
FieldNames,
|
||||
InternalDataEntity,
|
||||
} from '../interface';
|
||||
import { fillLegacyProps } from './legacyUtil';
|
||||
import type { SkipType } from '../hooks/useKeyValueMapping';
|
||||
import type { FlattenNode } from '../../vc-tree/interface';
|
||||
import { flattenTreeData } from '../../vc-tree/utils/treeUtil';
|
||||
import type { FilterFunc } from '../../vc-select/interface/generator';
|
||||
|
||||
type CompatibleDataNode = Omit<FlattenDataNode, 'level'>;
|
||||
|
||||
export function toArray<T>(value: T | T[]): T[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return value !== undefined ? [value] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill `fieldNames` with default field names.
|
||||
*
|
||||
* @param fieldNames passed props
|
||||
* @param skipTitle Skip if no need fill `title`. This is useful since we have 2 name as same title level
|
||||
* @returns
|
||||
*/
|
||||
export function fillFieldNames(fieldNames?: FieldNames, skipTitle = false) {
|
||||
const { label, value, children } = fieldNames || {};
|
||||
|
||||
const filledNames: FieldNames = {
|
||||
value: value || 'value',
|
||||
children: children || 'children',
|
||||
};
|
||||
|
||||
if (!skipTitle || label) {
|
||||
filledNames.label = label || 'label';
|
||||
}
|
||||
|
||||
return filledNames;
|
||||
}
|
||||
|
||||
export function findValueOption(values: RawValueType[], options: CompatibleDataNode[]): DataNode[] {
|
||||
const optionMap: Map<RawValueType, DataNode> = new Map();
|
||||
|
||||
options.forEach(flattenItem => {
|
||||
const { data, value } = flattenItem;
|
||||
optionMap.set(value, data.node);
|
||||
});
|
||||
|
||||
return values.map(val => fillLegacyProps(optionMap.get(val)));
|
||||
}
|
||||
|
||||
export function isValueDisabled(value: RawValueType, options: CompatibleDataNode[]): boolean {
|
||||
const option = findValueOption([value], options)[0];
|
||||
if (option) {
|
||||
return option.disabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isCheckDisabled(node: DataNode) {
|
||||
return node.disabled || node.disableCheckbox || node.checkable === false;
|
||||
}
|
||||
|
||||
interface TreeDataNode extends InternalDataEntity {
|
||||
key: Key;
|
||||
children?: TreeDataNode[];
|
||||
}
|
||||
|
||||
function getLevel({ parent }: FlattenNode): number {
|
||||
let level = 0;
|
||||
let current = parent;
|
||||
|
||||
while (current) {
|
||||
current = current.parent;
|
||||
level += 1;
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Before reuse `rc-tree` logic, we need to add key since TreeSelect use `value` instead of `key`.
|
||||
*/
|
||||
export function flattenOptions(options: any): FlattenDataNode[] {
|
||||
const typedOptions = options as InternalDataEntity[];
|
||||
|
||||
// Add missing key
|
||||
function fillKey(list: InternalDataEntity[]): TreeDataNode[] {
|
||||
return (list || []).map(node => {
|
||||
const { value, key, children } = node;
|
||||
|
||||
const clone: TreeDataNode = {
|
||||
...node,
|
||||
key: 'key' in node ? key : value,
|
||||
};
|
||||
|
||||
if (children) {
|
||||
clone.children = fillKey(children);
|
||||
}
|
||||
|
||||
return clone;
|
||||
});
|
||||
}
|
||||
|
||||
const flattenList = flattenTreeData(fillKey(typedOptions), true, null);
|
||||
|
||||
const cacheMap = new Map<Key, FlattenDataNode>();
|
||||
const flattenDateNodeList: (FlattenDataNode & { parentKey?: Key })[] = flattenList.map(option => {
|
||||
const { data, key, value } = option as any as Omit<FlattenNode, 'data'> & {
|
||||
value: RawValueType;
|
||||
data: InternalDataEntity;
|
||||
};
|
||||
|
||||
const flattenNode = {
|
||||
key,
|
||||
value,
|
||||
data,
|
||||
level: getLevel(option),
|
||||
parentKey: option.parent?.data.key,
|
||||
};
|
||||
|
||||
cacheMap.set(key, flattenNode);
|
||||
|
||||
return flattenNode;
|
||||
});
|
||||
|
||||
// Fill parent
|
||||
flattenDateNodeList.forEach(flattenNode => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
flattenNode.parent = cacheMap.get(flattenNode.parentKey);
|
||||
});
|
||||
|
||||
return flattenDateNodeList;
|
||||
}
|
||||
|
||||
function getDefaultFilterOption(optionFilterProp: string) {
|
||||
return (searchValue: string, dataNode: LegacyDataNode) => {
|
||||
const value = dataNode[optionFilterProp];
|
||||
|
||||
return String(value).toLowerCase().includes(String(searchValue).toLowerCase());
|
||||
};
|
||||
}
|
||||
|
||||
/** Filter options and return a new options by the search text */
|
||||
export function filterOptions(
|
||||
searchValue: string,
|
||||
options: DataNode[],
|
||||
{
|
||||
optionFilterProp,
|
||||
filterOption,
|
||||
}: {
|
||||
optionFilterProp: string;
|
||||
filterOption: boolean | FilterFunc<LegacyDataNode>;
|
||||
},
|
||||
): DataNode[] {
|
||||
if (filterOption === false) {
|
||||
return options;
|
||||
}
|
||||
|
||||
let filterOptionFunc: FilterFunc<LegacyDataNode>;
|
||||
if (typeof filterOption === 'function') {
|
||||
filterOptionFunc = filterOption;
|
||||
} else {
|
||||
filterOptionFunc = getDefaultFilterOption(optionFilterProp);
|
||||
}
|
||||
|
||||
function dig(list: DataNode[], keepAll = false) {
|
||||
return list
|
||||
.map(dataNode => {
|
||||
const { children } = dataNode;
|
||||
|
||||
const match = keepAll || filterOptionFunc(searchValue, fillLegacyProps(dataNode));
|
||||
const childList = dig(children || [], match);
|
||||
|
||||
if (match || childList.length) {
|
||||
return {
|
||||
...dataNode,
|
||||
children: childList,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(node => node);
|
||||
}
|
||||
|
||||
return dig(options);
|
||||
}
|
||||
|
||||
export function getRawValueLabeled(
|
||||
values: RawValueType[],
|
||||
prevValue: DefaultValueType,
|
||||
getEntityByValue: (
|
||||
value: RawValueType,
|
||||
skipType?: SkipType,
|
||||
ignoreDisabledCheck?: boolean,
|
||||
) => FlattenDataNode,
|
||||
getLabelProp: (entity: FlattenDataNode) => any,
|
||||
): LabelValueType[] {
|
||||
const valueMap = new Map<RawValueType, LabelValueType>();
|
||||
|
||||
toArray(prevValue).forEach(item => {
|
||||
if (item && typeof item === 'object' && 'value' in item) {
|
||||
valueMap.set(item.value, item);
|
||||
}
|
||||
});
|
||||
|
||||
return values.map(val => {
|
||||
const item: LabelValueType = { value: val };
|
||||
const entity = getEntityByValue(val, 'select', true);
|
||||
const label = entity ? getLabelProp(entity) : val;
|
||||
|
||||
if (valueMap.has(val)) {
|
||||
const labeledValue = valueMap.get(val);
|
||||
item.label = 'label' in labeledValue ? labeledValue.label : label;
|
||||
if ('halfChecked' in labeledValue) {
|
||||
item.halfChecked = labeledValue.halfChecked;
|
||||
}
|
||||
} else {
|
||||
item.label = label;
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
export function addValue(rawValues: RawValueType[], value: RawValueType) {
|
||||
const values = new Set(rawValues);
|
||||
values.add(value);
|
||||
return Array.from(values);
|
||||
}
|
||||
export function removeValue(rawValues: RawValueType[], value: RawValueType) {
|
||||
const values = new Set(rawValues);
|
||||
values.delete(value);
|
||||
return Array.from(values);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { warning } from '../../vc-util/warning';
|
||||
import { toArray } from './valueUtil';
|
||||
|
||||
function warningProps(props: any) {
|
||||
const { searchPlaceholder, treeCheckStrictly, treeCheckable, labelInValue, value, multiple } =
|
||||
props;
|
||||
|
||||
warning(
|
||||
!searchPlaceholder,
|
||||
'`searchPlaceholder` has been removed, please use `placeholder` instead',
|
||||
);
|
||||
|
||||
if (treeCheckStrictly && labelInValue === false) {
|
||||
warning(false, '`treeCheckStrictly` will force set `labelInValue` to `true`.');
|
||||
}
|
||||
|
||||
if (labelInValue || treeCheckStrictly) {
|
||||
warning(
|
||||
toArray(value).every(val => val && typeof val === 'object' && 'value' in val),
|
||||
'Invalid prop `value` supplied to `TreeSelect`. You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.',
|
||||
);
|
||||
}
|
||||
|
||||
if (treeCheckStrictly || multiple || treeCheckable) {
|
||||
warning(
|
||||
!value || Array.isArray(value),
|
||||
'`value` should be an array when `TreeSelect` is checkable or multiple.',
|
||||
);
|
||||
} else {
|
||||
warning(!Array.isArray(value), '`value` should not be array when `TreeSelect` is single mode.');
|
||||
}
|
||||
}
|
||||
|
||||
export default warningProps;
|
Loading…
Reference in New Issue