refactor: tree-select

refactor-cascader
tangjinzhou 2022-01-15 22:44:47 +08:00
parent 314461848c
commit 4e8a16f2bb
43 changed files with 1451 additions and 1732 deletions

View File

@ -10,7 +10,7 @@ import VcTreeSelect, {
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import initDefaultProps from '../_util/props-util/initDefaultProps'; import initDefaultProps from '../_util/props-util/initDefaultProps';
import type { SizeType } from '../config-provider'; import type { SizeType } from '../config-provider';
import type { DefaultValueType, FieldNames } from '../vc-tree-select/interface'; import type { FieldNames, Key } from '../vc-tree-select/interface';
import omit from '../_util/omit'; import omit from '../_util/omit';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import useConfigInject from '../_util/hooks/useConfigInject'; import useConfigInject from '../_util/hooks/useConfigInject';
@ -21,6 +21,9 @@ import type { AntTreeNodeProps } from '../tree/Tree';
import { warning } from '../vc-util/warning'; import { warning } from '../vc-util/warning';
import { flattenChildren } from '../_util/props-util'; import { flattenChildren } from '../_util/props-util';
import { useInjectFormItemContext } from '../form/FormItemContext'; import { useInjectFormItemContext } from '../form/FormItemContext';
import type { BaseSelectRef } from '../vc-select';
import type { BaseOptionType, DefaultOptionType } from '../vc-tree-select/TreeSelect';
import type { TreeProps } from '../tree';
const getTransitionName = (rootPrefixCls: string, motion: string, transitionName?: string) => { const getTransitionName = (rootPrefixCls: string, motion: string, transitionName?: string) => {
if (transitionName !== undefined) { if (transitionName !== undefined) {
@ -34,29 +37,43 @@ type RawValue = string | number;
export interface LabeledValue { export interface LabeledValue {
key?: string; key?: string;
value: RawValue; value: RawValue;
label: any; label?: any;
} }
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[]; export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[];
export interface RefTreeSelectProps { export type RefTreeSelectProps = BaseSelectRef;
focus: () => void;
blur: () => void; export function treeSelectProps<
} ValueType = any,
export const treeSelectProps = { OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType,
...omit(vcTreeSelectProps<DefaultValueType>(), ['showTreeIcon', 'treeMotion', 'inputIcon']), >() {
return {
...omit(vcTreeSelectProps<ValueType, OptionType>(), [
'showTreeIcon',
'treeMotion',
'inputIcon',
'getInputElement',
'treeLine',
'customSlots',
]),
suffixIcon: PropTypes.any, suffixIcon: PropTypes.any,
size: { type: String as PropType<SizeType> }, size: { type: String as PropType<SizeType> },
bordered: { type: Boolean, default: undefined }, bordered: { type: Boolean, default: undefined },
treeLine: { type: [Boolean, Object] as PropType<TreeProps['showLine']>, default: undefined },
replaceFields: { type: Object as PropType<FieldNames> }, replaceFields: { type: Object as PropType<FieldNames> },
}; 'onUpdate:value': { type: Function as PropType<(value: any) => void> },
export type TreeSelectProps = Partial<ExtractPropTypes<typeof treeSelectProps>>; 'onUpdate:treeExpandedKeys': { type: Function as PropType<(keys: Key[]) => void> },
'onUpdate:searchValue': { type: Function as PropType<(value: string) => void> },
};
}
export type TreeSelectProps = Partial<ExtractPropTypes<ReturnType<typeof treeSelectProps>>>;
const TreeSelect = defineComponent({ const TreeSelect = defineComponent({
TreeNode, TreeNode,
name: 'ATreeSelect', name: 'ATreeSelect',
inheritAttrs: false, inheritAttrs: false,
props: initDefaultProps(treeSelectProps, { props: initDefaultProps(treeSelectProps(), {
choiceTransitionName: '', choiceTransitionName: '',
listHeight: 256, listHeight: 256,
treeIcon: false, treeIcon: false,
@ -135,18 +152,18 @@ const TreeSelect = defineComponent({
}, },
}); });
const handleChange = (...args: any[]) => { const handleChange: TreeSelectProps['onChange'] = (...args: any[]) => {
emit('update:value', args[0]); emit('update:value', args[0]);
emit('change', ...args); emit('change', ...args);
formItemContext.onFieldChange(); formItemContext.onFieldChange();
}; };
const handleTreeExpand = (...args: any[]) => { const handleTreeExpand: TreeSelectProps['onTreeExpand'] = (keys: Key[]) => {
emit('update:treeExpandedKeys', args[0]); emit('update:treeExpandedKeys', keys);
emit('treeExpand', ...args); emit('treeExpand', keys);
}; };
const handleSearch = (...args: any[]) => { const handleSearch: TreeSelectProps['onSearch'] = (value: string) => {
emit('update:searchValue', args[0]); emit('update:searchValue', value);
emit('search', ...args); emit('search', value);
}; };
const handleBlur = () => { const handleBlur = () => {
emit('blur'); emit('blur');
@ -190,6 +207,10 @@ const TreeSelect = defineComponent({
'removeIcon', 'removeIcon',
'clearIcon', 'clearIcon',
'switcherIcon', 'switcherIcon',
'bordered',
'onUpdate:value',
'onUpdate:treeExpandedKeys',
'onUpdate:searchValue',
]); ]);
const mergedClassName = classNames( const mergedClassName = classNames(
@ -242,6 +263,10 @@ const TreeSelect = defineComponent({
}} }}
{...otherProps} {...otherProps}
transitionName={transitionName.value} transitionName={transitionName.value}
customSlots={{
...slots,
treeCheckable: () => <span class={`${prefixCls.value}-tree-checkbox-inner`} />,
}}
/> />
); );
}; };

View File

@ -11,7 +11,7 @@
.@{tree-select-prefix-cls} { .@{tree-select-prefix-cls} {
// ======================= Dropdown ======================= // ======================= Dropdown =======================
&-dropdown { &-dropdown {
padding: @padding-xs (@padding-xs / 2) 0; padding: @padding-xs (@padding-xs / 2);
&-rtl { &-rtl {
direction: rtl; direction: rtl;
@ -24,8 +24,6 @@
align-items: stretch; align-items: stretch;
.@{select-tree-prefix-cls}-treenode { .@{select-tree-prefix-cls}-treenode {
padding-bottom: @padding-xs;
.@{select-tree-prefix-cls}-node-content-wrapper { .@{select-tree-prefix-cls}-node-content-wrapper {
flex: auto; flex: auto;
} }

View File

@ -1,34 +0,0 @@
import { flattenChildren, isValidElement } from '../_util/props-util';
export function convertChildrenToData(nodes: any[]): any[] {
return flattenChildren(nodes)
.map(node => {
if (!isValidElement(node) || !node.type) {
return null;
}
const { default: d, ...restSlot } = node.children || {};
const children = d ? d() : [];
const {
key,
props: { value, ...restProps },
} = node;
const data = {
key,
value,
...restProps,
};
Object.keys(restSlot).forEach(p => {
if (typeof restSlot[p] === 'function') {
data[p] = <>{restSlot[p]()}</>;
}
});
const childData = convertChildrenToData(children);
if (childData.length) {
data.children = childData;
}
return data;
})
.filter(data => data);
}

View File

@ -35,9 +35,9 @@ import isMobile from '../vc-util/isMobile';
import KeyCode from '../_util/KeyCode'; import KeyCode from '../_util/KeyCode';
import { toReactive } from '../_util/toReactive'; import { toReactive } from '../_util/toReactive';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import OptionList from './OptionList';
import createRef from '../_util/createRef'; import createRef from '../_util/createRef';
import type { BaseOptionType } from './Select'; import type { BaseOptionType } from './Select';
import useInjectLegacySelectContext from '../vc-tree-select/LegacyContext';
const DEFAULT_OMIT_PROPS = [ const DEFAULT_OMIT_PROPS = [
'value', 'value',
@ -269,7 +269,7 @@ export default defineComponent({
onMounted(() => { onMounted(() => {
mobile.value = isMobile(); mobile.value = isMobile();
}); });
const legacyTreeSelectContext = useInjectLegacySelectContext();
// ============================== Refs ============================== // ============================== Refs ==============================
const containerRef = ref<HTMLDivElement>(null); const containerRef = ref<HTMLDivElement>(null);
const selectorDomRef = createRef(); const selectorDomRef = createRef();
@ -673,6 +673,7 @@ export default defineComponent({
emptyOptions, emptyOptions,
activeDescendantId, activeDescendantId,
activeValue, activeValue,
OptionList,
...restProps ...restProps
} = { ...props, ...attrs } as BaseSelectProps; } = { ...props, ...attrs } as BaseSelectProps;
@ -752,7 +753,12 @@ export default defineComponent({
} }
// =========================== OptionList =========================== // =========================== OptionList ===========================
const optionList = <OptionList ref={listRef} v-slots={{ option: slots.option }} />; const optionList = (
<OptionList
ref={listRef}
v-slots={{ ...legacyTreeSelectContext.customSlots, option: slots.option }}
/>
);
// ============================= Select ============================= // ============================= Select =============================
const mergedClassName = classNames(prefixCls, attrs.class, { const mergedClassName = classNames(prefixCls, attrs.class, {
@ -840,7 +846,7 @@ export default defineComponent({
// onFocus={onContainerFocus} // onFocus={onContainerFocus}
// onBlur={onContainerBlur} // onBlur={onContainerBlur}
> >
{mockFocused && !mergedOpen.value && ( {mockFocused.value && !mergedOpen.value && (
<span <span
style={{ style={{
width: 0, width: 0,

View File

@ -3,8 +3,8 @@ import Input from './Input';
import type { InnerSelectorProps } from './interface'; import type { InnerSelectorProps } from './interface';
import { Fragment, computed, defineComponent, ref, watch } from 'vue'; import { Fragment, computed, defineComponent, ref, watch } from 'vue';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import { useInjectTreeSelectContext } from '../../vc-tree-select/Context';
import type { VueNode } from '../../_util/type'; import type { VueNode } from '../../_util/type';
import useInjectLegacySelectContext from '../../vc-tree-select/LegacyContext';
interface SelectorProps extends InnerSelectorProps { interface SelectorProps extends InnerSelectorProps {
inputElement: VueNode; inputElement: VueNode;
@ -50,7 +50,7 @@ const SingleSelector = defineComponent<SelectorProps>({
} }
return inputValue; return inputValue;
}); });
const treeSelectContext = useInjectTreeSelectContext(); const legacyTreeSelectContext = useInjectLegacySelectContext();
watch( watch(
[combobox, () => props.activeValue], [combobox, () => props.activeValue],
() => { () => {
@ -108,13 +108,16 @@ const SingleSelector = defineComponent<SelectorProps>({
const item = values[0]; const item = values[0];
let titleNode = null; let titleNode = null;
// custom tree-select title by slot // custom tree-select title by slot
if (item && treeSelectContext.value.slots) {
if (item && legacyTreeSelectContext.customSlots) {
const key = item.key ?? item.value;
const originData = legacyTreeSelectContext.keyEntities[key]?.node || {};
titleNode = titleNode =
treeSelectContext.value.slots[item?.option?.data?.slots?.title] || legacyTreeSelectContext.customSlots[originData.slots?.title] ||
treeSelectContext.value.slots.title || legacyTreeSelectContext.customSlots.title ||
item.label; item.label;
if (typeof titleNode === 'function') { if (typeof titleNode === 'function') {
titleNode = titleNode(item.option?.data || {}); titleNode = titleNode(originData);
} }
// else if (treeSelectContext.value.slots.titleRender) { // else if (treeSelectContext.value.slots.titleRender) {
// // title titleRender title titleRender // // title titleRender title titleRender
@ -155,7 +158,7 @@ const SingleSelector = defineComponent<SelectorProps>({
{/* Display value */} {/* Display value */}
{!combobox.value && item && !hasTextInput.value && ( {!combobox.value && item && !hasTextInput.value && (
<span class={`${prefixCls}-selection-item`} title={title.value}> <span class={`${prefixCls}-selection-item`} title={title.value}>
<Fragment key={item.key || item.value}>{titleNode}</Fragment> <Fragment key={item.key ?? item.value}>{titleNode}</Fragment>
</span> </span>
)} )}

View File

@ -6,9 +6,9 @@
import type { InjectionKey } from 'vue'; import type { InjectionKey } from 'vue';
import { inject, provide } from 'vue'; import { inject, provide } from 'vue';
import type { DataEntity, IconType } from '../vc-tree/interface'; import type { DataEntity, IconType } from '../vc-tree/interface';
import type { Key, LegacyDataNode, RawValueType } from './interface'; import type { InternalDataEntity, Key, LegacyDataNode, RawValueType } from './interface';
interface LegacyContextProps { export interface LegacyContextProps {
checkable: boolean; checkable: boolean;
checkedKeys: Key[]; checkedKeys: Key[];
customCheckable: () => any; customCheckable: () => any;
@ -29,11 +29,11 @@ interface LegacyContextProps {
keyEntities: Record<RawValueType, DataEntity<any>>; keyEntities: Record<RawValueType, DataEntity<any>>;
// slots: { customSlots: {
// title?: (data: InternalDataEntity) => any; title?: (data: InternalDataEntity) => any;
// titleRender?: (data: InternalDataEntity) => any; treeCheckable: () => any;
// [key: string]: ((...args: any[]) => any) | undefined; [key: string]: ((...args: any[]) => any) | undefined;
// }; };
} }
const TreeSelectLegacyContextPropsKey: InjectionKey<LegacyContextProps> = Symbol( const TreeSelectLegacyContextPropsKey: InjectionKey<LegacyContextProps> = Symbol(

View File

@ -1,14 +1,16 @@
import type { DataNode, TreeDataNode, Key } from './interface'; import type { TreeDataNode, Key } from './interface';
import { useInjectTreeSelectContext } from './Context';
import type { RefOptionListProps } from '../vc-select/OptionList'; import type { RefOptionListProps } from '../vc-select/OptionList';
import type { ScrollTo } from '../vc-virtual-list/List'; import type { ScrollTo } from '../vc-virtual-list/List';
import { computed, defineComponent, nextTick, ref, shallowRef, watch } from 'vue'; import { computed, defineComponent, nextTick, ref, shallowRef, watch } from 'vue';
import { optionListProps } from './props';
import useMemo from '../_util/hooks/useMemo'; import useMemo from '../_util/hooks/useMemo';
import type { EventDataNode } from '../tree'; import type { EventDataNode } from '../tree';
import KeyCode from '../_util/KeyCode'; import KeyCode from '../_util/KeyCode';
import Tree from '../vc-tree/Tree'; import Tree from '../vc-tree/Tree';
import type { TreeProps } from '../vc-tree/props'; import type { TreeProps } from '../vc-tree/props';
import { getAllKeys, isCheckDisabled } from './utils/valueUtil';
import { useBaseProps } from '../vc-select';
import useInjectLegacySelectContext from './LegacyContext';
import useInjectSelectContext from './TreeSelectContext';
const HIDDEN_STYLE = { const HIDDEN_STYLE = {
width: 0, width: 0,
@ -32,44 +34,36 @@ type ReviseRefOptionListProps = Omit<RefOptionListProps, 'scrollTo'> & { scrollT
export default defineComponent({ export default defineComponent({
name: 'OptionList', name: 'OptionList',
inheritAttrs: false, inheritAttrs: false,
props: optionListProps<DataNode>(),
slots: ['notFoundContent', 'menuItemSelectedIcon'], slots: ['notFoundContent', 'menuItemSelectedIcon'],
setup(props, { slots, expose }) { setup(_, { slots, expose }) {
const context = useInjectTreeSelectContext(); const baseProps = useBaseProps();
const legacyContext = useInjectLegacySelectContext();
const context = useInjectSelectContext();
const treeRef = ref(); const treeRef = ref();
const memoOptions = useMemo( const memoTreeData = useMemo(
() => props.options, () => context.treeData,
[() => props.open, () => props.options], [() => baseProps.open, () => context.treeData],
next => next[0], 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 mergedCheckedKeys = computed(() => {
const { checkable, halfCheckedKeys } = context.value; const { checkable, halfCheckedKeys, checkedKeys } = legacyContext;
if (!checkable) { if (!checkable) {
return null; return null;
} }
return { return {
checked: valueKeys.value, checked: checkedKeys,
halfChecked: halfCheckedKeys, halfChecked: halfCheckedKeys,
}; };
}); });
watch( watch(
() => props.open, () => baseProps.open,
() => { () => {
nextTick(() => { nextTick(() => {
if (props.open && !props.multiple && valueKeys.value.length) { if (baseProps.open && !baseProps.multiple && legacyContext.checkedKeys.length) {
treeRef.value?.scrollTo({ key: valueKeys.value[0] }); treeRef.value?.scrollTo({ key: legacyContext.checkedKeys[0] });
} }
}); });
}, },
@ -77,25 +71,25 @@ export default defineComponent({
); );
// ========================== Search ========================== // ========================== Search ==========================
const lowerSearchValue = computed(() => String(props.searchValue).toLowerCase()); const lowerSearchValue = computed(() => String(baseProps.searchValue).toLowerCase());
const filterTreeNode = (treeNode: EventDataNode) => { const filterTreeNode = (treeNode: EventDataNode) => {
if (!lowerSearchValue.value) { if (!lowerSearchValue.value) {
return false; return false;
} }
return String(treeNode[context.value.treeNodeFilterProp]) return String(treeNode[legacyContext.treeNodeFilterProp])
.toLowerCase() .toLowerCase()
.includes(lowerSearchValue.value); .includes(lowerSearchValue.value);
}; };
// =========================== Keys =========================== // =========================== Keys ===========================
const expandedKeys = shallowRef<Key[]>(context.value.treeDefaultExpandedKeys); const expandedKeys = shallowRef<Key[]>(legacyContext.treeDefaultExpandedKeys);
const searchExpandedKeys = shallowRef<Key[]>(null); const searchExpandedKeys = shallowRef<Key[]>(null);
watch( watch(
() => props.searchValue, () => baseProps.searchValue,
() => { () => {
if (props.searchValue) { if (baseProps.searchValue) {
searchExpandedKeys.value = props.flattenOptions.map(o => o.key); searchExpandedKeys.value = getAllKeys(context.treeData, context.fieldNames);
} }
}, },
{ {
@ -103,17 +97,17 @@ export default defineComponent({
}, },
); );
const mergedExpandedKeys = computed(() => { const mergedExpandedKeys = computed(() => {
if (context.value.treeExpandedKeys) { if (legacyContext.treeExpandedKeys) {
return [...context.value.treeExpandedKeys]; return [...legacyContext.treeExpandedKeys];
} }
return props.searchValue ? searchExpandedKeys.value : expandedKeys.value; return baseProps.searchValue ? searchExpandedKeys.value : expandedKeys.value;
}); });
const onInternalExpand = (keys: Key[]) => { const onInternalExpand = (keys: Key[]) => {
expandedKeys.value = keys; expandedKeys.value = keys;
searchExpandedKeys.value = keys; searchExpandedKeys.value = keys;
context.value.onTreeExpand?.(keys); legacyContext.onTreeExpand?.(keys);
}; };
// ========================== Events ========================== // ========================== Events ==========================
@ -121,23 +115,23 @@ export default defineComponent({
event.preventDefault(); event.preventDefault();
}; };
const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => { const onInternalSelect = (_: Key[], { node }: TreeEventInfo) => {
const { getEntityByKey, checkable, checkedKeys } = context.value; const { checkable, checkedKeys } = legacyContext;
const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select'); if (checkable && isCheckDisabled(node)) {
if (entity !== null) { return;
props.onSelect?.(entity.data.value, {
selected: !checkedKeys.includes(entity.data.value),
});
} }
context.onSelect?.(node.key, {
selected: !checkedKeys.includes(node.key),
});
if (!props.multiple) { if (!baseProps.multiple) {
props.onToggleOpen?.(false); baseProps.toggleOpen?.(false);
} }
}; };
// ========================= Keyboard ========================= // ========================= Keyboard =========================
const activeKey = ref<Key>(null); const activeKey = ref<Key>(null);
const activeEntity = computed(() => context.value.getEntityByKey(activeKey.value)); const activeEntity = computed(() => legacyContext.keyEntities[activeKey.value]);
const setActiveKey = (key: Key) => { const setActiveKey = (key: Key) => {
activeKey.value = key; activeKey.value = key;
@ -157,11 +151,11 @@ export default defineComponent({
// >>> Select item // >>> Select item
case KeyCode.ENTER: { case KeyCode.ENTER: {
const { selectable, value } = activeEntity.value?.data.node || {}; const { selectable, value } = activeEntity.value?.node || {};
if (selectable !== false) { if (selectable !== false) {
onInternalSelect(null, { onInternalSelect(null, {
node: { key: activeKey.value }, node: { key: activeKey.value },
selected: !context.value.checkedKeys.includes(value), selected: !legacyContext.checkedKeys.includes(value),
}); });
} }
break; break;
@ -169,7 +163,7 @@ export default defineComponent({
// >>> Close // >>> Close
case KeyCode.ESC: { case KeyCode.ESC: {
props.onToggleOpen(false); baseProps.toggleOpen(false);
} }
} }
}, },
@ -179,15 +173,12 @@ export default defineComponent({
return () => { return () => {
const { const {
prefixCls, prefixCls,
height,
itemHeight,
virtual,
multiple, multiple,
searchValue, searchValue,
open, open,
notFoundContent = slots.notFoundContent?.(), notFoundContent = slots.notFoundContent?.(),
onMouseenter, } = baseProps;
} = props; const { listHeight, listItemHeight, virtual } = context;
const { const {
checkable, checkable,
treeDefaultExpandAll, treeDefaultExpandAll,
@ -199,9 +190,10 @@ export default defineComponent({
treeLoadedKeys, treeLoadedKeys,
treeMotion, treeMotion,
onTreeLoad, onTreeLoad,
} = context.value; checkedKeys,
} = legacyContext;
// ========================== Render ========================== // ========================== Render ==========================
if (memoOptions.value.length === 0) { if (memoTreeData.value.length === 0) {
return ( return (
<div role="listbox" class={`${prefixCls}-empty`} onMousedown={onListMouseDown}> <div role="listbox" class={`${prefixCls}-empty`} onMousedown={onListMouseDown}>
{notFoundContent} {notFoundContent}
@ -209,7 +201,9 @@ export default defineComponent({
); );
} }
const treeProps: Partial<TreeProps> = {}; const treeProps: Partial<TreeProps> = {
fieldNames: context.fieldNames,
};
if (treeLoadedKeys) { if (treeLoadedKeys) {
treeProps.loadedKeys = treeLoadedKeys; treeProps.loadedKeys = treeLoadedKeys;
} }
@ -217,10 +211,10 @@ export default defineComponent({
treeProps.expandedKeys = mergedExpandedKeys.value; treeProps.expandedKeys = mergedExpandedKeys.value;
} }
return ( return (
<div onMousedown={onListMouseDown} onMouseenter={onMouseenter}> <div onMousedown={onListMouseDown}>
{activeEntity.value && open && ( {activeEntity.value && open && (
<span style={HIDDEN_STYLE} aria-live="assertive"> <span style={HIDDEN_STYLE} aria-live="assertive">
{activeEntity.value.data.value} {activeEntity.value.node.value}
</span> </span>
)} )}
@ -228,9 +222,9 @@ export default defineComponent({
ref={treeRef} ref={treeRef}
focusable={false} focusable={false}
prefixCls={`${prefixCls}-tree`} prefixCls={`${prefixCls}-tree`}
treeData={memoOptions.value as TreeDataNode[]} treeData={memoTreeData.value as TreeDataNode[]}
height={height} height={listHeight}
itemHeight={itemHeight} itemHeight={listItemHeight}
virtual={virtual} virtual={virtual}
multiple={multiple} multiple={multiple}
icon={treeIcon} icon={treeIcon}
@ -243,7 +237,7 @@ export default defineComponent({
checkable={checkable} checkable={checkable}
checkStrictly checkStrictly
checkedKeys={mergedCheckedKeys.value} checkedKeys={mergedCheckedKeys.value}
selectedKeys={!checkable ? valueKeys.value : []} selectedKeys={!checkable ? checkedKeys : []}
defaultExpandAll={treeDefaultExpandAll} defaultExpandAll={treeDefaultExpandAll}
{...treeProps} {...treeProps}
// Proxy event out // Proxy event out
@ -253,7 +247,7 @@ export default defineComponent({
onExpand={onInternalExpand} onExpand={onInternalExpand}
onLoad={onTreeLoad} onLoad={onTreeLoad}
filterTreeNode={filterTreeNode} filterTreeNode={filterTreeNode}
v-slots={{ ...slots, checkable: context.value.customCheckable }} v-slots={{ ...slots, checkable: legacyContext.customSlots.treeCheckable }}
/> />
</div> </div>
); );

View File

@ -1,6 +1,734 @@
import generate from './generate';
import OptionList from './OptionList'; import OptionList from './OptionList';
import { formatStrategyValues, SHOW_CHILD } from './utils/strategyUtil';
import type { CheckedStrategy } from './utils/strategyUtil';
import { useProvideSelectContext } from './TreeSelectContext';
import type { TreeSelectContextProps } from './TreeSelectContext';
import type { LegacyContextProps } from './LegacyContext';
import { useProvideLegacySelectContext } from './LegacyContext';
import useTreeData from './hooks/useTreeData';
import { toArray, fillFieldNames, isNil } from './utils/valueUtil';
import useCache from './hooks/useCache';
import useDataEntities from './hooks/useDataEntities';
import { fillAdditionalInfo, fillLegacyProps } from './utils/legacyUtil';
import useCheckedKeys from './hooks/useCheckedKeys';
import useFilterTreeData from './hooks/useFilterTreeData';
import warningProps from './utils/warningPropsUtil';
import type { Key } from './interface';
import type { DisplayValueType } from '../vc-select/BaseSelect';
import { baseSelectPropsWithoutPrivate } from '../vc-select/BaseSelect';
import { computed, defineComponent, ref, shallowRef, toRaw, toRef, toRefs, watchEffect } from 'vue';
import type { ExtractPropTypes, PropType } from 'vue';
import omit from '../_util/omit';
import PropTypes from '../_util/vue-types';
import type { SelectProps, BaseSelectProps, BaseSelectRef } from '../vc-select';
import { BaseSelect } from '../vc-select';
import { initDefaultProps } from '../_util/props-util';
import useId from '../vc-select/hooks/useId';
import useMergedState from '../_util/hooks/useMergedState';
import type { VueNode } from '../_util/type';
import { conductCheck } from '../vc-tree/utils/conductUtil';
import { warning } from '../vc-util/warning';
import { toReactive } from '../_util/toReactive';
const TreeSelect = generate({ prefixCls: 'vc-tree-select', optionList: OptionList as any }); export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
export default TreeSelect; export type RawValueType = string | number;
export interface LabeledValueType {
key?: Key;
value?: RawValueType;
label?: any;
/** Only works on `treeCheckStrictly` */
halfChecked?: boolean;
}
export type SelectSource = 'option' | 'selection' | 'input' | 'clear';
export type DraftValueType = RawValueType | LabeledValueType | (RawValueType | LabeledValueType)[];
/** @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: LabeledValueType[];
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;
}
export interface InternalFieldName extends Omit<FieldNames, 'label'> {
_title: string[];
}
export interface SimpleModeConfig {
id?: Key;
pId?: Key;
rootPId?: Key;
}
export interface BaseOptionType {
disabled?: boolean;
checkable?: boolean;
disableCheckbox?: boolean;
children?: BaseOptionType[];
[name: string]: any;
}
export interface DefaultOptionType extends BaseOptionType {
value?: RawValueType;
title?: any;
label?: any;
key?: Key;
children?: DefaultOptionType[];
}
export interface LegacyDataNode extends DefaultOptionType {
props: any;
}
export function treeSelectProps<
ValueType = any,
OptionType extends BaseOptionType = DefaultOptionType,
>() {
return {
...omit(baseSelectPropsWithoutPrivate(), ['mode']),
prefixCls: String,
id: String,
value: { type: [String, Number, Object, Array] as PropType<ValueType> },
defaultValue: { type: [String, Number, Object, Array] as PropType<ValueType> },
onChange: {
type: Function as PropType<
(value: ValueType, labelList: any[], extra: ChangeEventExtra) => void
>,
},
searchValue: String,
/** @deprecated Use `searchValue` instead */
inputValue: String,
onSearch: { type: Function as PropType<(value: string) => void> },
autoClearSearchValue: { type: Boolean, default: undefined },
filterTreeNode: {
type: [Boolean, Function] as PropType<
boolean | ((inputValue: string, treeNode: DefaultOptionType) => boolean)
>,
default: undefined,
},
treeNodeFilterProp: String,
// >>> Select
onSelect: Function as PropType<SelectProps['onSelect']>,
onDeselect: Function as PropType<SelectProps['onDeselect']>,
showCheckedStrategy: { type: String as PropType<CheckedStrategy> },
treeNodeLabelProp: String,
fieldNames: { type: Object as PropType<FieldNames> },
// >>> Mode
multiple: { type: Boolean, default: undefined },
treeCheckable: { type: Boolean, default: undefined },
treeCheckStrictly: { type: Boolean, default: undefined },
labelInValue: { type: Boolean, default: undefined },
// >>> Data
treeData: { type: Array as PropType<OptionType[]> },
treeDataSimpleMode: {
type: [Boolean, Object] as PropType<boolean | SimpleModeConfig>,
default: undefined,
},
loadData: { type: Function as PropType<(dataNode: LegacyDataNode) => Promise<unknown>> },
treeLoadedKeys: { type: Array as PropType<Key[]> },
onTreeLoad: { type: Function as PropType<(loadedKeys: Key[]) => void> },
// >>> Expanded
treeDefaultExpandAll: { type: Boolean, default: undefined },
treeExpandedKeys: { type: Array as PropType<Key[]> },
treeDefaultExpandedKeys: { type: Array as PropType<Key[]> },
onTreeExpand: { type: Function as PropType<(expandedKeys: Key[]) => void> },
// >>> Options
virtual: { type: Boolean, default: undefined },
listHeight: Number,
listItemHeight: Number,
onDropdownVisibleChange: { type: Function as PropType<(open: boolean) => void> },
// >>> Tree
treeLine: { type: Boolean, default: undefined },
treeIcon: PropTypes.any,
showTreeIcon: { type: Boolean, default: undefined },
switcherIcon: PropTypes.any,
treeMotion: PropTypes.any,
children: Array as PropType<VueNode[]>,
showArrow: { type: Boolean, default: undefined },
showSearch: { type: Boolean, default: undefined },
open: { type: Boolean, default: undefined },
defaultOpen: { type: Boolean, default: undefined },
disabled: { type: Boolean, default: undefined },
placeholder: PropTypes.any,
maxTagPlaceholder: { type: Function as PropType<(omittedValues: DisplayValueType[]) => any> },
dropdownPopupAlign: PropTypes.any,
customSlots: Object,
};
}
export type TreeSelectProps = Partial<ExtractPropTypes<ReturnType<typeof treeSelectProps>>>;
function isRawValue(value: RawValueType | LabeledValueType): value is RawValueType {
return !value || typeof value !== 'object';
}
export default defineComponent({
name: 'TreeSelect',
inheritAttrs: false,
props: initDefaultProps(treeSelectProps(), {
treeNodeFilterProp: 'value',
autoClearSearchValue: true,
showCheckedStrategy: SHOW_CHILD,
listHeight: 200,
listItemHeight: 20,
prefixCls: 'vc-tree-select',
}),
setup(props, { attrs, expose, slots }) {
const mergedId = useId(toRef(props, 'id'));
const treeConduction = computed(() => props.treeCheckable && !props.treeCheckStrictly);
const mergedCheckable = computed(() => props.treeCheckable || props.treeCheckStrictly);
const mergedLabelInValue = computed(() => props.treeCheckStrictly || props.labelInValue);
const mergedMultiple = computed(() => mergedCheckable.value || props.multiple);
// ========================== Warning ===========================
if (process.env.NODE_ENV !== 'production') {
watchEffect(() => {
warningProps(props);
});
}
// ========================= FieldNames =========================
const mergedFieldNames = computed<InternalFieldName>(() => fillFieldNames(props.fieldNames));
// =========================== Search ===========================
const [mergedSearchValue, setSearchValue] = useMergedState('', {
value: computed(() =>
props.searchValue !== undefined ? props.searchValue : props.inputValue,
),
postState: search => search || '',
});
const onInternalSearch: BaseSelectProps['onSearch'] = searchText => {
setSearchValue(searchText);
props.onSearch?.(searchText);
};
// ============================ Data ============================
// `useTreeData` only do convert of `children` or `simpleMode`.
// Else will return origin `treeData` for perf consideration.
// Do not do anything to loop the data.
const mergedTreeData = useTreeData(
toRef(props, 'treeData'),
toRef(props, 'children'),
toRef(props, 'treeDataSimpleMode'),
);
const { keyEntities, valueEntities } = useDataEntities(mergedTreeData, mergedFieldNames);
/** 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 (valueEntities.value.has(val)) {
existRawValues.push(val);
} else {
missingRawValues.push(val);
}
});
return { missingRawValues, existRawValues };
};
// Filtered Tree
const filteredTreeData = useFilterTreeData(mergedTreeData, mergedSearchValue, {
fieldNames: mergedFieldNames,
treeNodeFilterProp: toRef(props, 'treeNodeFilterProp'),
filterTreeNode: toRef(props, 'filterTreeNode'),
});
// =========================== Label ============================
const getLabel = (item: DefaultOptionType) => {
if (item) {
if (props.treeNodeLabelProp) {
return item[props.treeNodeLabelProp];
}
// Loop from fieldNames
const { _title: titleList } = mergedFieldNames.value;
for (let i = 0; i < titleList.length; i += 1) {
const title = item[titleList[i]];
if (title !== undefined) {
return title;
}
}
}
};
// ========================= Wrap Value =========================
const toLabeledValues = (draftValues: DraftValueType) => {
const values = toArray(draftValues);
return values.map(val => {
if (isRawValue(val)) {
return { value: val };
}
return val;
});
};
const convert2LabelValues = (draftValues: DraftValueType) => {
const values = toLabeledValues(draftValues);
return values.map(item => {
let { label: rawLabel } = item;
const { value: rawValue, halfChecked: rawHalfChecked } = item;
let rawDisabled: boolean | undefined;
const entity = valueEntities.value.get(rawValue);
// Fill missing label & status
if (entity) {
rawLabel = rawLabel ?? getLabel(entity.node);
rawDisabled = entity.node.disabled;
}
return {
label: rawLabel,
value: rawValue,
halfChecked: rawHalfChecked,
disabled: rawDisabled,
};
});
};
// =========================== Values ===========================
const [internalValue, setInternalValue] = useMergedState(props.defaultValue, {
value: toRef(props, 'value'),
});
const rawMixedLabeledValues = computed(() => toLabeledValues(internalValue.value));
// Split value into full check and half check
const rawLabeledValues = shallowRef([]);
const rawHalfLabeledValues = shallowRef([]);
watchEffect(() => {
const fullCheckValues: LabeledValueType[] = [];
const halfCheckValues: LabeledValueType[] = [];
rawMixedLabeledValues.value.forEach(item => {
if (item.halfChecked) {
halfCheckValues.push(item);
} else {
fullCheckValues.push(item);
}
});
rawLabeledValues.value = fullCheckValues;
rawHalfLabeledValues.value = halfCheckValues;
});
// const [mergedValues] = useCache(rawLabeledValues);
const rawValues = computed(() => rawLabeledValues.value.map(item => item.value));
// Convert value to key. Will fill missed keys for conduct check.
const [rawCheckedValues, rawHalfCheckedValues] = useCheckedKeys(
rawLabeledValues,
rawHalfLabeledValues,
treeConduction,
keyEntities,
);
// Convert rawCheckedKeys to check strategy related values
const displayValues = computed(() => {
// Collect keys which need to show
const displayKeys = formatStrategyValues(
rawCheckedValues.value,
props.showCheckedStrategy,
keyEntities.value,
mergedFieldNames.value,
);
// Convert to value and filled with label
const values = displayKeys.map(
key => keyEntities.value[key]?.node?.[mergedFieldNames.value.value] ?? key,
);
const rawDisplayValues = convert2LabelValues(values);
const firstVal = rawDisplayValues[0];
if (!mergedMultiple.value && firstVal && isNil(firstVal.value) && isNil(firstVal.label)) {
return [];
}
return rawDisplayValues.map(item => ({
...item,
label: item.label ?? item.value,
}));
});
const [cachedDisplayValues] = useCache(displayValues);
// =========================== Change ===========================
const triggerChange = (
newRawValues: RawValueType[],
extra: { triggerValue?: RawValueType; selected?: boolean },
source: SelectSource,
) => {
const labeledValues = convert2LabelValues(newRawValues);
setInternalValue(labeledValues);
// Clean up if needed
if (props.autoClearSearchValue) {
setSearchValue('');
}
// Generate rest parameters is costly, so only do it when necessary
if (props.onChange) {
let eventValues: RawValueType[] = newRawValues;
if (treeConduction.value) {
const formattedKeyList = formatStrategyValues(
newRawValues,
props.showCheckedStrategy,
keyEntities.value,
mergedFieldNames.value,
);
eventValues = formattedKeyList.map(key => {
const entity = valueEntities.value.get(key);
return entity ? entity.node[mergedFieldNames.value.value] : key;
});
}
const { triggerValue, selected } = extra || {
triggerValue: undefined,
selected: undefined,
};
let returnRawValues: (LabeledValueType | RawValueType)[] = eventValues;
// We need fill half check back
if (props.treeCheckStrictly) {
const halfValues = rawHalfLabeledValues.value.filter(
item => !eventValues.includes(item.value),
);
returnRawValues = [...returnRawValues, ...halfValues];
}
const returnLabeledValues = convert2LabelValues(returnRawValues);
const additionalInfo = {
// [Legacy] Always return as array contains label & value
preValue: rawLabeledValues.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 (props.treeCheckStrictly || (source === 'selection' && !selected)) {
showPosition = false;
}
fillAdditionalInfo(
additionalInfo,
triggerValue,
newRawValues,
mergedTreeData.value,
showPosition,
mergedFieldNames.value,
);
if (mergedCheckable.value) {
additionalInfo.checked = selected;
} else {
additionalInfo.selected = selected;
}
const returnValues = mergedLabelInValue.value
? returnLabeledValues
: returnLabeledValues.map(item => item.value);
props.onChange(
mergedMultiple.value ? returnValues : returnValues[0],
mergedLabelInValue.value ? null : returnLabeledValues.map(item => item.label),
additionalInfo,
);
}
};
// ========================== Options ===========================
/** Trigger by option list */
const onOptionSelect = (
selectedKey: Key,
{ selected, source }: { selected: boolean; source: SelectSource },
) => {
const entity = keyEntities.value[selectedKey];
const node = entity?.node;
const selectedValue = node?.[mergedFieldNames.value.value] ?? selectedKey;
// Never be falsy but keep it safe
if (!mergedMultiple.value) {
// Single mode always set value
triggerChange([selectedValue], { selected: true, triggerValue: selectedValue }, 'option');
} else {
let newRawValues = selected
? [...rawValues.value, selectedValue]
: rawCheckedValues.value.filter(v => v !== selectedValue);
// Add keys if tree conduction
if (treeConduction.value) {
// Should keep missing values
const { missingRawValues, existRawValues } = splitRawValues(newRawValues);
const keyList = existRawValues.map(val => valueEntities.value.get(val).key);
// Conduction by selected or not
let checkedKeys: Key[];
if (selected) {
({ checkedKeys } = conductCheck(keyList, true, keyEntities.value));
} else {
({ checkedKeys } = conductCheck(
keyList,
{ checked: false, halfCheckedKeys: rawHalfCheckedValues.value },
keyEntities.value,
));
}
// Fill back of keys
newRawValues = [
...missingRawValues,
...checkedKeys.map(key => keyEntities.value[key].node[mergedFieldNames.value.value]),
];
}
triggerChange(newRawValues, { selected, triggerValue: selectedValue }, source || 'option');
}
// Trigger select event
if (selected || !mergedMultiple.value) {
props.onSelect?.(selectedValue, fillLegacyProps(node));
} else {
props.onDeselect?.(selectedValue, fillLegacyProps(node));
}
};
// ========================== Dropdown ==========================
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);
}
};
// ====================== Display Change ========================
const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (newValues, info) => {
const newRawValues = newValues.map(item => item.value);
if (info.type === 'clear') {
triggerChange(newRawValues, {}, 'selection');
return;
}
// TreeSelect only have multiple mode which means display change only has remove
if (info.values.length) {
onOptionSelect(info.values[0].value, { selected: false, source: 'selection' });
}
};
const {
treeNodeFilterProp,
// Data
loadData,
treeLoadedKeys,
onTreeLoad,
// Expanded
treeDefaultExpandAll,
treeExpandedKeys,
treeDefaultExpandedKeys,
onTreeExpand,
// Options
virtual,
listHeight,
listItemHeight,
// Tree
treeLine,
treeIcon,
showTreeIcon,
switcherIcon,
treeMotion,
customSlots,
} = toRefs(props);
toRaw;
useProvideLegacySelectContext(
toReactive({
checkable: mergedCheckable,
loadData,
treeLoadedKeys,
onTreeLoad,
checkedKeys: rawCheckedValues,
halfCheckedKeys: rawHalfCheckedValues,
treeDefaultExpandAll,
treeExpandedKeys,
treeDefaultExpandedKeys,
onTreeExpand,
treeIcon,
treeMotion,
showTreeIcon,
switcherIcon,
treeLine,
treeNodeFilterProp,
keyEntities,
customSlots,
} as unknown as LegacyContextProps),
);
useProvideSelectContext(
toReactive({
virtual,
listHeight,
listItemHeight,
treeData: filteredTreeData,
fieldNames: mergedFieldNames,
onSelect: onOptionSelect,
} as unknown as TreeSelectContextProps),
);
const selectRef = ref<BaseSelectRef>();
expose({
focus() {
selectRef.value?.focus();
},
blur() {
selectRef.value?.blur();
},
scrollTo(arg) {
selectRef.value?.scrollTo(arg);
},
} as BaseSelectRef);
return () => {
const restProps = omit(props, [
'id',
'prefixCls',
// Value
'value',
'defaultValue',
'onChange',
'onSelect',
'onDeselect',
// Search
'searchValue',
'inputValue',
'onSearch',
'autoClearSearchValue',
'filterTreeNode',
'treeNodeFilterProp',
// Selector
'showCheckedStrategy',
'treeNodeLabelProp',
// Mode
'multiple',
'treeCheckable',
'treeCheckStrictly',
'labelInValue',
// FieldNames
'fieldNames',
// Data
'treeDataSimpleMode',
'treeData',
'children',
'loadData',
'treeLoadedKeys',
'onTreeLoad',
// Expanded
'treeDefaultExpandAll',
'treeExpandedKeys',
'treeDefaultExpandedKeys',
'onTreeExpand',
// Options
'virtual',
'listHeight',
'listItemHeight',
'onDropdownVisibleChange',
// Tree
'treeLine',
'treeIcon',
'showTreeIcon',
'switcherIcon',
'treeMotion',
]);
return (
<BaseSelect
v-slots={slots}
ref={selectRef}
{...attrs}
{...restProps}
// >>> MISC
id={mergedId}
prefixCls={props.prefixCls}
mode={mergedMultiple.value ? 'multiple' : undefined}
// >>> Display Value
displayValues={cachedDisplayValues.value}
onDisplayValuesChange={onDisplayValuesChange}
// >>> Search
searchValue={mergedSearchValue.value}
onSearch={onInternalSearch}
// >>> Options
OptionList={OptionList}
emptyOptions={!mergedTreeData.value.length}
onDropdownVisibleChange={onInternalDropdownVisibleChange}
/>
);
};
},
});

View File

@ -3,28 +3,28 @@ import type { DataEntity } from '../../vc-tree/interface';
import { conductCheck } from '../../vc-tree/utils/conductUtil'; import { conductCheck } from '../../vc-tree/utils/conductUtil';
import type { LabeledValueType, RawValueType } from '../TreeSelect'; import type { LabeledValueType, RawValueType } from '../TreeSelect';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed } from 'vue'; import { shallowRef, watchEffect } from 'vue';
export default ( export default (
rawLabeledValues: Ref<LabeledValueType[]>, rawLabeledValues: Ref<LabeledValueType[]>,
rawHalfCheckedValues: Ref<LabeledValueType[]>, rawHalfCheckedValues: Ref<LabeledValueType[]>,
treeConduction: Ref<boolean>, treeConduction: Ref<boolean>,
keyEntities: Ref<Record<Key, DataEntity>>, keyEntities: Ref<Record<Key, DataEntity>>,
) => ) => {
computed(() => { const newRawCheckedValues = shallowRef<RawValueType[]>([]);
const newRawHalfCheckedValues = shallowRef<RawValueType[]>([]);
watchEffect(() => {
let checkedKeys: RawValueType[] = rawLabeledValues.value.map(({ value }) => value); let checkedKeys: RawValueType[] = rawLabeledValues.value.map(({ value }) => value);
let halfCheckedKeys: RawValueType[] = rawHalfCheckedValues.value.map(({ value }) => value); let halfCheckedKeys: RawValueType[] = rawHalfCheckedValues.value.map(({ value }) => value);
const missingValues = checkedKeys.filter(key => !keyEntities[key]); const missingValues = checkedKeys.filter(key => !keyEntities.value[key]);
if (treeConduction.value) { if (treeConduction.value) {
({ checkedKeys, halfCheckedKeys } = conductCheck(checkedKeys, true, keyEntities.value)); ({ checkedKeys, halfCheckedKeys } = conductCheck(checkedKeys, true, keyEntities.value));
} }
newRawCheckedValues.value = Array.from(new Set([...missingValues, ...checkedKeys]));
return [ newRawHalfCheckedValues.value = halfCheckedKeys;
// Checked keys should fill with missing keys which should de-duplicated
Array.from(new Set([...missingValues, ...checkedKeys])),
// Half checked keys
halfCheckedKeys,
];
}); });
return [newRawCheckedValues, newRawHalfCheckedValues];
};

View File

@ -4,14 +4,13 @@ import type { FieldNames, RawValueType } from '../TreeSelect';
import { isNil } from '../utils/valueUtil'; import { isNil } from '../utils/valueUtil';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed } from 'vue'; import { ref, watchEffect } from 'vue';
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
export default (treeData: Ref<any>, fieldNames: Ref<FieldNames>) => export default (treeData: Ref<any>, fieldNames: Ref<FieldNames>) => {
computed<{ const valueEntities = ref<Map<RawValueType, DataEntity>>(new Map());
valueEntities: Map<RawValueType, DataEntity>; const keyEntities = ref<Record<string, DataEntity>>({});
keyEntities: Record<string, DataEntity>; watchEffect(() => {
}>(() => {
const collection = convertDataToEntities(treeData.value, { const collection = convertDataToEntities(treeData.value, {
fieldNames: fieldNames.value, fieldNames: fieldNames.value,
initWrapper: wrapper => ({ initWrapper: wrapper => ({
@ -34,7 +33,9 @@ export default (treeData: Ref<any>, fieldNames: Ref<FieldNames>) =>
} }
wrapper.valueEntities.set(val, entity); wrapper.valueEntities.set(val, entity);
}, },
}) as any;
valueEntities.value = collection.valueEntities;
keyEntities.value = collection.keyEntities;
}); });
return { valueEntities, keyEntities };
return collection as any; };
});

View File

@ -1,16 +1,9 @@
import { warning } from '../../vc-util/warning'; import type { Ref } from 'vue';
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue'; import { computed } from 'vue';
import type { import type { DataNode, SimpleModeConfig } from '../interface';
DataNode,
InternalDataEntity,
SimpleModeConfig,
RawValueType,
FieldNames,
} from '../interface';
import { convertChildrenToData } from '../utils/legacyUtil'; import { convertChildrenToData } from '../utils/legacyUtil';
import type { DefaultOptionType } from '../TreeSelect';
const MAX_WARNING_TIMES = 10; import type { VueNode } from 'ant-design-vue/es/_util/type';
function parseSimpleTreeData( function parseSimpleTreeData(
treeData: DataNode[], treeData: DataNode[],
@ -48,108 +41,27 @@ function parseSimpleTreeData(
return rootNodeList; return rootNodeList;
} }
/**
* Format `treeData` with `value` & `key` which is used for calculation
*/
function formatTreeData(
treeData: DataNode[],
getLabelProp: (node: DataNode) => any,
fieldNames: FieldNames,
): InternalDataEntity[] {
let warningTimes = 0;
const valueSet = new Set<RawValueType>();
// Field names
const { value: fieldValue, children: fieldChildren } = fieldNames;
function dig(dataNodes: DataNode[]) {
return (dataNodes || []).map(node => {
const { key, disableCheckbox, disabled, checkable, selectable, isLeaf } = node;
const value = node[fieldValue];
const mergedValue = fieldValue in node ? value : key;
const dataNode: InternalDataEntity = {
disableCheckbox,
disabled,
key: key !== null && key !== undefined ? key : mergedValue,
value: mergedValue,
title: getLabelProp(node),
node,
selectable,
isLeaf,
dataRef: node,
checkable,
};
if (node.slots) {
dataNode.slots = node.slots;
}
// Check `key` & `value` and warning user
if (process.env.NODE_ENV !== 'production') {
if (
key !== null &&
key !== undefined &&
value !== undefined &&
String(key) !== String(value) &&
warningTimes < MAX_WARNING_TIMES
) {
warningTimes += 1;
warning(
false,
`\`key\` or \`value\` with TreeNode must be the same or you can remove one of them. key: ${key}, value: ${value}.`,
);
}
warning(!valueSet.has(value), `Same \`value\` exist in the tree: ${value}`);
valueSet.add(value);
}
if (fieldChildren in node) {
dataNode.children = dig(node[fieldChildren]);
}
return dataNode;
});
}
return dig(treeData);
}
/** /**
* Convert `treeData` or `children` into formatted `treeData`. * Convert `treeData` or `children` into formatted `treeData`.
* Will not re-calculate if `treeData` or `children` not change. * Will not re-calculate if `treeData` or `children` not change.
*/ */
export default function useTreeData( export default function useTreeData(
treeData: Ref<DataNode[]>, treeData: Ref<DataNode[]>,
children: Ref<any[]>, children: Ref<VueNode[]>,
{ simpleMode: Ref<boolean | SimpleModeConfig>,
getLabelProp, ): Ref<DefaultOptionType[]> {
simpleMode,
fieldNames,
}: {
getLabelProp: (node: DataNode) => any;
simpleMode: Ref<boolean | SimpleModeConfig>;
fieldNames: Ref<FieldNames>;
},
): ComputedRef<InternalDataEntity[]> {
return computed(() => { return computed(() => {
if (treeData.value) { if (treeData.value) {
return formatTreeData( return simpleMode.value
simpleMode.value
? parseSimpleTreeData(treeData.value, { ? parseSimpleTreeData(treeData.value, {
id: 'id', id: 'id',
pId: 'pId', pId: 'pId',
rootPId: null, rootPId: null,
...(simpleMode.value !== true ? simpleMode.value : {}), ...(simpleMode.value !== true ? simpleMode.value : {}),
}) })
: treeData.value, : treeData.value;
getLabelProp,
fieldNames.value,
);
} else {
return formatTreeData(convertChildrenToData(children.value), getLabelProp, fieldNames.value);
} }
return convertChildrenToData(children.value);
}); });
} }

View File

@ -1,9 +1,8 @@
// base rc-tree-select@4.6.1 // base rc-tree-select@5.0.0-alpha.4
import TreeSelect from './TreeSelect'; import type { TreeSelectProps } from './TreeSelect';
import TreeSelect, { treeSelectProps } from './TreeSelect';
import TreeNode from './TreeNode'; import TreeNode from './TreeNode';
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil'; 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 { TreeNode, SHOW_ALL, SHOW_CHILD, SHOW_PARENT, treeSelectProps };
export type { TreeSelectProps }; export type { TreeSelectProps };

View File

@ -1,21 +1,15 @@
import { filterEmpty } from '../../_util/props-util'; import { filterEmpty } from '../../_util/props-util';
import { camelize } from 'vue'; import { camelize } from 'vue';
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
import type { import type { DataNode, ChangeEventExtra, RawValueType, LegacyCheckedNode } from '../interface';
DataNode,
LegacyDataNode,
ChangeEventExtra,
InternalDataEntity,
RawValueType,
LegacyCheckedNode,
} from '../interface';
import TreeNode from '../TreeNode'; import TreeNode from '../TreeNode';
import type { VueNode } from '../../_util/type'; import type { VueNode } from '../../_util/type';
import type { DefaultOptionType, FieldNames } from '../TreeSelect';
function isTreeSelectNode(node: any) { function isTreeSelectNode(node: any) {
return node && node.type && (node.type as any).isTreeSelectNode; return node && node.type && (node.type as any).isTreeSelectNode;
} }
export function convertChildrenToData(rootNodes: VueNode): DataNode[] { export function convertChildrenToData(rootNodes: VueNode[]): DataNode[] {
function dig(treeNodes: any[] = []): DataNode[] { function dig(treeNodes: any[] = []): DataNode[] {
return filterEmpty(treeNodes).map(treeNode => { return filterEmpty(treeNodes).map(treeNode => {
// Filter invalidate node // Filter invalidate node
@ -66,10 +60,10 @@ export function convertChildrenToData(rootNodes: VueNode): DataNode[] {
return dig(rootNodes as any[]); return dig(rootNodes as any[]);
} }
export function fillLegacyProps(dataNode: DataNode): LegacyDataNode { export function fillLegacyProps(dataNode: DataNode): any {
// Skip if not dataNode exist // Skip if not dataNode exist
if (!dataNode) { if (!dataNode) {
return dataNode as LegacyDataNode; return dataNode;
} }
const cloneNode = { ...dataNode }; const cloneNode = { ...dataNode };
@ -79,37 +73,43 @@ export function fillLegacyProps(dataNode: DataNode): LegacyDataNode {
get() { get() {
warning( warning(
false, false,
'New `rc-tree-select` not support return node instance as argument anymore. Please consider to remove `props` access.', 'New `vc-tree-select` not support return node instance as argument anymore. Please consider to remove `props` access.',
); );
return cloneNode; return cloneNode;
}, },
}); });
} }
return cloneNode as LegacyDataNode; return cloneNode;
} }
export function fillAdditionalInfo( export function fillAdditionalInfo(
extra: ChangeEventExtra, extra: ChangeEventExtra,
triggerValue: RawValueType, triggerValue: RawValueType,
checkedValues: RawValueType[], checkedValues: RawValueType[],
treeData: InternalDataEntity[], treeData: DefaultOptionType[],
showPosition: boolean, showPosition: boolean,
fieldNames: FieldNames,
) { ) {
let triggerNode = null; let triggerNode = null;
let nodeList: LegacyCheckedNode[] = null; let nodeList: LegacyCheckedNode[] = null;
function generateMap() { function generateMap() {
function dig(list: InternalDataEntity[], level = '0', parentIncluded = false) { function dig(list: DefaultOptionType[], level = '0', parentIncluded = false) {
return list return list
.map((dataNode, index) => { .map((option, index) => {
const pos = `${level}-${index}`; const pos = `${level}-${index}`;
const included = checkedValues.includes(dataNode.value); const value = option[fieldNames.value];
const children = dig(dataNode.children || [], pos, included); const included = checkedValues.includes(value);
const node = <TreeNode {...dataNode}>{children.map(child => child.node)}</TreeNode>; const children = dig(option[fieldNames.children] || [], pos, included);
const node = (
<TreeNode {...(option as Required<DefaultOptionType>)}>
{children.map(child => child.node)}
</TreeNode>
);
// Link with trigger node // Link with trigger node
if (triggerValue === dataNode.value) { if (triggerValue === value) {
triggerNode = node; triggerNode = node;
} }

View File

@ -1,5 +1,6 @@
import type { DataEntity } from '../../vc-tree/interface'; import type { DataEntity } from '../../vc-tree/interface';
import type { RawValueType, Key, DataNode } from '../interface'; import type { InternalFieldName } from '../TreeSelect';
import type { RawValueType, Key } from '../interface';
import { isCheckDisabled } from './valueUtil'; import { isCheckDisabled } from './valueUtil';
export const SHOW_ALL = 'SHOW_ALL'; export const SHOW_ALL = 'SHOW_ALL';
@ -8,22 +9,23 @@ export const SHOW_CHILD = 'SHOW_CHILD';
export type CheckedStrategy = typeof SHOW_ALL | typeof SHOW_PARENT | typeof SHOW_CHILD; export type CheckedStrategy = typeof SHOW_ALL | typeof SHOW_PARENT | typeof SHOW_CHILD;
export function formatStrategyKeys( export function formatStrategyValues(
keys: Key[], values: Key[],
strategy: CheckedStrategy, strategy: CheckedStrategy,
keyEntities: Record<Key, DataEntity>, keyEntities: Record<Key, DataEntity>,
fieldNames: InternalFieldName,
): RawValueType[] { ): RawValueType[] {
const keySet = new Set(keys); const valueSet = new Set(values);
if (strategy === SHOW_CHILD) { if (strategy === SHOW_CHILD) {
return keys.filter(key => { return values.filter(key => {
const entity = keyEntities[key]; const entity = keyEntities[key];
if ( if (
entity && entity &&
entity.children && entity.children &&
entity.children.every( entity.children.every(
({ node }) => isCheckDisabled(node) || keySet.has((node as DataNode).key), ({ node }) => isCheckDisabled(node) || valueSet.has(node[fieldNames.value]),
) )
) { ) {
return false; return false;
@ -32,15 +34,14 @@ export function formatStrategyKeys(
}); });
} }
if (strategy === SHOW_PARENT) { if (strategy === SHOW_PARENT) {
return keys.filter(key => { return values.filter(key => {
const entity = keyEntities[key]; const entity = keyEntities[key];
const parent = entity ? entity.parent : null; const parent = entity ? entity.parent : null;
if (parent && !isCheckDisabled(parent.node) && valueSet.has(parent.key)) {
if (parent && !isCheckDisabled(parent.node) && keySet.has((parent.node as DataNode).key)) {
return false; return false;
} }
return true; return true;
}); });
} }
return keys; return values;
} }

View File

@ -1,21 +1,5 @@
import type { import type { Key, DataNode, FieldNames } from '../interface';
FlattenDataNode, import type { DefaultOptionType, InternalFieldName } from '../TreeSelect';
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[] { export function toArray<T>(value: T | T[]): T[] {
if (Array.isArray(value)) { if (Array.isArray(value)) {
@ -24,221 +8,43 @@ export function toArray<T>(value: T | T[]): T[] {
return value !== undefined ? [value] : []; return value !== undefined ? [value] : [];
} }
/** export function fillFieldNames(fieldNames?: FieldNames) {
* 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 { label, value, children } = fieldNames || {};
const filledNames: FieldNames = { const mergedValue = value || 'value';
value: value || 'value',
return {
_title: label ? [label] : ['title', 'label'],
value: mergedValue,
key: mergedValue,
children: children || 'children', 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) { export function isCheckDisabled(node: DataNode) {
return node.disabled || node.disableCheckbox || node.checkable === false; return node.disabled || node.disableCheckbox || node.checkable === false;
} }
interface TreeDataNode extends InternalDataEntity { /** Loop fetch all the keys exist in the tree */
key: Key; export function getAllKeys(treeData: DefaultOptionType[], fieldNames: InternalFieldName) {
children?: TreeDataNode[]; const keys: Key[] = [];
}
function getLevel({ parent }: FlattenNode): number { function dig(list: DefaultOptionType[]) {
let level = 0; list.forEach(item => {
let current = parent; keys.push(item[fieldNames.value]);
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,
};
const children = item[fieldNames.children];
if (children) { if (children) {
clone.children = fillKey(children); dig(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; dig(treeData);
});
return keys;
} }
export function addValue(rawValues: RawValueType[], value: RawValueType) { export function isNil(val: any) {
const values = new Set(rawValues); return val === null || val === undefined;
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);
} }

View File

@ -1,7 +1,8 @@
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
import type { TreeSelectProps } from '../TreeSelect';
import { toArray } from './valueUtil'; import { toArray } from './valueUtil';
function warningProps(props: any) { function warningProps(props: TreeSelectProps & { searchPlaceholder?: string }) {
const { searchPlaceholder, treeCheckStrictly, treeCheckable, labelInValue, value, multiple } = const { searchPlaceholder, treeCheckStrictly, treeCheckable, labelInValue, value, multiple } =
props; props;

View File

@ -1,16 +1,14 @@
import type { TreeDataNode, Key } from './interface'; import type { DataNode, TreeDataNode, Key } from './interface';
import { useInjectTreeSelectContext } from './Context';
import type { RefOptionListProps } from '../vc-select/OptionList'; import type { RefOptionListProps } from '../vc-select/OptionList';
import type { ScrollTo } from '../vc-virtual-list/List'; import type { ScrollTo } from '../vc-virtual-list/List';
import { computed, defineComponent, nextTick, ref, shallowRef, watch } from 'vue'; import { computed, defineComponent, nextTick, ref, shallowRef, watch } from 'vue';
import { optionListProps } from './props';
import useMemo from '../_util/hooks/useMemo'; import useMemo from '../_util/hooks/useMemo';
import type { EventDataNode } from '../tree'; import type { EventDataNode } from '../tree';
import KeyCode from '../_util/KeyCode'; import KeyCode from '../_util/KeyCode';
import Tree from '../vc-tree/Tree'; import Tree from '../vc-tree/Tree';
import type { TreeProps } from '../vc-tree/props'; import type { TreeProps } from '../vc-tree/props';
import { getAllKeys, isCheckDisabled } from './utils/valueUtil';
import { useBaseProps } from '../vc-select';
import useInjectLegacySelectContext from './LegacyContext';
import useInjectSelectContext from './TreeSelectContext';
const HIDDEN_STYLE = { const HIDDEN_STYLE = {
width: 0, width: 0,
@ -34,36 +32,44 @@ type ReviseRefOptionListProps = Omit<RefOptionListProps, 'scrollTo'> & { scrollT
export default defineComponent({ export default defineComponent({
name: 'OptionList', name: 'OptionList',
inheritAttrs: false, inheritAttrs: false,
props: optionListProps<DataNode>(),
slots: ['notFoundContent', 'menuItemSelectedIcon'], slots: ['notFoundContent', 'menuItemSelectedIcon'],
setup(_, { slots, expose }) { setup(props, { slots, expose }) {
const baseProps = useBaseProps(); const context = useInjectTreeSelectContext();
const legacyContext = useInjectLegacySelectContext();
const context = useInjectSelectContext();
const treeRef = ref(); const treeRef = ref();
const memoTreeData = useMemo( const memoOptions = useMemo(
() => context.treeData, () => props.options,
[() => baseProps.open, () => context.treeData], [() => props.open, () => props.options],
next => next[0], 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 mergedCheckedKeys = computed(() => {
const { checkable, halfCheckedKeys, checkedKeys } = legacyContext; const { checkable, halfCheckedKeys } = context.value;
if (!checkable) { if (!checkable) {
return null; return null;
} }
return { return {
checked: checkedKeys, checked: valueKeys.value,
halfChecked: halfCheckedKeys, halfChecked: halfCheckedKeys,
}; };
}); });
watch( watch(
() => baseProps.open, () => props.open,
() => { () => {
nextTick(() => { nextTick(() => {
if (baseProps.open && !baseProps.multiple && legacyContext.checkedKeys.length) { if (props.open && !props.multiple && valueKeys.value.length) {
treeRef.value?.scrollTo({ key: legacyContext.checkedKeys[0] }); treeRef.value?.scrollTo({ key: valueKeys.value[0] });
} }
}); });
}, },
@ -71,25 +77,25 @@ export default defineComponent({
); );
// ========================== Search ========================== // ========================== Search ==========================
const lowerSearchValue = computed(() => String(baseProps.searchValue).toLowerCase()); const lowerSearchValue = computed(() => String(props.searchValue).toLowerCase());
const filterTreeNode = (treeNode: EventDataNode) => { const filterTreeNode = (treeNode: EventDataNode) => {
if (!lowerSearchValue.value) { if (!lowerSearchValue.value) {
return false; return false;
} }
return String(treeNode[legacyContext.treeNodeFilterProp]) return String(treeNode[context.value.treeNodeFilterProp])
.toLowerCase() .toLowerCase()
.includes(lowerSearchValue.value); .includes(lowerSearchValue.value);
}; };
// =========================== Keys =========================== // =========================== Keys ===========================
const expandedKeys = shallowRef<Key[]>(legacyContext.treeDefaultExpandedKeys); const expandedKeys = shallowRef<Key[]>(context.value.treeDefaultExpandedKeys);
const searchExpandedKeys = shallowRef<Key[]>(null); const searchExpandedKeys = shallowRef<Key[]>(null);
watch( watch(
() => baseProps.searchValue, () => props.searchValue,
() => { () => {
if (baseProps.searchValue) { if (props.searchValue) {
searchExpandedKeys.value = getAllKeys(context.treeData, context.fieldNames); searchExpandedKeys.value = props.flattenOptions.map(o => o.key);
} }
}, },
{ {
@ -97,17 +103,17 @@ export default defineComponent({
}, },
); );
const mergedExpandedKeys = computed(() => { const mergedExpandedKeys = computed(() => {
if (legacyContext.treeExpandedKeys) { if (context.value.treeExpandedKeys) {
return [...legacyContext.treeExpandedKeys]; return [...context.value.treeExpandedKeys];
} }
return baseProps.searchValue ? searchExpandedKeys.value : expandedKeys.value; return props.searchValue ? searchExpandedKeys.value : expandedKeys.value;
}); });
const onInternalExpand = (keys: Key[]) => { const onInternalExpand = (keys: Key[]) => {
expandedKeys.value = keys; expandedKeys.value = keys;
searchExpandedKeys.value = keys; searchExpandedKeys.value = keys;
legacyContext.onTreeExpand?.(keys); context.value.onTreeExpand?.(keys);
}; };
// ========================== Events ========================== // ========================== Events ==========================
@ -115,23 +121,23 @@ export default defineComponent({
event.preventDefault(); event.preventDefault();
}; };
const onInternalSelect = (_: Key[], { node }: TreeEventInfo) => { const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => {
const { checkable, checkedKeys } = legacyContext; const { getEntityByKey, checkable, checkedKeys } = context.value;
if (checkable && isCheckDisabled(node)) { const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select');
return; if (entity !== null) {
} props.onSelect?.(entity.data.value, {
context.onSelect?.(node.key, { selected: !checkedKeys.includes(entity.data.value),
selected: !checkedKeys.includes(node.key),
}); });
}
if (!baseProps.multiple) { if (!props.multiple) {
baseProps.toggleOpen?.(false); props.onToggleOpen?.(false);
} }
}; };
// ========================= Keyboard ========================= // ========================= Keyboard =========================
const activeKey = ref<Key>(null); const activeKey = ref<Key>(null);
const activeEntity = computed(() => legacyContext.keyEntities[activeKey.value]); const activeEntity = computed(() => context.value.getEntityByKey(activeKey.value));
const setActiveKey = (key: Key) => { const setActiveKey = (key: Key) => {
activeKey.value = key; activeKey.value = key;
@ -151,11 +157,11 @@ export default defineComponent({
// >>> Select item // >>> Select item
case KeyCode.ENTER: { case KeyCode.ENTER: {
const { selectable, value } = activeEntity.value?.node || {}; const { selectable, value } = activeEntity.value?.data.node || {};
if (selectable !== false) { if (selectable !== false) {
onInternalSelect(null, { onInternalSelect(null, {
node: { key: activeKey.value }, node: { key: activeKey.value },
selected: !legacyContext.checkedKeys.includes(value), selected: !context.value.checkedKeys.includes(value),
}); });
} }
break; break;
@ -163,7 +169,7 @@ export default defineComponent({
// >>> Close // >>> Close
case KeyCode.ESC: { case KeyCode.ESC: {
baseProps.toggleOpen(false); props.onToggleOpen(false);
} }
} }
}, },
@ -173,12 +179,15 @@ export default defineComponent({
return () => { return () => {
const { const {
prefixCls, prefixCls,
height,
itemHeight,
virtual,
multiple, multiple,
searchValue, searchValue,
open, open,
notFoundContent = slots.notFoundContent?.(), notFoundContent = slots.notFoundContent?.(),
} = baseProps; onMouseenter,
const { listHeight, listItemHeight, virtual } = context; } = props;
const { const {
checkable, checkable,
treeDefaultExpandAll, treeDefaultExpandAll,
@ -190,10 +199,9 @@ export default defineComponent({
treeLoadedKeys, treeLoadedKeys,
treeMotion, treeMotion,
onTreeLoad, onTreeLoad,
checkedKeys, } = context.value;
} = legacyContext;
// ========================== Render ========================== // ========================== Render ==========================
if (memoTreeData.value.length === 0) { if (memoOptions.value.length === 0) {
return ( return (
<div role="listbox" class={`${prefixCls}-empty`} onMousedown={onListMouseDown}> <div role="listbox" class={`${prefixCls}-empty`} onMousedown={onListMouseDown}>
{notFoundContent} {notFoundContent}
@ -209,10 +217,10 @@ export default defineComponent({
treeProps.expandedKeys = mergedExpandedKeys.value; treeProps.expandedKeys = mergedExpandedKeys.value;
} }
return ( return (
<div onMousedown={onListMouseDown}> <div onMousedown={onListMouseDown} onMouseenter={onMouseenter}>
{activeEntity.value && open && ( {activeEntity.value && open && (
<span style={HIDDEN_STYLE} aria-live="assertive"> <span style={HIDDEN_STYLE} aria-live="assertive">
{activeEntity.value.node.value} {activeEntity.value.data.value}
</span> </span>
)} )}
@ -220,9 +228,9 @@ export default defineComponent({
ref={treeRef} ref={treeRef}
focusable={false} focusable={false}
prefixCls={`${prefixCls}-tree`} prefixCls={`${prefixCls}-tree`}
treeData={memoTreeData.value as TreeDataNode[]} treeData={memoOptions.value as TreeDataNode[]}
height={listHeight} height={height}
itemHeight={listItemHeight} itemHeight={itemHeight}
virtual={virtual} virtual={virtual}
multiple={multiple} multiple={multiple}
icon={treeIcon} icon={treeIcon}
@ -235,7 +243,7 @@ export default defineComponent({
checkable={checkable} checkable={checkable}
checkStrictly checkStrictly
checkedKeys={mergedCheckedKeys.value} checkedKeys={mergedCheckedKeys.value}
selectedKeys={!checkable ? checkedKeys : []} selectedKeys={!checkable ? valueKeys.value : []}
defaultExpandAll={treeDefaultExpandAll} defaultExpandAll={treeDefaultExpandAll}
{...treeProps} {...treeProps}
// Proxy event out // Proxy event out
@ -245,7 +253,7 @@ export default defineComponent({
onExpand={onInternalExpand} onExpand={onInternalExpand}
onLoad={onTreeLoad} onLoad={onTreeLoad}
filterTreeNode={filterTreeNode} filterTreeNode={filterTreeNode}
v-slots={{ ...slots, checkable: legacyContext.customCheckable }} v-slots={{ ...slots, checkable: context.value.customCheckable }}
/> />
</div> </div>
); );

View File

@ -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;

View File

@ -0,0 +1,155 @@
import { warning } from '../../vc-util/warning';
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';
import type {
DataNode,
InternalDataEntity,
SimpleModeConfig,
RawValueType,
FieldNames,
} from '../interface';
import { convertChildrenToData } from '../utils/legacyUtil';
const MAX_WARNING_TIMES = 10;
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;
}
/**
* Format `treeData` with `value` & `key` which is used for calculation
*/
function formatTreeData(
treeData: DataNode[],
getLabelProp: (node: DataNode) => any,
fieldNames: FieldNames,
): InternalDataEntity[] {
let warningTimes = 0;
const valueSet = new Set<RawValueType>();
// Field names
const { value: fieldValue, children: fieldChildren } = fieldNames;
function dig(dataNodes: DataNode[]) {
return (dataNodes || []).map(node => {
const { key, disableCheckbox, disabled, checkable, selectable, isLeaf } = node;
const value = node[fieldValue];
const mergedValue = fieldValue in node ? value : key;
const dataNode: InternalDataEntity = {
disableCheckbox,
disabled,
key: key !== null && key !== undefined ? key : mergedValue,
value: mergedValue,
title: getLabelProp(node),
node,
selectable,
isLeaf,
dataRef: node,
checkable,
};
if (node.slots) {
dataNode.slots = node.slots;
}
// Check `key` & `value` and warning user
if (process.env.NODE_ENV !== 'production') {
if (
key !== null &&
key !== undefined &&
value !== undefined &&
String(key) !== String(value) &&
warningTimes < MAX_WARNING_TIMES
) {
warningTimes += 1;
warning(
false,
`\`key\` or \`value\` with TreeNode must be the same or you can remove one of them. key: ${key}, value: ${value}.`,
);
}
warning(!valueSet.has(value), `Same \`value\` exist in the tree: ${value}`);
valueSet.add(value);
}
if (fieldChildren in node) {
dataNode.children = dig(node[fieldChildren]);
}
return dataNode;
});
}
return dig(treeData);
}
/**
* 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<any[]>,
{
getLabelProp,
simpleMode,
fieldNames,
}: {
getLabelProp: (node: DataNode) => any;
simpleMode: Ref<boolean | SimpleModeConfig>;
fieldNames: Ref<FieldNames>;
},
): ComputedRef<InternalDataEntity[]> {
return computed(() => {
if (treeData.value) {
return formatTreeData(
simpleMode.value
? parseSimpleTreeData(treeData.value, {
id: 'id',
pId: 'pId',
rootPId: null,
...(simpleMode.value !== true ? simpleMode.value : {}),
})
: treeData.value,
getLabelProp,
fieldNames.value,
);
} else {
return formatTreeData(convertChildrenToData(children.value), getLabelProp, fieldNames.value);
}
});
}

View File

@ -1,4 +1,4 @@
// base rc-tree-select@5.0.0-alpha.4 // base rc-tree-select@4.6.1
import TreeSelect from './TreeSelect'; import TreeSelect from './TreeSelect';
import TreeNode from './TreeNode'; import TreeNode from './TreeNode';
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil'; import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';

View File

@ -1,15 +1,21 @@
import { filterEmpty } from '../../_util/props-util'; import { filterEmpty } from '../../_util/props-util';
import { camelize } from 'vue'; import { camelize } from 'vue';
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
import type { DataNode, ChangeEventExtra, RawValueType, LegacyCheckedNode } from '../interface'; import type {
DataNode,
LegacyDataNode,
ChangeEventExtra,
InternalDataEntity,
RawValueType,
LegacyCheckedNode,
} from '../interface';
import TreeNode from '../TreeNode'; import TreeNode from '../TreeNode';
import type { VueNode } from '../../_util/type'; import type { VueNode } from '../../_util/type';
import type { DefaultOptionType, FieldNames } from '../TreeSelect';
function isTreeSelectNode(node: any) { function isTreeSelectNode(node: any) {
return node && node.type && (node.type as any).isTreeSelectNode; return node && node.type && (node.type as any).isTreeSelectNode;
} }
export function convertChildrenToData(rootNodes: VueNode[]): DataNode[] { export function convertChildrenToData(rootNodes: VueNode): DataNode[] {
function dig(treeNodes: any[] = []): DataNode[] { function dig(treeNodes: any[] = []): DataNode[] {
return filterEmpty(treeNodes).map(treeNode => { return filterEmpty(treeNodes).map(treeNode => {
// Filter invalidate node // Filter invalidate node
@ -60,10 +66,10 @@ export function convertChildrenToData(rootNodes: VueNode[]): DataNode[] {
return dig(rootNodes as any[]); return dig(rootNodes as any[]);
} }
export function fillLegacyProps(dataNode: DataNode): any { export function fillLegacyProps(dataNode: DataNode): LegacyDataNode {
// Skip if not dataNode exist // Skip if not dataNode exist
if (!dataNode) { if (!dataNode) {
return dataNode; return dataNode as LegacyDataNode;
} }
const cloneNode = { ...dataNode }; const cloneNode = { ...dataNode };
@ -73,43 +79,37 @@ export function fillLegacyProps(dataNode: DataNode): any {
get() { get() {
warning( warning(
false, false,
'New `vc-tree-select` not support return node instance as argument anymore. Please consider to remove `props` access.', 'New `rc-tree-select` not support return node instance as argument anymore. Please consider to remove `props` access.',
); );
return cloneNode; return cloneNode;
}, },
}); });
} }
return cloneNode; return cloneNode as LegacyDataNode;
} }
export function fillAdditionalInfo( export function fillAdditionalInfo(
extra: ChangeEventExtra, extra: ChangeEventExtra,
triggerValue: RawValueType, triggerValue: RawValueType,
checkedValues: RawValueType[], checkedValues: RawValueType[],
treeData: DefaultOptionType[], treeData: InternalDataEntity[],
showPosition: boolean, showPosition: boolean,
fieldNames: FieldNames,
) { ) {
let triggerNode = null; let triggerNode = null;
let nodeList: LegacyCheckedNode[] = null; let nodeList: LegacyCheckedNode[] = null;
function generateMap() { function generateMap() {
function dig(list: DefaultOptionType[], level = '0', parentIncluded = false) { function dig(list: InternalDataEntity[], level = '0', parentIncluded = false) {
return list return list
.map((option, index) => { .map((dataNode, index) => {
const pos = `${level}-${index}`; const pos = `${level}-${index}`;
const value = option[fieldNames.value]; const included = checkedValues.includes(dataNode.value);
const included = checkedValues.includes(value); const children = dig(dataNode.children || [], pos, included);
const children = dig(option[fieldNames.children] || [], pos, included); const node = <TreeNode {...dataNode}>{children.map(child => child.node)}</TreeNode>;
const node = (
<TreeNode {...(option as Required<DefaultOptionType>)}>
{children.map(child => child.node)}
</TreeNode>
);
// Link with trigger node // Link with trigger node
if (triggerValue === value) { if (triggerValue === dataNode.value) {
triggerNode = node; triggerNode = node;
} }

View File

@ -1,6 +1,5 @@
import type { DataEntity } from '../../vc-tree/interface'; import type { DataEntity } from '../../vc-tree/interface';
import type { InternalFieldName } from '../TreeSelect'; import type { RawValueType, Key, DataNode } from '../interface';
import type { RawValueType, Key } from '../interface';
import { isCheckDisabled } from './valueUtil'; import { isCheckDisabled } from './valueUtil';
export const SHOW_ALL = 'SHOW_ALL'; export const SHOW_ALL = 'SHOW_ALL';
@ -9,23 +8,22 @@ export const SHOW_CHILD = 'SHOW_CHILD';
export type CheckedStrategy = typeof SHOW_ALL | typeof SHOW_PARENT | typeof SHOW_CHILD; export type CheckedStrategy = typeof SHOW_ALL | typeof SHOW_PARENT | typeof SHOW_CHILD;
export function formatStrategyValues( export function formatStrategyKeys(
values: Key[], keys: Key[],
strategy: CheckedStrategy, strategy: CheckedStrategy,
keyEntities: Record<Key, DataEntity>, keyEntities: Record<Key, DataEntity>,
fieldNames: InternalFieldName,
): RawValueType[] { ): RawValueType[] {
const valueSet = new Set(values); const keySet = new Set(keys);
if (strategy === SHOW_CHILD) { if (strategy === SHOW_CHILD) {
return values.filter(key => { return keys.filter(key => {
const entity = keyEntities[key]; const entity = keyEntities[key];
if ( if (
entity && entity &&
entity.children && entity.children &&
entity.children.every( entity.children.every(
({ node }) => isCheckDisabled(node) || valueSet.has(node[fieldNames.value]), ({ node }) => isCheckDisabled(node) || keySet.has((node as DataNode).key),
) )
) { ) {
return false; return false;
@ -34,15 +32,15 @@ export function formatStrategyValues(
}); });
} }
if (strategy === SHOW_PARENT) { if (strategy === SHOW_PARENT) {
return values.filter(key => { return keys.filter(key => {
const entity = keyEntities[key]; const entity = keyEntities[key];
const parent = entity ? entity.parent : null; const parent = entity ? entity.parent : null;
if (parent && !isCheckDisabled(parent.node) && valueSet.has(parent.node.key)) { if (parent && !isCheckDisabled(parent.node) && keySet.has((parent.node as DataNode).key)) {
return false; return false;
} }
return true; return true;
}); });
} }
return values; return keys;
} }

View File

@ -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);
}

View File

@ -1,8 +1,7 @@
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
import type { TreeSelectProps } from '../TreeSelect';
import { toArray } from './valueUtil'; import { toArray } from './valueUtil';
function warningProps(props: TreeSelectProps & { searchPlaceholder?: string }) { function warningProps(props: any) {
const { searchPlaceholder, treeCheckStrictly, treeCheckable, labelInValue, value, multiple } = const { searchPlaceholder, treeCheckStrictly, treeCheckable, labelInValue, value, multiple } =
props; props;

View File

@ -1,210 +0,0 @@
import OptionList from './OptionList';
import TreeNode from './TreeNode';
import { formatStrategyValues, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil';
import type { CheckedStrategy } from './utils/strategyUtil';
import TreeSelectContext from './TreeSelectContext';
import type { TreeSelectContextProps } from './TreeSelectContext';
import LegacyContext from './LegacyContext';
import useTreeData from './hooks/useTreeData';
import { toArray, fillFieldNames, isNil } from './utils/valueUtil';
import useCache from './hooks/useCache';
import useRefFunc from './hooks/useRefFunc';
import useDataEntities from './hooks/useDataEntities';
import { fillAdditionalInfo, fillLegacyProps } from './utils/legacyUtil';
import useCheckedKeys from './hooks/useCheckedKeys';
import useFilterTreeData from './hooks/useFilterTreeData';
import warningProps from './utils/warningPropsUtil';
import type { Key } from './interface';
import { baseSelectPropsWithoutPrivate } from '../vc-select/BaseSelect';
import { defineComponent } from 'vue';
import type { ExtractPropTypes, PropType } from 'vue';
import omit from '../_util/omit';
import PropTypes from '../_util/vue-types';
import type { SelectProps } from '../vc-select';
export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
export type RawValueType = string | number;
export interface LabeledValueType {
key?: Key;
value?: RawValueType;
label?: any;
/** Only works on `treeCheckStrictly` */
halfChecked?: boolean;
}
export type SelectSource = 'option' | 'selection' | 'input' | 'clear';
export type DraftValueType = RawValueType | LabeledValueType | (RawValueType | LabeledValueType)[];
/** @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: LabeledValueType[];
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;
}
export interface InternalFieldName extends Omit<FieldNames, 'label'> {
_title: string[];
}
export interface SimpleModeConfig {
id?: Key;
pId?: Key;
rootPId?: Key;
}
export interface BaseOptionType {
disabled?: boolean;
checkable?: boolean;
disableCheckbox?: boolean;
children?: BaseOptionType[];
[name: string]: any;
}
export interface DefaultOptionType extends BaseOptionType {
value?: RawValueType;
title?: any;
label?: any;
key?: Key;
children?: DefaultOptionType[];
}
export interface LegacyDataNode extends DefaultOptionType {
props: any;
}
export function treeSelectProps<
ValueType = any,
OptionType extends BaseOptionType = DefaultOptionType,
>() {
return {
...omit(baseSelectPropsWithoutPrivate(), ['mode']),
prefixCls: String,
id: String,
value: { type: [String, Number, Object, Array] as PropType<ValueType> },
defaultValue: { type: [String, Number, Object, Array] as PropType<ValueType> },
onChange: {
type: Function as PropType<
(value: ValueType, labelList: any[], extra: ChangeEventExtra) => void
>,
},
searchValue: String,
/** @deprecated Use `searchValue` instead */
inputValue: String,
onSearch: { type: Function as PropType<(value: string) => void> },
autoClearSearchValue: { type: Boolean, default: undefined },
filterTreeNode: {
type: [Boolean, Function] as PropType<
boolean | ((inputValue: string, treeNode: DefaultOptionType) => boolean)
>,
default: undefined,
},
treeNodeFilterProp: String,
// >>> Select
onSelect: Function as PropType<SelectProps['onSelect']>,
onDeselect: Function as PropType<SelectProps['onDeselect']>,
showCheckedStrategy: { type: String as PropType<CheckedStrategy> },
treeNodeLabelProp: String,
fieldNames: { type: Object as PropType<FieldNames> },
// >>> Mode
multiple: { type: Boolean, default: undefined },
treeCheckable: { type: Boolean, default: undefined },
treeCheckStrictly: { type: Boolean, default: undefined },
labelInValue: { type: Boolean, default: undefined },
// >>> Data
treeData: { type: Array as PropType<OptionType[]> },
treeDataSimpleMode: {
type: [Boolean, Object] as PropType<boolean | SimpleModeConfig>,
default: undefined,
},
treeLoadedKeys: { type: Array as PropType<Key[]> },
onTreeLoad: { type: Function as PropType<(loadedKeys: Key[]) => void> },
// >>> Options
virtual: { type: Boolean, default: undefined },
listHeight: Number,
listItemHeight: Number,
onDropdownVisibleChange: { type: Function as PropType<(open: boolean) => void> },
// >>> Tree
treeLine: { type: Boolean, default: undefined },
treeIcon: PropTypes.any,
showTreeIcon: { type: Boolean, default: undefined },
switcherIcon: PropTypes.any,
treeMotion: PropTypes.any,
// showArrow: { type: Boolean, default: undefined },
// showSearch: { type: Boolean, default: undefined },
// open: { type: Boolean, default: undefined },
// defaultOpen: { type: Boolean, default: undefined },
// disabled: { type: Boolean, default: undefined },
// placeholder: PropTypes.any,
// maxTagPlaceholder: { type: Function as PropType<(omittedValues: LabelValueType[]) => any> },
// loadData: { type: Function as PropType<(dataNode: LegacyDataNode) => Promise<unknown>> },
// treeExpandedKeys: { type: Array as PropType<Key[]> },
// treeDefaultExpandedKeys: { type: Array as PropType<Key[]> },
// treeDefaultExpandAll: { type: Boolean, default: undefined },
// children: Array,
// dropdownPopupAlign: PropTypes.any,
// // Event
// onTreeExpand: { type: Function as PropType<(expandedKeys: Key[]) => void> },
};
}
export type TreeSelectProps = Partial<ExtractPropTypes<ReturnType<typeof treeSelectProps>>>;
function isRawValue(value: RawValueType | LabeledValueType): value is RawValueType {
return !value || typeof value !== 'object';
}
export default defineComponent({
name: 'TreeSelect',
inheritAttrs: false,
props: treeSelectProps(),
setup() {
return () => {
return null;
};
},
});

View File

@ -1,514 +0,0 @@
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>
);
};
},
});
}

View File

@ -1,25 +0,0 @@
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];
}

View File

@ -1,58 +0,0 @@
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];
}

View File

@ -1,67 +0,0 @@
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;
}

View File

@ -1,67 +0,0 @@
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);
});
}

View File

@ -1,140 +0,0 @@
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']>
>;

View File

@ -1,50 +0,0 @@
import type { Key, DataNode, FieldNames } from '../interface';
import type { DefaultOptionType, InternalFieldName } from '../TreeSelect';
export function toArray<T>(value: T | T[]): T[] {
if (Array.isArray(value)) {
return value;
}
return value !== undefined ? [value] : [];
}
export function fillFieldNames(fieldNames?: FieldNames) {
const { label, value, children } = fieldNames || {};
const mergedValue = value || 'value';
return {
_title: label ? [label] : ['title', 'label'],
value: mergedValue,
key: mergedValue,
children: children || 'children',
};
}
export function isCheckDisabled(node: DataNode) {
return node.disabled || node.disableCheckbox || node.checkable === false;
}
/** Loop fetch all the keys exist in the tree */
export function getAllKeys(treeData: DefaultOptionType[], fieldNames: InternalFieldName) {
const keys: Key[] = [];
function dig(list: DefaultOptionType[]) {
list.forEach(item => {
keys.push(item[fieldNames.value]);
const children = item[fieldNames.children];
if (children) {
dig(children);
}
});
}
dig(treeData);
return keys;
}
export function isNil(val: any) {
return val === null || val === undefined;
}