refactor: tree-select

refactor-cascader
tangjinzhou 2022-01-13 21:15:28 +08:00
parent 6982052e74
commit 314461848c
9 changed files with 399 additions and 308 deletions

View File

@ -0,0 +1,63 @@
/**
* BaseSelect provide some parsed data into context.
* You can use this hooks to get them.
*/
import type { InjectionKey } from 'vue';
import { inject, provide } from 'vue';
import type { DataEntity, IconType } from '../vc-tree/interface';
import type { Key, LegacyDataNode, RawValueType } from './interface';
interface LegacyContextProps {
checkable: boolean;
checkedKeys: Key[];
customCheckable: () => any;
halfCheckedKeys: Key[];
treeExpandedKeys: Key[];
treeDefaultExpandedKeys: Key[];
onTreeExpand: (keys: Key[]) => void;
treeDefaultExpandAll: boolean;
treeIcon: IconType;
showTreeIcon: boolean;
switcherIcon: IconType;
treeLine: boolean;
treeNodeFilterProp: string;
treeLoadedKeys: Key[];
treeMotion: any;
loadData: (treeNode: LegacyDataNode) => Promise<unknown>;
onTreeLoad: (loadedKeys: Key[]) => void;
keyEntities: Record<RawValueType, DataEntity<any>>;
// slots: {
// title?: (data: InternalDataEntity) => any;
// titleRender?: (data: InternalDataEntity) => any;
// [key: string]: ((...args: any[]) => any) | undefined;
// };
}
const TreeSelectLegacyContextPropsKey: InjectionKey<LegacyContextProps> = Symbol(
'TreeSelectLegacyContextPropsKey',
);
// export const LegacySelectContext = defineComponent({
// name: 'SelectContext',
// props: {
// value: { type: Object as PropType<LegacyContextProps> },
// },
// setup(props, { slots }) {
// provide(
// TreeSelectLegacyContextPropsKey,
// computed(() => props.value),
// );
// return () => slots.default?.();
// },
// });
export function useProvideLegacySelectContext(props: LegacyContextProps) {
return provide(TreeSelectLegacyContextPropsKey, props);
}
export default function useInjectLegacySelectContext() {
return inject(TreeSelectLegacyContextPropsKey, {} as LegacyContextProps);
}

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}
@ -217,10 +209,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 +220,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 +235,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 +245,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.customCheckable }}
/> />
</div> </div>
); );

View File

@ -1,6 +1,210 @@
import generate from './generate';
import OptionList from './OptionList'; 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';
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,
},
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

@ -0,0 +1,23 @@
import type { InjectionKey } from 'vue';
import { provide, inject } from 'vue';
import type { DefaultOptionType, InternalFieldName, OnInternalSelect } from './TreeSelect';
export interface TreeSelectContextProps {
virtual?: boolean;
listHeight: number;
listItemHeight: number;
treeData: DefaultOptionType[];
fieldNames: InternalFieldName;
onSelect: OnInternalSelect;
}
const TreeSelectContextPropsKey: InjectionKey<TreeSelectContextProps> = Symbol(
'TreeSelectContextPropsKey',
);
export function useProvideSelectContext(props: TreeSelectContextProps) {
return provide(TreeSelectContextPropsKey, props);
}
export default function useInjectSelectContext() {
return inject(TreeSelectContextPropsKey, {} as TreeSelectContextProps);
}

View File

@ -1,4 +1,4 @@
// base rc-tree-select@4.6.1 // base rc-tree-select@5.0.0-alpha.4
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,16 +1,10 @@
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;
@ -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,15 @@ 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) && keySet.has((parent.node as DataNode).key)) { if (parent && !isCheckDisabled(parent.node) && valueSet.has(parent.node.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); dig(treeData);
const cacheMap = new Map<Key, FlattenDataNode>(); return keys;
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) { export function isNil(val: any) {
return (searchValue: string, dataNode: LegacyDataNode) => { return val === null || val === undefined;
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,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;