From 4e8a16f2bb8a216361b453da290d8489544a3fff Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sat, 15 Jan 2022 22:44:47 +0800 Subject: [PATCH] refactor: tree-select --- components/tree-select/index.tsx | 67 +- components/tree-select/style/index.less | 4 +- components/tree-select/utils.tsx | 34 - components/vc-select/BaseSelect.tsx | 14 +- .../vc-select/Selector/SingleSelector.tsx | 17 +- .../LegacyContext.tsx | 14 +- components/vc-tree-select/OptionList.tsx | 116 ++- components/vc-tree-select/TreeSelect.tsx | 734 +++++++++++++++++- .../TreeSelectContext.ts | 0 .../hooks/useCache.ts | 0 .../hooks/useCheckedKeys.ts | 22 +- .../hooks/useDataEntities.ts | 19 +- .../hooks/useFilterTreeData.ts | 0 .../vc-tree-select/hooks/useTreeData.ts | 122 +-- components/vc-tree-select/index.tsx | 7 +- .../vc-tree-select/utils/legacyUtil.tsx | 40 +- .../vc-tree-select/utils/strategyUtil.ts | 21 +- components/vc-tree-select/utils/valueUtil.ts | 236 +----- .../vc-tree-select/utils/warningPropsUtil.ts | 3 +- .../Context.tsx | 0 .../OptionList.tsx | 112 +-- .../TreeNode.tsx | 0 components/vc-tree-select1/TreeSelect.tsx | 6 + .../generate.tsx | 0 .../hooks/useKeyValueMap.ts | 0 .../hooks/useKeyValueMapping.ts | 0 .../hooks/useSelectValues.ts | 0 .../vc-tree-select1/hooks/useTreeData.ts | 155 ++++ .../index.tsx | 2 +- .../interface.ts | 0 .../props.ts | 0 .../utils/legacyUtil.tsx | 40 +- .../utils/strategyUtil.ts | 20 +- components/vc-tree-select1/utils/valueUtil.ts | 244 ++++++ .../utils/warningPropsUtil.ts | 3 +- components/vc-tree-select2/TreeSelect.tsx | 210 ----- components/vc-tree-select2/generate.tsx | 514 ------------ .../vc-tree-select2/hooks/useKeyValueMap.ts | 25 - .../hooks/useKeyValueMapping.ts | 58 -- .../vc-tree-select2/hooks/useSelectValues.ts | 67 -- .../vc-tree-select2/hooks/useTreeData.ts | 67 -- components/vc-tree-select2/props.ts | 140 ---- components/vc-tree-select2/utils/valueUtil.ts | 50 -- 43 files changed, 1451 insertions(+), 1732 deletions(-) delete mode 100644 components/tree-select/utils.tsx rename components/{vc-tree-select2 => vc-tree-select}/LegacyContext.tsx (84%) rename components/{vc-tree-select2 => vc-tree-select}/TreeSelectContext.ts (100%) rename components/{vc-tree-select2 => vc-tree-select}/hooks/useCache.ts (100%) rename components/{vc-tree-select2 => vc-tree-select}/hooks/useCheckedKeys.ts (66%) rename components/{vc-tree-select2 => vc-tree-select}/hooks/useDataEntities.ts (78%) rename components/{vc-tree-select2 => vc-tree-select}/hooks/useFilterTreeData.ts (100%) rename components/{vc-tree-select2 => vc-tree-select1}/Context.tsx (100%) rename components/{vc-tree-select2 => vc-tree-select1}/OptionList.tsx (65%) rename components/{vc-tree-select2 => vc-tree-select1}/TreeNode.tsx (100%) create mode 100644 components/vc-tree-select1/TreeSelect.tsx rename components/{vc-tree-select => vc-tree-select1}/generate.tsx (100%) rename components/{vc-tree-select => vc-tree-select1}/hooks/useKeyValueMap.ts (100%) rename components/{vc-tree-select => vc-tree-select1}/hooks/useKeyValueMapping.ts (100%) rename components/{vc-tree-select => vc-tree-select1}/hooks/useSelectValues.ts (100%) create mode 100644 components/vc-tree-select1/hooks/useTreeData.ts rename components/{vc-tree-select2 => vc-tree-select1}/index.tsx (90%) rename components/{vc-tree-select2 => vc-tree-select1}/interface.ts (100%) rename components/{vc-tree-select => vc-tree-select1}/props.ts (100%) rename components/{vc-tree-select2 => vc-tree-select1}/utils/legacyUtil.tsx (80%) rename components/{vc-tree-select2 => vc-tree-select1}/utils/strategyUtil.ts (62%) create mode 100644 components/vc-tree-select1/utils/valueUtil.ts rename components/{vc-tree-select2 => vc-tree-select1}/utils/warningPropsUtil.ts (89%) delete mode 100644 components/vc-tree-select2/TreeSelect.tsx delete mode 100644 components/vc-tree-select2/generate.tsx delete mode 100644 components/vc-tree-select2/hooks/useKeyValueMap.ts delete mode 100644 components/vc-tree-select2/hooks/useKeyValueMapping.ts delete mode 100644 components/vc-tree-select2/hooks/useSelectValues.ts delete mode 100644 components/vc-tree-select2/hooks/useTreeData.ts delete mode 100644 components/vc-tree-select2/props.ts delete mode 100644 components/vc-tree-select2/utils/valueUtil.ts diff --git a/components/tree-select/index.tsx b/components/tree-select/index.tsx index 3cd8f5999..f9604b546 100644 --- a/components/tree-select/index.tsx +++ b/components/tree-select/index.tsx @@ -10,7 +10,7 @@ import VcTreeSelect, { import classNames from '../_util/classNames'; import initDefaultProps from '../_util/props-util/initDefaultProps'; 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 PropTypes from '../_util/vue-types'; import useConfigInject from '../_util/hooks/useConfigInject'; @@ -21,6 +21,9 @@ import type { AntTreeNodeProps } from '../tree/Tree'; import { warning } from '../vc-util/warning'; import { flattenChildren } from '../_util/props-util'; 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) => { if (transitionName !== undefined) { @@ -34,29 +37,43 @@ type RawValue = string | number; export interface LabeledValue { key?: string; value: RawValue; - label: any; + label?: any; } export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[]; -export interface RefTreeSelectProps { - focus: () => void; - blur: () => void; +export type RefTreeSelectProps = BaseSelectRef; + +export function treeSelectProps< + ValueType = any, + OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType, +>() { + return { + ...omit(vcTreeSelectProps(), [ + 'showTreeIcon', + 'treeMotion', + 'inputIcon', + 'getInputElement', + 'treeLine', + 'customSlots', + ]), + suffixIcon: PropTypes.any, + size: { type: String as PropType }, + bordered: { type: Boolean, default: undefined }, + treeLine: { type: [Boolean, Object] as PropType, default: undefined }, + replaceFields: { type: Object as PropType }, + 'onUpdate:value': { type: Function as PropType<(value: any) => void> }, + 'onUpdate:treeExpandedKeys': { type: Function as PropType<(keys: Key[]) => void> }, + 'onUpdate:searchValue': { type: Function as PropType<(value: string) => void> }, + }; } -export const treeSelectProps = { - ...omit(vcTreeSelectProps(), ['showTreeIcon', 'treeMotion', 'inputIcon']), - suffixIcon: PropTypes.any, - size: { type: String as PropType }, - bordered: { type: Boolean, default: undefined }, - replaceFields: { type: Object as PropType }, -}; -export type TreeSelectProps = Partial>; +export type TreeSelectProps = Partial>>; const TreeSelect = defineComponent({ TreeNode, name: 'ATreeSelect', inheritAttrs: false, - props: initDefaultProps(treeSelectProps, { + props: initDefaultProps(treeSelectProps(), { choiceTransitionName: '', listHeight: 256, 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('change', ...args); formItemContext.onFieldChange(); }; - const handleTreeExpand = (...args: any[]) => { - emit('update:treeExpandedKeys', args[0]); - emit('treeExpand', ...args); + const handleTreeExpand: TreeSelectProps['onTreeExpand'] = (keys: Key[]) => { + emit('update:treeExpandedKeys', keys); + emit('treeExpand', keys); }; - const handleSearch = (...args: any[]) => { - emit('update:searchValue', args[0]); - emit('search', ...args); + const handleSearch: TreeSelectProps['onSearch'] = (value: string) => { + emit('update:searchValue', value); + emit('search', value); }; const handleBlur = () => { emit('blur'); @@ -190,6 +207,10 @@ const TreeSelect = defineComponent({ 'removeIcon', 'clearIcon', 'switcherIcon', + 'bordered', + 'onUpdate:value', + 'onUpdate:treeExpandedKeys', + 'onUpdate:searchValue', ]); const mergedClassName = classNames( @@ -242,6 +263,10 @@ const TreeSelect = defineComponent({ }} {...otherProps} transitionName={transitionName.value} + customSlots={{ + ...slots, + treeCheckable: () => , + }} /> ); }; diff --git a/components/tree-select/style/index.less b/components/tree-select/style/index.less index e6672b587..b2cf719f3 100644 --- a/components/tree-select/style/index.less +++ b/components/tree-select/style/index.less @@ -11,7 +11,7 @@ .@{tree-select-prefix-cls} { // ======================= Dropdown ======================= &-dropdown { - padding: @padding-xs (@padding-xs / 2) 0; + padding: @padding-xs (@padding-xs / 2); &-rtl { direction: rtl; @@ -24,8 +24,6 @@ align-items: stretch; .@{select-tree-prefix-cls}-treenode { - padding-bottom: @padding-xs; - .@{select-tree-prefix-cls}-node-content-wrapper { flex: auto; } diff --git a/components/tree-select/utils.tsx b/components/tree-select/utils.tsx deleted file mode 100644 index 0f16a65ef..000000000 --- a/components/tree-select/utils.tsx +++ /dev/null @@ -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); -} diff --git a/components/vc-select/BaseSelect.tsx b/components/vc-select/BaseSelect.tsx index c5452e530..0bcc79eb1 100644 --- a/components/vc-select/BaseSelect.tsx +++ b/components/vc-select/BaseSelect.tsx @@ -35,9 +35,9 @@ import isMobile from '../vc-util/isMobile'; import KeyCode from '../_util/KeyCode'; import { toReactive } from '../_util/toReactive'; import classNames from '../_util/classNames'; -import OptionList from './OptionList'; import createRef from '../_util/createRef'; import type { BaseOptionType } from './Select'; +import useInjectLegacySelectContext from '../vc-tree-select/LegacyContext'; const DEFAULT_OMIT_PROPS = [ 'value', @@ -269,7 +269,7 @@ export default defineComponent({ onMounted(() => { mobile.value = isMobile(); }); - + const legacyTreeSelectContext = useInjectLegacySelectContext(); // ============================== Refs ============================== const containerRef = ref(null); const selectorDomRef = createRef(); @@ -673,6 +673,7 @@ export default defineComponent({ emptyOptions, activeDescendantId, activeValue, + OptionList, ...restProps } = { ...props, ...attrs } as BaseSelectProps; @@ -752,7 +753,12 @@ export default defineComponent({ } // =========================== OptionList =========================== - const optionList = ; + const optionList = ( + + ); // ============================= Select ============================= const mergedClassName = classNames(prefixCls, attrs.class, { @@ -840,7 +846,7 @@ export default defineComponent({ // onFocus={onContainerFocus} // onBlur={onContainerBlur} > - {mockFocused && !mergedOpen.value && ( + {mockFocused.value && !mergedOpen.value && ( ({ } return inputValue; }); - const treeSelectContext = useInjectTreeSelectContext(); + const legacyTreeSelectContext = useInjectLegacySelectContext(); watch( [combobox, () => props.activeValue], () => { @@ -108,13 +108,16 @@ const SingleSelector = defineComponent({ const item = values[0]; let titleNode = null; // 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 = - treeSelectContext.value.slots[item?.option?.data?.slots?.title] || - treeSelectContext.value.slots.title || + legacyTreeSelectContext.customSlots[originData.slots?.title] || + legacyTreeSelectContext.customSlots.title || item.label; if (typeof titleNode === 'function') { - titleNode = titleNode(item.option?.data || {}); + titleNode = titleNode(originData); } // else if (treeSelectContext.value.slots.titleRender) { // // 因历史 title 是覆盖逻辑,新增 titleRender,所有的 title 都走一遍 titleRender @@ -155,7 +158,7 @@ const SingleSelector = defineComponent({ {/* Display value */} {!combobox.value && item && !hasTextInput.value && ( - {titleNode} + {titleNode} )} diff --git a/components/vc-tree-select2/LegacyContext.tsx b/components/vc-tree-select/LegacyContext.tsx similarity index 84% rename from components/vc-tree-select2/LegacyContext.tsx rename to components/vc-tree-select/LegacyContext.tsx index 9e13bf2f4..7ad3eb37e 100644 --- a/components/vc-tree-select2/LegacyContext.tsx +++ b/components/vc-tree-select/LegacyContext.tsx @@ -6,9 +6,9 @@ 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'; +import type { InternalDataEntity, Key, LegacyDataNode, RawValueType } from './interface'; -interface LegacyContextProps { +export interface LegacyContextProps { checkable: boolean; checkedKeys: Key[]; customCheckable: () => any; @@ -29,11 +29,11 @@ interface LegacyContextProps { keyEntities: Record>; - // slots: { - // title?: (data: InternalDataEntity) => any; - // titleRender?: (data: InternalDataEntity) => any; - // [key: string]: ((...args: any[]) => any) | undefined; - // }; + customSlots: { + title?: (data: InternalDataEntity) => any; + treeCheckable: () => any; + [key: string]: ((...args: any[]) => any) | undefined; + }; } const TreeSelectLegacyContextPropsKey: InjectionKey = Symbol( diff --git a/components/vc-tree-select/OptionList.tsx b/components/vc-tree-select/OptionList.tsx index 6d3ec67e4..d7238ba3f 100644 --- a/components/vc-tree-select/OptionList.tsx +++ b/components/vc-tree-select/OptionList.tsx @@ -1,14 +1,16 @@ -import type { DataNode, TreeDataNode, Key } from './interface'; -import { useInjectTreeSelectContext } from './Context'; +import type { TreeDataNode, Key } from './interface'; import type { RefOptionListProps } from '../vc-select/OptionList'; import type { ScrollTo } from '../vc-virtual-list/List'; import { computed, defineComponent, nextTick, ref, shallowRef, watch } from 'vue'; -import { optionListProps } from './props'; import useMemo from '../_util/hooks/useMemo'; import type { EventDataNode } from '../tree'; import KeyCode from '../_util/KeyCode'; import Tree from '../vc-tree/Tree'; import type { TreeProps } from '../vc-tree/props'; +import { getAllKeys, isCheckDisabled } from './utils/valueUtil'; +import { useBaseProps } from '../vc-select'; +import useInjectLegacySelectContext from './LegacyContext'; +import useInjectSelectContext from './TreeSelectContext'; const HIDDEN_STYLE = { width: 0, @@ -32,44 +34,36 @@ type ReviseRefOptionListProps = Omit & { scrollT export default defineComponent({ name: 'OptionList', inheritAttrs: false, - props: optionListProps(), slots: ['notFoundContent', 'menuItemSelectedIcon'], - setup(props, { slots, expose }) { - const context = useInjectTreeSelectContext(); - + setup(_, { slots, expose }) { + const baseProps = useBaseProps(); + const legacyContext = useInjectLegacySelectContext(); + const context = useInjectSelectContext(); const treeRef = ref(); - const memoOptions = useMemo( - () => props.options, - [() => props.open, () => props.options], + const memoTreeData = useMemo( + () => context.treeData, + [() => baseProps.open, () => context.treeData], next => next[0], ); - const valueKeys = computed(() => { - const { checkedKeys, getEntityByValue } = context.value; - return checkedKeys.map(val => { - const entity = getEntityByValue(val); - return entity ? entity.key : null; - }); - }); - const mergedCheckedKeys = computed(() => { - const { checkable, halfCheckedKeys } = context.value; + const { checkable, halfCheckedKeys, checkedKeys } = legacyContext; if (!checkable) { return null; } return { - checked: valueKeys.value, + checked: checkedKeys, halfChecked: halfCheckedKeys, }; }); watch( - () => props.open, + () => baseProps.open, () => { nextTick(() => { - if (props.open && !props.multiple && valueKeys.value.length) { - treeRef.value?.scrollTo({ key: valueKeys.value[0] }); + if (baseProps.open && !baseProps.multiple && legacyContext.checkedKeys.length) { + treeRef.value?.scrollTo({ key: legacyContext.checkedKeys[0] }); } }); }, @@ -77,25 +71,25 @@ export default defineComponent({ ); // ========================== Search ========================== - const lowerSearchValue = computed(() => String(props.searchValue).toLowerCase()); + const lowerSearchValue = computed(() => String(baseProps.searchValue).toLowerCase()); const filterTreeNode = (treeNode: EventDataNode) => { if (!lowerSearchValue.value) { return false; } - return String(treeNode[context.value.treeNodeFilterProp]) + return String(treeNode[legacyContext.treeNodeFilterProp]) .toLowerCase() .includes(lowerSearchValue.value); }; // =========================== Keys =========================== - const expandedKeys = shallowRef(context.value.treeDefaultExpandedKeys); + const expandedKeys = shallowRef(legacyContext.treeDefaultExpandedKeys); const searchExpandedKeys = shallowRef(null); watch( - () => props.searchValue, + () => baseProps.searchValue, () => { - if (props.searchValue) { - searchExpandedKeys.value = props.flattenOptions.map(o => o.key); + if (baseProps.searchValue) { + searchExpandedKeys.value = getAllKeys(context.treeData, context.fieldNames); } }, { @@ -103,17 +97,17 @@ export default defineComponent({ }, ); const mergedExpandedKeys = computed(() => { - if (context.value.treeExpandedKeys) { - return [...context.value.treeExpandedKeys]; + if (legacyContext.treeExpandedKeys) { + return [...legacyContext.treeExpandedKeys]; } - return props.searchValue ? searchExpandedKeys.value : expandedKeys.value; + return baseProps.searchValue ? searchExpandedKeys.value : expandedKeys.value; }); const onInternalExpand = (keys: Key[]) => { expandedKeys.value = keys; searchExpandedKeys.value = keys; - context.value.onTreeExpand?.(keys); + legacyContext.onTreeExpand?.(keys); }; // ========================== Events ========================== @@ -121,23 +115,23 @@ export default defineComponent({ event.preventDefault(); }; - const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => { - const { getEntityByKey, checkable, checkedKeys } = context.value; - const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select'); - if (entity !== null) { - props.onSelect?.(entity.data.value, { - selected: !checkedKeys.includes(entity.data.value), - }); + const onInternalSelect = (_: Key[], { node }: TreeEventInfo) => { + const { checkable, checkedKeys } = legacyContext; + if (checkable && isCheckDisabled(node)) { + return; } + context.onSelect?.(node.key, { + selected: !checkedKeys.includes(node.key), + }); - if (!props.multiple) { - props.onToggleOpen?.(false); + if (!baseProps.multiple) { + baseProps.toggleOpen?.(false); } }; // ========================= Keyboard ========================= const activeKey = ref(null); - const activeEntity = computed(() => context.value.getEntityByKey(activeKey.value)); + const activeEntity = computed(() => legacyContext.keyEntities[activeKey.value]); const setActiveKey = (key: Key) => { activeKey.value = key; @@ -157,11 +151,11 @@ export default defineComponent({ // >>> Select item case KeyCode.ENTER: { - const { selectable, value } = activeEntity.value?.data.node || {}; + const { selectable, value } = activeEntity.value?.node || {}; if (selectable !== false) { onInternalSelect(null, { node: { key: activeKey.value }, - selected: !context.value.checkedKeys.includes(value), + selected: !legacyContext.checkedKeys.includes(value), }); } break; @@ -169,7 +163,7 @@ export default defineComponent({ // >>> Close case KeyCode.ESC: { - props.onToggleOpen(false); + baseProps.toggleOpen(false); } } }, @@ -179,15 +173,12 @@ export default defineComponent({ return () => { const { prefixCls, - height, - itemHeight, - virtual, multiple, searchValue, open, notFoundContent = slots.notFoundContent?.(), - onMouseenter, - } = props; + } = baseProps; + const { listHeight, listItemHeight, virtual } = context; const { checkable, treeDefaultExpandAll, @@ -199,9 +190,10 @@ export default defineComponent({ treeLoadedKeys, treeMotion, onTreeLoad, - } = context.value; + checkedKeys, + } = legacyContext; // ========================== Render ========================== - if (memoOptions.value.length === 0) { + if (memoTreeData.value.length === 0) { return (
{notFoundContent} @@ -209,7 +201,9 @@ export default defineComponent({ ); } - const treeProps: Partial = {}; + const treeProps: Partial = { + fieldNames: context.fieldNames, + }; if (treeLoadedKeys) { treeProps.loadedKeys = treeLoadedKeys; } @@ -217,10 +211,10 @@ export default defineComponent({ treeProps.expandedKeys = mergedExpandedKeys.value; } return ( -
+
{activeEntity.value && open && ( - {activeEntity.value.data.value} + {activeEntity.value.node.value} )} @@ -228,9 +222,9 @@ export default defineComponent({ ref={treeRef} focusable={false} prefixCls={`${prefixCls}-tree`} - treeData={memoOptions.value as TreeDataNode[]} - height={height} - itemHeight={itemHeight} + treeData={memoTreeData.value as TreeDataNode[]} + height={listHeight} + itemHeight={listItemHeight} virtual={virtual} multiple={multiple} icon={treeIcon} @@ -243,7 +237,7 @@ export default defineComponent({ checkable={checkable} checkStrictly checkedKeys={mergedCheckedKeys.value} - selectedKeys={!checkable ? valueKeys.value : []} + selectedKeys={!checkable ? checkedKeys : []} defaultExpandAll={treeDefaultExpandAll} {...treeProps} // Proxy event out @@ -253,7 +247,7 @@ export default defineComponent({ onExpand={onInternalExpand} onLoad={onTreeLoad} filterTreeNode={filterTreeNode} - v-slots={{ ...slots, checkable: context.value.customCheckable }} + v-slots={{ ...slots, checkable: legacyContext.customSlots.treeCheckable }} />
); diff --git a/components/vc-tree-select/TreeSelect.tsx b/components/vc-tree-select/TreeSelect.tsx index 1f2d1684c..6612ab7d2 100644 --- a/components/vc-tree-select/TreeSelect.tsx +++ b/components/vc-tree-select/TreeSelect.tsx @@ -1,6 +1,734 @@ -import generate from './generate'; 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 { + _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 }, + defaultValue: { type: [String, Number, Object, Array] as PropType }, + 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, + onDeselect: Function as PropType, + + showCheckedStrategy: { type: String as PropType }, + treeNodeLabelProp: String, + + fieldNames: { type: Object as PropType }, + + // >>> 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 }, + treeDataSimpleMode: { + type: [Boolean, Object] as PropType, + default: undefined, + }, + loadData: { type: Function as PropType<(dataNode: LegacyDataNode) => Promise> }, + treeLoadedKeys: { type: Array as PropType }, + onTreeLoad: { type: Function as PropType<(loadedKeys: Key[]) => void> }, + + // >>> Expanded + treeDefaultExpandAll: { type: Boolean, default: undefined }, + treeExpandedKeys: { type: Array as PropType }, + treeDefaultExpandedKeys: { type: Array as PropType }, + 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, + + 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>>; + +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(() => 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(); + 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 ( + >> 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} + /> + ); + }; + }, +}); diff --git a/components/vc-tree-select2/TreeSelectContext.ts b/components/vc-tree-select/TreeSelectContext.ts similarity index 100% rename from components/vc-tree-select2/TreeSelectContext.ts rename to components/vc-tree-select/TreeSelectContext.ts diff --git a/components/vc-tree-select2/hooks/useCache.ts b/components/vc-tree-select/hooks/useCache.ts similarity index 100% rename from components/vc-tree-select2/hooks/useCache.ts rename to components/vc-tree-select/hooks/useCache.ts diff --git a/components/vc-tree-select2/hooks/useCheckedKeys.ts b/components/vc-tree-select/hooks/useCheckedKeys.ts similarity index 66% rename from components/vc-tree-select2/hooks/useCheckedKeys.ts rename to components/vc-tree-select/hooks/useCheckedKeys.ts index 1731c42c3..ad1eace6a 100644 --- a/components/vc-tree-select2/hooks/useCheckedKeys.ts +++ b/components/vc-tree-select/hooks/useCheckedKeys.ts @@ -3,28 +3,28 @@ import type { DataEntity } from '../../vc-tree/interface'; import { conductCheck } from '../../vc-tree/utils/conductUtil'; import type { LabeledValueType, RawValueType } from '../TreeSelect'; import type { Ref } from 'vue'; -import { computed } from 'vue'; +import { shallowRef, watchEffect } from 'vue'; export default ( rawLabeledValues: Ref, rawHalfCheckedValues: Ref, treeConduction: Ref, keyEntities: Ref>, -) => - computed(() => { +) => { + const newRawCheckedValues = shallowRef([]); + const newRawHalfCheckedValues = shallowRef([]); + + watchEffect(() => { let checkedKeys: RawValueType[] = rawLabeledValues.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) { ({ checkedKeys, halfCheckedKeys } = conductCheck(checkedKeys, true, keyEntities.value)); } - - return [ - // Checked keys should fill with missing keys which should de-duplicated - Array.from(new Set([...missingValues, ...checkedKeys])), - // Half checked keys - halfCheckedKeys, - ]; + newRawCheckedValues.value = Array.from(new Set([...missingValues, ...checkedKeys])); + newRawHalfCheckedValues.value = halfCheckedKeys; }); + return [newRawCheckedValues, newRawHalfCheckedValues]; +}; diff --git a/components/vc-tree-select2/hooks/useDataEntities.ts b/components/vc-tree-select/hooks/useDataEntities.ts similarity index 78% rename from components/vc-tree-select2/hooks/useDataEntities.ts rename to components/vc-tree-select/hooks/useDataEntities.ts index 9c7152ae9..6756a9fb5 100644 --- a/components/vc-tree-select2/hooks/useDataEntities.ts +++ b/components/vc-tree-select/hooks/useDataEntities.ts @@ -4,14 +4,13 @@ import type { FieldNames, RawValueType } from '../TreeSelect'; import { isNil } from '../utils/valueUtil'; import type { Ref } from 'vue'; -import { computed } from 'vue'; +import { ref, watchEffect } from 'vue'; import { warning } from '../../vc-util/warning'; -export default (treeData: Ref, fieldNames: Ref) => - computed<{ - valueEntities: Map; - keyEntities: Record; - }>(() => { +export default (treeData: Ref, fieldNames: Ref) => { + const valueEntities = ref>(new Map()); + const keyEntities = ref>({}); + watchEffect(() => { const collection = convertDataToEntities(treeData.value, { fieldNames: fieldNames.value, initWrapper: wrapper => ({ @@ -34,7 +33,9 @@ export default (treeData: Ref, fieldNames: Ref) => } wrapper.valueEntities.set(val, entity); }, - }); - - return collection as any; + }) as any; + valueEntities.value = collection.valueEntities; + keyEntities.value = collection.keyEntities; }); + return { valueEntities, keyEntities }; +}; diff --git a/components/vc-tree-select2/hooks/useFilterTreeData.ts b/components/vc-tree-select/hooks/useFilterTreeData.ts similarity index 100% rename from components/vc-tree-select2/hooks/useFilterTreeData.ts rename to components/vc-tree-select/hooks/useFilterTreeData.ts diff --git a/components/vc-tree-select/hooks/useTreeData.ts b/components/vc-tree-select/hooks/useTreeData.ts index a274f3173..6f9eee41e 100644 --- a/components/vc-tree-select/hooks/useTreeData.ts +++ b/components/vc-tree-select/hooks/useTreeData.ts @@ -1,16 +1,9 @@ -import { warning } from '../../vc-util/warning'; -import type { ComputedRef, Ref } from 'vue'; +import type { Ref } from 'vue'; import { computed } from 'vue'; -import type { - DataNode, - InternalDataEntity, - SimpleModeConfig, - RawValueType, - FieldNames, -} from '../interface'; +import type { DataNode, SimpleModeConfig } from '../interface'; import { convertChildrenToData } from '../utils/legacyUtil'; - -const MAX_WARNING_TIMES = 10; +import type { DefaultOptionType } from '../TreeSelect'; +import type { VueNode } from 'ant-design-vue/es/_util/type'; function parseSimpleTreeData( treeData: DataNode[], @@ -48,108 +41,27 @@ function parseSimpleTreeData( 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(); - - // 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, - children: Ref, - { - getLabelProp, - simpleMode, - fieldNames, - }: { - getLabelProp: (node: DataNode) => any; - simpleMode: Ref; - fieldNames: Ref; - }, -): ComputedRef { + children: Ref, + simpleMode: Ref, +): Ref { 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); + return simpleMode.value + ? parseSimpleTreeData(treeData.value, { + id: 'id', + pId: 'pId', + rootPId: null, + ...(simpleMode.value !== true ? simpleMode.value : {}), + }) + : treeData.value; } + + return convertChildrenToData(children.value); }); } diff --git a/components/vc-tree-select/index.tsx b/components/vc-tree-select/index.tsx index d3e52682a..97d659e85 100644 --- a/components/vc-tree-select/index.tsx +++ b/components/vc-tree-select/index.tsx @@ -1,9 +1,8 @@ -// base rc-tree-select@4.6.1 -import TreeSelect from './TreeSelect'; +// base rc-tree-select@5.0.0-alpha.4 +import type { TreeSelectProps } from './TreeSelect'; +import TreeSelect, { treeSelectProps } from './TreeSelect'; import TreeNode from './TreeNode'; import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil'; -import type { TreeSelectProps } from './props'; -import { treeSelectProps } from './props'; export { TreeNode, SHOW_ALL, SHOW_CHILD, SHOW_PARENT, treeSelectProps }; export type { TreeSelectProps }; diff --git a/components/vc-tree-select/utils/legacyUtil.tsx b/components/vc-tree-select/utils/legacyUtil.tsx index 8c2e358f3..621898bb8 100644 --- a/components/vc-tree-select/utils/legacyUtil.tsx +++ b/components/vc-tree-select/utils/legacyUtil.tsx @@ -1,21 +1,15 @@ import { filterEmpty } from '../../_util/props-util'; import { camelize } from 'vue'; import { warning } from '../../vc-util/warning'; -import type { - DataNode, - LegacyDataNode, - ChangeEventExtra, - InternalDataEntity, - RawValueType, - LegacyCheckedNode, -} from '../interface'; +import type { DataNode, ChangeEventExtra, RawValueType, LegacyCheckedNode } from '../interface'; import TreeNode from '../TreeNode'; import type { VueNode } from '../../_util/type'; +import type { DefaultOptionType, FieldNames } from '../TreeSelect'; function isTreeSelectNode(node: any) { 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[] { return filterEmpty(treeNodes).map(treeNode => { // Filter invalidate node @@ -66,10 +60,10 @@ export function convertChildrenToData(rootNodes: VueNode): DataNode[] { return dig(rootNodes as any[]); } -export function fillLegacyProps(dataNode: DataNode): LegacyDataNode { +export function fillLegacyProps(dataNode: DataNode): any { // Skip if not dataNode exist if (!dataNode) { - return dataNode as LegacyDataNode; + return dataNode; } const cloneNode = { ...dataNode }; @@ -79,37 +73,43 @@ export function fillLegacyProps(dataNode: DataNode): LegacyDataNode { get() { warning( 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 as LegacyDataNode; + return cloneNode; } export function fillAdditionalInfo( extra: ChangeEventExtra, triggerValue: RawValueType, checkedValues: RawValueType[], - treeData: InternalDataEntity[], + treeData: DefaultOptionType[], showPosition: boolean, + fieldNames: FieldNames, ) { let triggerNode = null; let nodeList: LegacyCheckedNode[] = null; function generateMap() { - function dig(list: InternalDataEntity[], level = '0', parentIncluded = false) { + function dig(list: DefaultOptionType[], level = '0', parentIncluded = false) { return list - .map((dataNode, index) => { + .map((option, index) => { const pos = `${level}-${index}`; - const included = checkedValues.includes(dataNode.value); - const children = dig(dataNode.children || [], pos, included); - const node = {children.map(child => child.node)}; + const value = option[fieldNames.value]; + const included = checkedValues.includes(value); + const children = dig(option[fieldNames.children] || [], pos, included); + const node = ( + )}> + {children.map(child => child.node)} + + ); // Link with trigger node - if (triggerValue === dataNode.value) { + if (triggerValue === value) { triggerNode = node; } diff --git a/components/vc-tree-select/utils/strategyUtil.ts b/components/vc-tree-select/utils/strategyUtil.ts index ad47e768a..c40be6ac8 100644 --- a/components/vc-tree-select/utils/strategyUtil.ts +++ b/components/vc-tree-select/utils/strategyUtil.ts @@ -1,5 +1,6 @@ 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'; 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 function formatStrategyKeys( - keys: Key[], +export function formatStrategyValues( + values: Key[], strategy: CheckedStrategy, keyEntities: Record, + fieldNames: InternalFieldName, ): RawValueType[] { - const keySet = new Set(keys); + const valueSet = new Set(values); if (strategy === SHOW_CHILD) { - return keys.filter(key => { + return values.filter(key => { const entity = keyEntities[key]; if ( entity && entity.children && entity.children.every( - ({ node }) => isCheckDisabled(node) || keySet.has((node as DataNode).key), + ({ node }) => isCheckDisabled(node) || valueSet.has(node[fieldNames.value]), ) ) { return false; @@ -32,15 +34,14 @@ export function formatStrategyKeys( }); } if (strategy === SHOW_PARENT) { - return keys.filter(key => { + return values.filter(key => { const entity = keyEntities[key]; 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.key)) { return false; } return true; }); } - return keys; + return values; } diff --git a/components/vc-tree-select/utils/valueUtil.ts b/components/vc-tree-select/utils/valueUtil.ts index 33f71a74f..00ba0efec 100644 --- a/components/vc-tree-select/utils/valueUtil.ts +++ b/components/vc-tree-select/utils/valueUtil.ts @@ -1,21 +1,5 @@ -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; +import type { Key, DataNode, FieldNames } from '../interface'; +import type { DefaultOptionType, InternalFieldName } from '../TreeSelect'; export function toArray(value: T | T[]): T[] { if (Array.isArray(value)) { @@ -24,221 +8,43 @@ export function toArray(value: T | T[]): T[] { 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) { +export function fillFieldNames(fieldNames?: FieldNames) { const { label, value, children } = fieldNames || {}; - const filledNames: FieldNames = { - value: value || 'value', + const mergedValue = value || 'value'; + + return { + _title: label ? [label] : ['title', 'label'], + value: mergedValue, + key: mergedValue, children: children || 'children', }; - - if (!skipTitle || label) { - filledNames.label = label || 'label'; - } - - return filledNames; -} - -export function findValueOption(values: RawValueType[], options: CompatibleDataNode[]): DataNode[] { - const optionMap: Map = 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[]; -} +/** Loop fetch all the keys exist in the tree */ +export function getAllKeys(treeData: DefaultOptionType[], fieldNames: InternalFieldName) { + const keys: Key[] = []; -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, - }; + function dig(list: DefaultOptionType[]) { + list.forEach(item => { + keys.push(item[fieldNames.value]); + const children = item[fieldNames.children]; if (children) { - clone.children = fillKey(children); + dig(children); } - - return clone; }); } - const flattenList = flattenTreeData(fillKey(typedOptions), true, null); + dig(treeData); - const cacheMap = new Map(); - const flattenDateNodeList: (FlattenDataNode & { parentKey?: Key })[] = flattenList.map(option => { - const { data, key, value } = option as any as Omit & { - 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; + return keys; } -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; - }, -): DataNode[] { - if (filterOption === false) { - return options; - } - - let filterOptionFunc: FilterFunc; - 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(); - - 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); +export function isNil(val: any) { + return val === null || val === undefined; } diff --git a/components/vc-tree-select/utils/warningPropsUtil.ts b/components/vc-tree-select/utils/warningPropsUtil.ts index 2b58b8936..1af3d888e 100644 --- a/components/vc-tree-select/utils/warningPropsUtil.ts +++ b/components/vc-tree-select/utils/warningPropsUtil.ts @@ -1,7 +1,8 @@ import { warning } from '../../vc-util/warning'; +import type { TreeSelectProps } from '../TreeSelect'; import { toArray } from './valueUtil'; -function warningProps(props: any) { +function warningProps(props: TreeSelectProps & { searchPlaceholder?: string }) { const { searchPlaceholder, treeCheckStrictly, treeCheckable, labelInValue, value, multiple } = props; diff --git a/components/vc-tree-select2/Context.tsx b/components/vc-tree-select1/Context.tsx similarity index 100% rename from components/vc-tree-select2/Context.tsx rename to components/vc-tree-select1/Context.tsx diff --git a/components/vc-tree-select2/OptionList.tsx b/components/vc-tree-select1/OptionList.tsx similarity index 65% rename from components/vc-tree-select2/OptionList.tsx rename to components/vc-tree-select1/OptionList.tsx index 3db2c88fb..6d3ec67e4 100644 --- a/components/vc-tree-select2/OptionList.tsx +++ b/components/vc-tree-select1/OptionList.tsx @@ -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 { ScrollTo } from '../vc-virtual-list/List'; import { computed, defineComponent, nextTick, ref, shallowRef, watch } from 'vue'; +import { optionListProps } from './props'; import useMemo from '../_util/hooks/useMemo'; import type { EventDataNode } from '../tree'; import KeyCode from '../_util/KeyCode'; import Tree from '../vc-tree/Tree'; import type { TreeProps } from '../vc-tree/props'; -import { getAllKeys, isCheckDisabled } from './utils/valueUtil'; -import { useBaseProps } from '../vc-select'; -import useInjectLegacySelectContext from './LegacyContext'; -import useInjectSelectContext from './TreeSelectContext'; const HIDDEN_STYLE = { width: 0, @@ -34,36 +32,44 @@ type ReviseRefOptionListProps = Omit & { scrollT export default defineComponent({ name: 'OptionList', inheritAttrs: false, + props: optionListProps(), slots: ['notFoundContent', 'menuItemSelectedIcon'], - setup(_, { slots, expose }) { - const baseProps = useBaseProps(); - const legacyContext = useInjectLegacySelectContext(); - const context = useInjectSelectContext(); + setup(props, { slots, expose }) { + const context = useInjectTreeSelectContext(); + const treeRef = ref(); - const memoTreeData = useMemo( - () => context.treeData, - [() => baseProps.open, () => context.treeData], + const memoOptions = useMemo( + () => props.options, + [() => props.open, () => props.options], next => next[0], ); + const valueKeys = computed(() => { + const { checkedKeys, getEntityByValue } = context.value; + return checkedKeys.map(val => { + const entity = getEntityByValue(val); + return entity ? entity.key : null; + }); + }); + const mergedCheckedKeys = computed(() => { - const { checkable, halfCheckedKeys, checkedKeys } = legacyContext; + const { checkable, halfCheckedKeys } = context.value; if (!checkable) { return null; } return { - checked: checkedKeys, + checked: valueKeys.value, halfChecked: halfCheckedKeys, }; }); watch( - () => baseProps.open, + () => props.open, () => { nextTick(() => { - if (baseProps.open && !baseProps.multiple && legacyContext.checkedKeys.length) { - treeRef.value?.scrollTo({ key: legacyContext.checkedKeys[0] }); + if (props.open && !props.multiple && valueKeys.value.length) { + treeRef.value?.scrollTo({ key: valueKeys.value[0] }); } }); }, @@ -71,25 +77,25 @@ export default defineComponent({ ); // ========================== Search ========================== - const lowerSearchValue = computed(() => String(baseProps.searchValue).toLowerCase()); + const lowerSearchValue = computed(() => String(props.searchValue).toLowerCase()); const filterTreeNode = (treeNode: EventDataNode) => { if (!lowerSearchValue.value) { return false; } - return String(treeNode[legacyContext.treeNodeFilterProp]) + return String(treeNode[context.value.treeNodeFilterProp]) .toLowerCase() .includes(lowerSearchValue.value); }; // =========================== Keys =========================== - const expandedKeys = shallowRef(legacyContext.treeDefaultExpandedKeys); + const expandedKeys = shallowRef(context.value.treeDefaultExpandedKeys); const searchExpandedKeys = shallowRef(null); watch( - () => baseProps.searchValue, + () => props.searchValue, () => { - if (baseProps.searchValue) { - searchExpandedKeys.value = getAllKeys(context.treeData, context.fieldNames); + if (props.searchValue) { + searchExpandedKeys.value = props.flattenOptions.map(o => o.key); } }, { @@ -97,17 +103,17 @@ export default defineComponent({ }, ); const mergedExpandedKeys = computed(() => { - if (legacyContext.treeExpandedKeys) { - return [...legacyContext.treeExpandedKeys]; + if (context.value.treeExpandedKeys) { + return [...context.value.treeExpandedKeys]; } - return baseProps.searchValue ? searchExpandedKeys.value : expandedKeys.value; + return props.searchValue ? searchExpandedKeys.value : expandedKeys.value; }); const onInternalExpand = (keys: Key[]) => { expandedKeys.value = keys; searchExpandedKeys.value = keys; - legacyContext.onTreeExpand?.(keys); + context.value.onTreeExpand?.(keys); }; // ========================== Events ========================== @@ -115,23 +121,23 @@ export default defineComponent({ event.preventDefault(); }; - const onInternalSelect = (_: Key[], { node }: TreeEventInfo) => { - const { checkable, checkedKeys } = legacyContext; - if (checkable && isCheckDisabled(node)) { - return; + const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => { + const { getEntityByKey, checkable, checkedKeys } = context.value; + const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select'); + if (entity !== null) { + props.onSelect?.(entity.data.value, { + selected: !checkedKeys.includes(entity.data.value), + }); } - context.onSelect?.(node.key, { - selected: !checkedKeys.includes(node.key), - }); - if (!baseProps.multiple) { - baseProps.toggleOpen?.(false); + if (!props.multiple) { + props.onToggleOpen?.(false); } }; // ========================= Keyboard ========================= const activeKey = ref(null); - const activeEntity = computed(() => legacyContext.keyEntities[activeKey.value]); + const activeEntity = computed(() => context.value.getEntityByKey(activeKey.value)); const setActiveKey = (key: Key) => { activeKey.value = key; @@ -151,11 +157,11 @@ export default defineComponent({ // >>> Select item case KeyCode.ENTER: { - const { selectable, value } = activeEntity.value?.node || {}; + const { selectable, value } = activeEntity.value?.data.node || {}; if (selectable !== false) { onInternalSelect(null, { node: { key: activeKey.value }, - selected: !legacyContext.checkedKeys.includes(value), + selected: !context.value.checkedKeys.includes(value), }); } break; @@ -163,7 +169,7 @@ export default defineComponent({ // >>> Close case KeyCode.ESC: { - baseProps.toggleOpen(false); + props.onToggleOpen(false); } } }, @@ -173,12 +179,15 @@ export default defineComponent({ return () => { const { prefixCls, + height, + itemHeight, + virtual, multiple, searchValue, open, notFoundContent = slots.notFoundContent?.(), - } = baseProps; - const { listHeight, listItemHeight, virtual } = context; + onMouseenter, + } = props; const { checkable, treeDefaultExpandAll, @@ -190,10 +199,9 @@ export default defineComponent({ treeLoadedKeys, treeMotion, onTreeLoad, - checkedKeys, - } = legacyContext; + } = context.value; // ========================== Render ========================== - if (memoTreeData.value.length === 0) { + if (memoOptions.value.length === 0) { return (
{notFoundContent} @@ -209,10 +217,10 @@ export default defineComponent({ treeProps.expandedKeys = mergedExpandedKeys.value; } return ( -
+
{activeEntity.value && open && ( - {activeEntity.value.node.value} + {activeEntity.value.data.value} )} @@ -220,9 +228,9 @@ export default defineComponent({ ref={treeRef} focusable={false} prefixCls={`${prefixCls}-tree`} - treeData={memoTreeData.value as TreeDataNode[]} - height={listHeight} - itemHeight={listItemHeight} + treeData={memoOptions.value as TreeDataNode[]} + height={height} + itemHeight={itemHeight} virtual={virtual} multiple={multiple} icon={treeIcon} @@ -235,7 +243,7 @@ export default defineComponent({ checkable={checkable} checkStrictly checkedKeys={mergedCheckedKeys.value} - selectedKeys={!checkable ? checkedKeys : []} + selectedKeys={!checkable ? valueKeys.value : []} defaultExpandAll={treeDefaultExpandAll} {...treeProps} // Proxy event out @@ -245,7 +253,7 @@ export default defineComponent({ onExpand={onInternalExpand} onLoad={onTreeLoad} filterTreeNode={filterTreeNode} - v-slots={{ ...slots, checkable: legacyContext.customCheckable }} + v-slots={{ ...slots, checkable: context.value.customCheckable }} />
); diff --git a/components/vc-tree-select2/TreeNode.tsx b/components/vc-tree-select1/TreeNode.tsx similarity index 100% rename from components/vc-tree-select2/TreeNode.tsx rename to components/vc-tree-select1/TreeNode.tsx diff --git a/components/vc-tree-select1/TreeSelect.tsx b/components/vc-tree-select1/TreeSelect.tsx new file mode 100644 index 000000000..1f2d1684c --- /dev/null +++ b/components/vc-tree-select1/TreeSelect.tsx @@ -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; diff --git a/components/vc-tree-select/generate.tsx b/components/vc-tree-select1/generate.tsx similarity index 100% rename from components/vc-tree-select/generate.tsx rename to components/vc-tree-select1/generate.tsx diff --git a/components/vc-tree-select/hooks/useKeyValueMap.ts b/components/vc-tree-select1/hooks/useKeyValueMap.ts similarity index 100% rename from components/vc-tree-select/hooks/useKeyValueMap.ts rename to components/vc-tree-select1/hooks/useKeyValueMap.ts diff --git a/components/vc-tree-select/hooks/useKeyValueMapping.ts b/components/vc-tree-select1/hooks/useKeyValueMapping.ts similarity index 100% rename from components/vc-tree-select/hooks/useKeyValueMapping.ts rename to components/vc-tree-select1/hooks/useKeyValueMapping.ts diff --git a/components/vc-tree-select/hooks/useSelectValues.ts b/components/vc-tree-select1/hooks/useSelectValues.ts similarity index 100% rename from components/vc-tree-select/hooks/useSelectValues.ts rename to components/vc-tree-select1/hooks/useSelectValues.ts diff --git a/components/vc-tree-select1/hooks/useTreeData.ts b/components/vc-tree-select1/hooks/useTreeData.ts new file mode 100644 index 000000000..a274f3173 --- /dev/null +++ b/components/vc-tree-select1/hooks/useTreeData.ts @@ -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(); + + // 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, + children: Ref, + { + getLabelProp, + simpleMode, + fieldNames, + }: { + getLabelProp: (node: DataNode) => any; + simpleMode: Ref; + fieldNames: Ref; + }, +): ComputedRef { + 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); + } + }); +} diff --git a/components/vc-tree-select2/index.tsx b/components/vc-tree-select1/index.tsx similarity index 90% rename from components/vc-tree-select2/index.tsx rename to components/vc-tree-select1/index.tsx index 86cfc2e02..d3e52682a 100644 --- a/components/vc-tree-select2/index.tsx +++ b/components/vc-tree-select1/index.tsx @@ -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 TreeNode from './TreeNode'; import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil'; diff --git a/components/vc-tree-select2/interface.ts b/components/vc-tree-select1/interface.ts similarity index 100% rename from components/vc-tree-select2/interface.ts rename to components/vc-tree-select1/interface.ts diff --git a/components/vc-tree-select/props.ts b/components/vc-tree-select1/props.ts similarity index 100% rename from components/vc-tree-select/props.ts rename to components/vc-tree-select1/props.ts diff --git a/components/vc-tree-select2/utils/legacyUtil.tsx b/components/vc-tree-select1/utils/legacyUtil.tsx similarity index 80% rename from components/vc-tree-select2/utils/legacyUtil.tsx rename to components/vc-tree-select1/utils/legacyUtil.tsx index 621898bb8..8c2e358f3 100644 --- a/components/vc-tree-select2/utils/legacyUtil.tsx +++ b/components/vc-tree-select1/utils/legacyUtil.tsx @@ -1,15 +1,21 @@ import { filterEmpty } from '../../_util/props-util'; import { camelize } from 'vue'; 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 type { VueNode } from '../../_util/type'; -import type { DefaultOptionType, FieldNames } from '../TreeSelect'; function isTreeSelectNode(node: any) { 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[] { return filterEmpty(treeNodes).map(treeNode => { // Filter invalidate node @@ -60,10 +66,10 @@ export function convertChildrenToData(rootNodes: VueNode[]): DataNode[] { return dig(rootNodes as any[]); } -export function fillLegacyProps(dataNode: DataNode): any { +export function fillLegacyProps(dataNode: DataNode): LegacyDataNode { // Skip if not dataNode exist if (!dataNode) { - return dataNode; + return dataNode as LegacyDataNode; } const cloneNode = { ...dataNode }; @@ -73,43 +79,37 @@ export function fillLegacyProps(dataNode: DataNode): any { get() { warning( 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 as LegacyDataNode; } export function fillAdditionalInfo( extra: ChangeEventExtra, triggerValue: RawValueType, checkedValues: RawValueType[], - treeData: DefaultOptionType[], + treeData: InternalDataEntity[], showPosition: boolean, - fieldNames: FieldNames, ) { let triggerNode = null; let nodeList: LegacyCheckedNode[] = null; function generateMap() { - function dig(list: DefaultOptionType[], level = '0', parentIncluded = false) { + function dig(list: InternalDataEntity[], level = '0', parentIncluded = false) { return list - .map((option, index) => { + .map((dataNode, index) => { const pos = `${level}-${index}`; - const value = option[fieldNames.value]; - const included = checkedValues.includes(value); - const children = dig(option[fieldNames.children] || [], pos, included); - const node = ( - )}> - {children.map(child => child.node)} - - ); + const included = checkedValues.includes(dataNode.value); + const children = dig(dataNode.children || [], pos, included); + const node = {children.map(child => child.node)}; // Link with trigger node - if (triggerValue === value) { + if (triggerValue === dataNode.value) { triggerNode = node; } diff --git a/components/vc-tree-select2/utils/strategyUtil.ts b/components/vc-tree-select1/utils/strategyUtil.ts similarity index 62% rename from components/vc-tree-select2/utils/strategyUtil.ts rename to components/vc-tree-select1/utils/strategyUtil.ts index 7d7116cd8..ad47e768a 100644 --- a/components/vc-tree-select2/utils/strategyUtil.ts +++ b/components/vc-tree-select1/utils/strategyUtil.ts @@ -1,6 +1,5 @@ import type { DataEntity } from '../../vc-tree/interface'; -import type { InternalFieldName } from '../TreeSelect'; -import type { RawValueType, Key } from '../interface'; +import type { RawValueType, Key, DataNode } from '../interface'; import { isCheckDisabled } from './valueUtil'; 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 function formatStrategyValues( - values: Key[], +export function formatStrategyKeys( + keys: Key[], strategy: CheckedStrategy, keyEntities: Record, - fieldNames: InternalFieldName, ): RawValueType[] { - const valueSet = new Set(values); + const keySet = new Set(keys); if (strategy === SHOW_CHILD) { - return values.filter(key => { + return keys.filter(key => { const entity = keyEntities[key]; if ( entity && entity.children && entity.children.every( - ({ node }) => isCheckDisabled(node) || valueSet.has(node[fieldNames.value]), + ({ node }) => isCheckDisabled(node) || keySet.has((node as DataNode).key), ) ) { return false; @@ -34,15 +32,15 @@ export function formatStrategyValues( }); } if (strategy === SHOW_PARENT) { - return values.filter(key => { + return keys.filter(key => { const entity = keyEntities[key]; 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 true; }); } - return values; + return keys; } diff --git a/components/vc-tree-select1/utils/valueUtil.ts b/components/vc-tree-select1/utils/valueUtil.ts new file mode 100644 index 000000000..33f71a74f --- /dev/null +++ b/components/vc-tree-select1/utils/valueUtil.ts @@ -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; + +export function toArray(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 = 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(); + const flattenDateNodeList: (FlattenDataNode & { parentKey?: Key })[] = flattenList.map(option => { + const { data, key, value } = option as any as Omit & { + 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; + }, +): DataNode[] { + if (filterOption === false) { + return options; + } + + let filterOptionFunc: FilterFunc; + 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(); + + 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); +} diff --git a/components/vc-tree-select2/utils/warningPropsUtil.ts b/components/vc-tree-select1/utils/warningPropsUtil.ts similarity index 89% rename from components/vc-tree-select2/utils/warningPropsUtil.ts rename to components/vc-tree-select1/utils/warningPropsUtil.ts index 1af3d888e..2b58b8936 100644 --- a/components/vc-tree-select2/utils/warningPropsUtil.ts +++ b/components/vc-tree-select1/utils/warningPropsUtil.ts @@ -1,8 +1,7 @@ import { warning } from '../../vc-util/warning'; -import type { TreeSelectProps } from '../TreeSelect'; import { toArray } from './valueUtil'; -function warningProps(props: TreeSelectProps & { searchPlaceholder?: string }) { +function warningProps(props: any) { const { searchPlaceholder, treeCheckStrictly, treeCheckable, labelInValue, value, multiple } = props; diff --git a/components/vc-tree-select2/TreeSelect.tsx b/components/vc-tree-select2/TreeSelect.tsx deleted file mode 100644 index 3667dc277..000000000 --- a/components/vc-tree-select2/TreeSelect.tsx +++ /dev/null @@ -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 { - _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 }, - defaultValue: { type: [String, Number, Object, Array] as PropType }, - 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, - onDeselect: Function as PropType, - - showCheckedStrategy: { type: String as PropType }, - treeNodeLabelProp: String, - - fieldNames: { type: Object as PropType }, - - // >>> 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 }, - treeDataSimpleMode: { - type: [Boolean, Object] as PropType, - default: undefined, - }, - treeLoadedKeys: { type: Array as PropType }, - 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> }, - - // treeExpandedKeys: { type: Array as PropType }, - // treeDefaultExpandedKeys: { type: Array as PropType }, - - // treeDefaultExpandAll: { type: Boolean, default: undefined }, - - // children: Array, - - // dropdownPopupAlign: PropTypes.any, - - // // Event - - // onTreeExpand: { type: Function as PropType<(expandedKeys: Key[]) => void> }, - }; -} - -export type TreeSelectProps = Partial>>; - -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; - }; - }, -}); diff --git a/components/vc-tree-select2/generate.tsx b/components/vc-tree-select2/generate.tsx deleted file mode 100644 index 228dddc0f..000000000 --- a/components/vc-tree-select2/generate.tsx +++ /dev/null @@ -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['components']['optionList']; -}) { - const { prefixCls, optionList } = config; - - const RefSelect = generateSelector({ - 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) => 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( - 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([]); - const rawHalfCheckedKeys = shallowRef([]); - - 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 ( - - - - ); - }; - }, - }); -} diff --git a/components/vc-tree-select2/hooks/useKeyValueMap.ts b/components/vc-tree-select2/hooks/useKeyValueMap.ts deleted file mode 100644 index c0ee006ae..000000000 --- a/components/vc-tree-select2/hooks/useKeyValueMap.ts +++ /dev/null @@ -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) { - const cacheKeyMap: Ref> = shallowRef(new Map()); - const cacheValueMap: Ref> = 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]; -} diff --git a/components/vc-tree-select2/hooks/useKeyValueMapping.ts b/components/vc-tree-select2/hooks/useKeyValueMapping.ts deleted file mode 100644 index e385a035a..000000000 --- a/components/vc-tree-select2/hooks/useKeyValueMapping.ts +++ /dev/null @@ -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>, - cacheValueMap: Ref>, -): [ - (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]; -} diff --git a/components/vc-tree-select2/hooks/useSelectValues.ts b/components/vc-tree-select2/hooks/useSelectValues.ts deleted file mode 100644 index a8cc663fc..000000000 --- a/components/vc-tree-select2/hooks/useSelectValues.ts +++ /dev/null @@ -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; - /** Current `value` of TreeSelect */ - value: Ref; - showCheckedStrategy: Ref; - conductKeyEntities: Ref>; - 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, - { - value, - getEntityByValue, - getEntityByKey, - treeConduction, - showCheckedStrategy, - conductKeyEntities, - getLabelProp, - }: Config, -): Ref { - 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; -} diff --git a/components/vc-tree-select2/hooks/useTreeData.ts b/components/vc-tree-select2/hooks/useTreeData.ts deleted file mode 100644 index 6f9eee41e..000000000 --- a/components/vc-tree-select2/hooks/useTreeData.ts +++ /dev/null @@ -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, - children: Ref, - simpleMode: Ref, -): Ref { - 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); - }); -} diff --git a/components/vc-tree-select2/props.ts b/components/vc-tree-select2/props.ts deleted file mode 100644 index 98b40c432..000000000 --- a/components/vc-tree-select2/props.ts +++ /dev/null @@ -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() { - return { - prefixCls: String, - id: String, - options: { type: Array as PropType }, - flattenOptions: { type: Array as PropType }, - height: Number, - itemHeight: Number, - virtual: { type: Boolean, default: undefined }, - values: { type: Set as PropType> }, - 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() { - const selectProps = omit(selectBaseProps(), [ - '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 }, - defaultValue: { type: [String, Number, Object, Array] as PropType }, - 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 }, - loadData: { type: Function as PropType<(dataNode: LegacyDataNode) => Promise> }, - treeNodeFilterProp: String, - treeNodeLabelProp: String, - treeDataSimpleMode: { - type: [Boolean, Object] as PropType, - default: undefined, - }, - treeExpandedKeys: { type: Array as PropType }, - treeDefaultExpandedKeys: { type: Array as PropType }, - treeLoadedKeys: { type: Array as PropType }, - treeCheckable: { type: Boolean, default: undefined }, - treeCheckStrictly: { type: Boolean, default: undefined }, - showCheckedStrategy: { type: String as PropType }, - treeDefaultExpandAll: { type: Boolean, default: undefined }, - treeData: { type: Array as PropType }, - 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>, - 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 { - ReturnOptionListProps = optionListProps(); - ReturnTreeSelectProps = treeSelectProps(); -} - -export type OptionListProps = Partial['ReturnOptionListProps']>>; - -export type TreeSelectProps = Partial< - ExtractPropTypes['ReturnTreeSelectProps']> ->; diff --git a/components/vc-tree-select2/utils/valueUtil.ts b/components/vc-tree-select2/utils/valueUtil.ts deleted file mode 100644 index 00ba0efec..000000000 --- a/components/vc-tree-select2/utils/valueUtil.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Key, DataNode, FieldNames } from '../interface'; -import type { DefaultOptionType, InternalFieldName } from '../TreeSelect'; - -export function toArray(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; -}