diff --git a/packages/components/transfer/src/composables/useTransferData.ts b/packages/components/transfer/src/composables/useTransferData.ts index 5183e0783..1c3e9c3ae 100644 --- a/packages/components/transfer/src/composables/useTransferData.ts +++ b/packages/components/transfer/src/composables/useTransferData.ts @@ -30,8 +30,7 @@ export interface TransferDataContext { paginatedTargetData: ComputedRef targetKeys: ComputedRef targetKeySet: ComputedRef> - append: (keys: VKey[]) => void - remove: (keys: VKey[]) => void + changeTargetKeys: (removedKeys: VKey[], appendedKeys: VKey[]) => void clear: () => void getKey: ComputedRef @@ -122,11 +121,16 @@ export function useTransferData( getPaginatedData(filteredTargetData.value, transferPaginationContext.targetPagination.value), ) - const append = (keys: VKey[]) => { - _append(keys, targetKeySet.value, getKey.value, handleChange) - } - const remove = (keys: VKey[]) => { - _remove(keys, targetKeySet.value, getKey.value, handleChange) + const changeTargetKeys = (removedKeys: VKey[], appendedKeys: VKey[]) => { + let newKeys = targetKeys.value + if (removedKeys.length > 0) { + newKeys = _remove(removedKeys, newKeys, getKey.value) + } + if (appendedKeys.length > 0) { + newKeys = _append(appendedKeys, newKeys, getKey.value) + } + + handleChange(newKeys) } const clear = () => { @@ -161,8 +165,7 @@ export function useTransferData( paginatedTargetData, targetKeys, targetKeySet, - append, - remove, + changeTargetKeys, clear, getKey, diff --git a/packages/components/transfer/src/composables/useTransferDataStrategies.ts b/packages/components/transfer/src/composables/useTransferDataStrategies.ts index f3b3d126f..55aa8fda7 100644 --- a/packages/components/transfer/src/composables/useTransferDataStrategies.ts +++ b/packages/components/transfer/src/composables/useTransferDataStrategies.ts @@ -74,27 +74,23 @@ function createDefaultStrategies( return data.filter(item => searchFn(item, searchValue)) }, - append: (keys, selectedKeySet, _, handleChange) => { + append: (keys, selectedKeySet) => { const newKeys = new Set(selectedKeySet) keys.forEach(key => { newKeys.add(key) }) - if (newKeys.size !== selectedKeySet.size) { - handleChange(Array.from(newKeys)) - } + return Array.from(newKeys) }, - remove: (keys, selectedKeySet, _, handleChange) => { + remove: (keys, selectedKeySet) => { const newKeys = new Set(selectedKeySet) keys.forEach(key => { newKeys.delete(key) }) - if (newKeys.size !== selectedKeySet.size) { - handleChange(Array.from(newKeys)) - } + return Array.from(newKeys) }, } } diff --git a/packages/components/transfer/src/composables/useTransferOperations.ts b/packages/components/transfer/src/composables/useTransferOperations.ts index 4f9b5939c..56af96d84 100644 --- a/packages/components/transfer/src/composables/useTransferOperations.ts +++ b/packages/components/transfer/src/composables/useTransferOperations.ts @@ -29,8 +29,16 @@ export function useTransferOperations( transferDataContext: TransferDataContext, transferSelectStateContext: TransferSelectStateContext, ): TransferOperationsContext { - const { dataKeyMap, sourceData, targetData, disabledSourceKeys, disabledTargetKeys, append, remove, clear, getKey } = - transferDataContext + const { + dataKeyMap, + sourceData, + targetData, + disabledSourceKeys, + disabledTargetKeys, + changeTargetKeys, + clear, + getKey, + } = transferDataContext const { sourceSelectedKeys, targetSelectedKeys } = transferSelectStateContext const appendDisabled = computed(() => props.disabled || sourceSelectedKeys.value.length <= 0) @@ -47,7 +55,10 @@ export function useTransferOperations( return } - append((keys ?? Array.from(sourceSelectedKeys.value)).filter(key => !disabledSourceKeys.value.has(key))) + changeTargetKeys( + [], + (keys ?? Array.from(sourceSelectedKeys.value)).filter(key => !disabledSourceKeys.value.has(key)), + ) } const triggerRemove = (keys?: VKey[]) => { @@ -55,7 +66,10 @@ export function useTransferOperations( return } - remove((keys ?? Array.from(targetSelectedKeys.value)).filter(key => !disabledTargetKeys.value.has(key))) + changeTargetKeys( + (keys ?? Array.from(targetSelectedKeys.value)).filter(key => !disabledTargetKeys.value.has(key)), + [], + ) } const triggerAppendAll = () => { @@ -63,7 +77,10 @@ export function useTransferOperations( return } - append(Array.from(dataKeyMap.value.keys()).filter(key => !disabledSourceKeys.value.has(key))) + changeTargetKeys( + [], + Array.from(dataKeyMap.value.keys()).filter(key => !disabledSourceKeys.value.has(key)), + ) } const triggerClear = () => { diff --git a/packages/components/transfer/src/composables/useTransferSelectState.ts b/packages/components/transfer/src/composables/useTransferSelectState.ts index c3f601ef1..f1b3b78d5 100644 --- a/packages/components/transfer/src/composables/useTransferSelectState.ts +++ b/packages/components/transfer/src/composables/useTransferSelectState.ts @@ -44,8 +44,15 @@ export function useTransferSelectState( const sourceSelectedKeySet = computed(() => new Set(sourceSelectedKeys.value)) const targetSelectedKeySet = computed(() => new Set(targetSelectedKeys.value)) - const { dataKeyMap, sourceDataKeys, targetDataKeys, disabledKeys, disabledSourceKeys, disabledTargetKeys } = - transferDataContext + const { + dataKeyMap, + sourceDataKeys, + targetDataKeys, + targetKeySet, + disabledKeys, + disabledSourceKeys, + disabledTargetKeys, + } = transferDataContext const sourceDataCount = computed(() => props.mode === 'immediate' ? dataKeyMap.value.size : sourceDataKeys.value.size, @@ -72,7 +79,7 @@ export function useTransferSelectState( }) watch( - [sourceCheckableDataCount, dataKeyMap, disabledKeys, targetDataKeys], + [sourceCheckableDataCount, dataKeyMap, disabledKeys, targetKeySet], (_, [, , , prevSelectedKeys]) => { const tempKeys = new Set(sourceSelectedKeys.value) @@ -82,19 +89,19 @@ export function useTransferSelectState( return } - if (props.mode === 'default' && targetDataKeys.value.has(key)) { + if (props.mode === 'default' && targetKeySet.value.has(key)) { tempKeys.delete(key) } }) if (props.mode === 'immediate') { - targetDataKeys.value.forEach(key => { + targetKeySet.value.forEach(key => { if (dataKeyMap.value.has(key)) { tempKeys.add(key) } }) prevSelectedKeys?.forEach(key => { - if (!targetDataKeys.value.has(key)) { + if (!targetKeySet.value.has(key)) { tempKeys.delete(key) } }) @@ -208,7 +215,7 @@ function transferBySelectionChange( currentCheckedKeys: Set, transferDataContext: TransferDataContext, ) { - const { append, remove } = transferDataContext + const { changeTargetKeys } = transferDataContext const appendedKeys: VKey[] = [] const removedKeys: VKey[] = [] @@ -224,11 +231,5 @@ function transferBySelectionChange( } } - if (appendedKeys.length > 0) { - append(appendedKeys) - } - - if (removedKeys.length > 0) { - remove(removedKeys) - } + changeTargetKeys(removedKeys, appendedKeys) } diff --git a/packages/components/transfer/src/types.ts b/packages/components/transfer/src/types.ts index d71bad984..4f8bff485 100644 --- a/packages/components/transfer/src/types.ts +++ b/packages/components/transfer/src/types.ts @@ -31,8 +31,8 @@ export interface TransferDataStrategiesConfig SeparatedData dataFilter?: (data: T[], searchValue: string, searchFn: (item: T, searchValue: string) => boolean) => T[] - append?: (keys: K[], selectedKeySet: Set, getKey: GetKeyFn, handleChange: (keys: K[]) => void) => void - remove?: (keys: K[], selectedKeySet: Set, getKey: GetKeyFn, handleChange: (keys: K[]) => void) => void + append?: (keys: K[], selectedKeys: K[], getKey: GetKeyFn) => K[] + remove?: (keys: K[], selectedKeys: K[], getKey: GetKeyFn) => K[] } export type TransferDataStrategies = Required> @@ -59,10 +59,10 @@ export interface TransferBindings { pagination: ComputedRef - triggerAppend: (keys: K[]) => void - triggerRemove: (keys: K[]) => void + triggerAppend: (keys: VKey[]) => void + triggerRemove: (keys: VKey[]) => void getKey: ComputedRef - handleSelectChange: | VKey[]>(keys: K) => void + handleSelectChange: (keys: Set | VKey[]) => void selectAll: (selected?: boolean) => void searchValue: ComputedRef diff --git a/packages/pro/transfer/demo/BasicTree.md b/packages/pro/transfer/demo/BasicTree.md index 2b493def5..dd37068e6 100644 --- a/packages/pro/transfer/demo/BasicTree.md +++ b/packages/pro/transfer/demo/BasicTree.md @@ -1,5 +1,5 @@ --- -order: 1 +order: 10 title: zh: 基本树穿梭框 en: Basic tree transfer diff --git a/packages/pro/transfer/demo/FlattenTree.md b/packages/pro/transfer/demo/FlattenTree.md index b0dee121d..1f5a0e417 100644 --- a/packages/pro/transfer/demo/FlattenTree.md +++ b/packages/pro/transfer/demo/FlattenTree.md @@ -1,5 +1,5 @@ --- -order: 6 +order: 60 title: zh: 平展已选树 en: Flatten target tree diff --git a/packages/pro/transfer/demo/TableCustomLabel.md b/packages/pro/transfer/demo/TableCustomLabel.md index 4ab6dcd4e..881ec6641 100644 --- a/packages/pro/transfer/demo/TableCustomLabel.md +++ b/packages/pro/transfer/demo/TableCustomLabel.md @@ -2,7 +2,7 @@ title: zh: 表格穿梭框自定义表格列渲染 en: Customize Table Transfer Column Render -order: 11 +order: 110 --- ## zh diff --git a/packages/pro/transfer/demo/TableOneWay.md b/packages/pro/transfer/demo/TableOneWay.md index 7a1cb41ab..6cf6fef40 100644 --- a/packages/pro/transfer/demo/TableOneWay.md +++ b/packages/pro/transfer/demo/TableOneWay.md @@ -1,5 +1,5 @@ --- -order: 4 +order: 40 title: zh: 单向表格穿梭框 en: One-wayed tree transfer diff --git a/packages/pro/transfer/demo/TablePagination.md b/packages/pro/transfer/demo/TablePagination.md index 9c7fdae32..b401f60b6 100644 --- a/packages/pro/transfer/demo/TablePagination.md +++ b/packages/pro/transfer/demo/TablePagination.md @@ -1,5 +1,5 @@ --- -order: 2 +order: 20 title: zh: 表格穿梭框分页 en: Table transfer pagination diff --git a/packages/pro/transfer/demo/TableRemote.md b/packages/pro/transfer/demo/TableRemote.md index daae8ee59..28a346a86 100644 --- a/packages/pro/transfer/demo/TableRemote.md +++ b/packages/pro/transfer/demo/TableRemote.md @@ -1,5 +1,5 @@ --- -order: 9 +order: 90 title: zh: 表格穿梭框远程数据加载 en: Table transfer remote data diff --git a/packages/pro/transfer/demo/TableVirtual.md b/packages/pro/transfer/demo/TableVirtual.md index 9f2f916a1..f08e75edb 100644 --- a/packages/pro/transfer/demo/TableVirtual.md +++ b/packages/pro/transfer/demo/TableVirtual.md @@ -1,5 +1,5 @@ --- -order: 7 +order: 70 title: zh: 表格穿梭框虚拟滚动 en: Table transfer virtual scroll diff --git a/packages/pro/transfer/demo/TreeCustomLabel.md b/packages/pro/transfer/demo/TreeCustomLabel.md index e8bb69d54..376a4fc2b 100644 --- a/packages/pro/transfer/demo/TreeCustomLabel.md +++ b/packages/pro/transfer/demo/TreeCustomLabel.md @@ -1,5 +1,5 @@ --- -order: 12 +order: 120 title: zh: 树穿梭框自定义标签渲染 en: Customize Tree Transfer Label Render diff --git a/packages/pro/transfer/demo/TreeLoadChildren.md b/packages/pro/transfer/demo/TreeLoadChildren.md index 3f21f4b02..17fdca82a 100644 --- a/packages/pro/transfer/demo/TreeLoadChildren.md +++ b/packages/pro/transfer/demo/TreeLoadChildren.md @@ -1,5 +1,5 @@ --- -order: 11 +order: 110 title: zh: 树节点异步加载 en: Tree Load Children diff --git a/packages/pro/transfer/demo/TreeOneWay.md b/packages/pro/transfer/demo/TreeOneWay.md index c728ad43a..c14253c12 100644 --- a/packages/pro/transfer/demo/TreeOneWay.md +++ b/packages/pro/transfer/demo/TreeOneWay.md @@ -1,5 +1,5 @@ --- -order: 5 +order: 50 title: zh: 单向树穿梭框 en: One-wayed tree transfer diff --git a/packages/pro/transfer/demo/TreePagination.md b/packages/pro/transfer/demo/TreePagination.md index de84112ff..63a7b5fd7 100644 --- a/packages/pro/transfer/demo/TreePagination.md +++ b/packages/pro/transfer/demo/TreePagination.md @@ -1,5 +1,5 @@ --- -order: 3 +order: 30 title: zh: 树穿梭框分页 en: Tree transfer pagination diff --git a/packages/pro/transfer/demo/TreeRemote.md b/packages/pro/transfer/demo/TreeRemote.md index 65c928b9b..17e4f9fdd 100644 --- a/packages/pro/transfer/demo/TreeRemote.md +++ b/packages/pro/transfer/demo/TreeRemote.md @@ -1,5 +1,5 @@ --- -order: 10 +order: 100 title: zh: 树穿梭框远程数据加载 en: Tree transfer remote data diff --git a/packages/pro/transfer/demo/TreeVirtual.md b/packages/pro/transfer/demo/TreeVirtual.md index 2ce56f3b6..56692f217 100644 --- a/packages/pro/transfer/demo/TreeVirtual.md +++ b/packages/pro/transfer/demo/TreeVirtual.md @@ -1,5 +1,5 @@ --- -order: 8 +order: 80 title: zh: 树穿梭框虚拟滚动 en: Tree transfer virtual scroll diff --git a/packages/pro/transfer/docs/Api.zh.md b/packages/pro/transfer/docs/Api.zh.md index 95b4c20ab..7e8420c43 100644 --- a/packages/pro/transfer/docs/Api.zh.md +++ b/packages/pro/transfer/docs/Api.zh.md @@ -77,7 +77,9 @@ export type ProTransferTreeProps = Pick< | 'loadChildren' | 'onExpand' | 'onExpandedChange' -> +> & { + cascaderStrategy: TreeProps['checkStrategy'] +} ``` #### ProTransferSlots diff --git a/packages/pro/transfer/src/ProTransfer.tsx b/packages/pro/transfer/src/ProTransfer.tsx index 5ee7d278f..d3bc08e54 100644 --- a/packages/pro/transfer/src/ProTransfer.tsx +++ b/packages/pro/transfer/src/ProTransfer.tsx @@ -7,6 +7,7 @@ import type { TableInstance } from '@idux/components/table' import type { TreeInstance } from '@idux/components/tree' +import type { GetKeyFn } from '@idux/components/utils' import { type ComputedRef, computed, defineComponent, provide, ref } from 'vue' @@ -56,7 +57,7 @@ export default defineComponent({ const sourceContentRef = ref() const targetContentRef = ref() - const { dataKeyMap, parentKeyMap, expandedKeysContext } = useTreeContext(props, childrenKey, targetKeys) + const { dataKeyMap, parentKeyMap, expandedKeysContext } = useTreeContext(props, childrenKey, getKey, targetKeys) const { dataSource, loadSourceChildren, loadTargetChildren } = useTransferData( props, getKey, @@ -172,6 +173,7 @@ export default defineComponent({ function useTreeContext( props: ProTransferProps, childrenKey: ComputedRef, + getKey: ComputedRef, targetKeys: ComputedRef, ): { dataKeyMap?: Map> @@ -185,8 +187,9 @@ function useTreeContext( const { dataKeyMap, parentKeyMap, dataStrategies } = useTreeDataStrategies( childrenKey, props.defaultTargetData as TreeTransferData[] | undefined, + props.treeProps?.cascaderStrategy, ) - const expandedKeysContext = useTreeExpandedKeys(props, targetKeys, parentKeyMap) + const expandedKeysContext = useTreeExpandedKeys(props, childrenKey, getKey, targetKeys, parentKeyMap, dataKeyMap) provide(TRANSFER_DATA_STRATEGIES, dataStrategies as unknown as TransferDataStrategiesConfig) diff --git a/packages/pro/transfer/src/composables/useTransferData.ts b/packages/pro/transfer/src/composables/useTransferData.ts index 057969aac..95d3b8635 100644 --- a/packages/pro/transfer/src/composables/useTransferData.ts +++ b/packages/pro/transfer/src/composables/useTransferData.ts @@ -89,7 +89,7 @@ export function useTransferData( } return async (node: TreeNode, isSource: boolean) => { - const loadedData = await _loadChildren(node as TreeTransferData) + const loadedData = (await _loadChildren(node as TreeTransferData)) as TreeTransferData[] const key = getKey.value(node) if (!loadedData || loadedData.length <= 0) { diff --git a/packages/pro/transfer/src/composables/useTransferTableProps.ts b/packages/pro/transfer/src/composables/useTransferTableProps.ts index 3d71244df..67e450199 100644 --- a/packages/pro/transfer/src/composables/useTransferTableProps.ts +++ b/packages/pro/transfer/src/composables/useTransferTableProps.ts @@ -5,15 +5,15 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { ProTransferProps } from '../types' +import type { ProTransferProps, TransferData } from '../types' import type { TableColumn, TableColumnSelectable, TableProps } from '@idux/components/table' +import type { TransferBindings } from '@idux/components/transfer' import { type ComputedRef, type Slots, computed } from 'vue' import { isString } from 'lodash-es' import { callEmit } from '@idux/cdk/utils' -import { type TransferBindings } from '@idux/components/transfer' import { renderRemovableLabel } from '../content/RenderRemovableLabel' @@ -90,7 +90,7 @@ function convertTableColumns( convertedColumns.unshift(defaultSelectableColumn) } else { convertedColumns[selectableColumnIdx] = { - ...convertedColumns[selectableColumnIdx], + ...(convertedColumns[selectableColumnIdx] as TableColumnSelectable), ...defaultSelectableColumn, } } @@ -102,7 +102,7 @@ function convertTableColumns( const lastCol = convertedColumns[convertedColumns.length - 1] if ('type' in lastCol) { convertedColumns.push({ - customCell: ({ record }) => { + customCell: ({ record }: { record: TransferData }) => { const key = getKey.value(record) return renderRemovableLabel( key, @@ -131,7 +131,8 @@ function convertTableColumns( convertedColumns.splice(convertedColumns.length - 1, 1, { ...lastCol, - customCell: data => { + //eslint-disable-next-line @typescript-eslint/no-explicit-any + customCell: (data: { value: any; record: any; rowIndex: number }) => { const key = getKey.value(data.record) return renderRemovableLabel( key, diff --git a/packages/pro/transfer/src/composables/useTransferTreeProps.ts b/packages/pro/transfer/src/composables/useTransferTreeProps.ts index 5ec97e71a..c2bdd41ff 100644 --- a/packages/pro/transfer/src/composables/useTransferTreeProps.ts +++ b/packages/pro/transfer/src/composables/useTransferTreeProps.ts @@ -85,8 +85,8 @@ export function useTransferTreeProps( cascade: true, childrenKey: childrenKey.value, checkable: isSource || props.mode !== 'immediate', + checkStrategy: props.treeProps?.cascaderStrategy, checkedKeys: checkedKeys.value, - checkStrategy: 'all', dataSource: treeDataSource.value, disabled: node => _disabledKeys.value.has(getKey.value(node as TransferData)) || !!props.disabled, draggable: false, diff --git a/packages/pro/transfer/src/composables/useTreeDataStrategy.ts b/packages/pro/transfer/src/composables/useTreeDataStrategy.ts index fc117a2de..d9cf0f6ef 100644 --- a/packages/pro/transfer/src/composables/useTreeDataStrategy.ts +++ b/packages/pro/transfer/src/composables/useTreeDataStrategy.ts @@ -8,8 +8,10 @@ import type { TreeTransferData } from '../types' import type { VKey } from '@idux/cdk/utils' import type { TransferDataStrategiesConfig } from '@idux/components/transfer' +import type { TreeCheckStrategy } from '@idux/components/tree' +import type { GetKeyFn } from '@idux/components/utils' -import { type ComputedRef, onUnmounted } from 'vue' +import { type ComputedRef, type Ref, onUnmounted, ref } from 'vue' import { isNil } from 'lodash-es' @@ -18,17 +20,18 @@ import { combineTrees, filterTree, genFlattenedTreeKeys, traverseTree } from '.. export function useTreeDataStrategies( childrenKey: ComputedRef, defaultTreeData: TreeTransferData[] | undefined, + checkStrategy: TreeCheckStrategy = 'all', ): { dataKeyMap: Map> parentKeyMap: Map dataStrategies: TransferDataStrategiesConfig> } { - let cachedTargetData: TreeTransferData[] = defaultTreeData ?? [] + const cachedTargetData = ref(defaultTreeData ?? []) as Ref[]> const dataKeyMap: Map> = new Map() const parentKeyMap: Map = new Map() onUnmounted(() => { - cachedTargetData = [] + cachedTargetData.value = [] dataKeyMap.clear() parentKeyMap.clear() }) @@ -70,71 +73,196 @@ export function useTreeDataStrategies( dataKeyMap, parentKeyMap, dataStrategies: { - genDataKeys: (data, getRowKey) => { - return new Set(genFlattenedTreeKeys(data, childrenKey.value, getRowKey)) + genDataKeys: (data, getKey) => { + return new Set(genFlattenedTreeKeys(data, childrenKey.value, getKey)) }, - genDataKeyMap: (data, getRowKey) => { + genDataKeyMap: (data, getKey) => { parentKeyMap.clear() dataKeyMap.clear() - traverseTree(data, childrenKey.value, (node, parent) => { - const key = getRowKey(node) + traverseTree(data, childrenKey.value, (node, parents) => { + const key = getKey(node) dataKeyMap.set(key, node) - parentKeyMap.set(key, parent && getRowKey(parent)) + + const parent = parents[0] + parentKeyMap.set(key, parent && getKey(parent)) }) return new Map(dataKeyMap) }, genDisabledKeys, - separateDataSource: (data, _, selectedKeySet, getRowKey) => { - const filterData = (data: TreeTransferData[], isSource: boolean) => - filterTree( - data, - childrenKey.value, - item => (isSource ? !selectedKeySet.has(getRowKey(item)) : selectedKeySet.has(getRowKey(item))), - 'or', - ) - - const newTargetData = filterData(data, false) - const previousTargetData = filterData(cachedTargetData, false) - - const targetData = combineTrees(newTargetData, previousTargetData, childrenKey.value, getRowKey) - cachedTargetData = targetData - - return { - sourceData: filterData(data, true), - targetData, + dataFilter: (data, searchValue, searchFn) => { + if (!searchValue) { + return data } + + return filterTree(data, childrenKey.value, item => searchFn(item, searchValue), 'or') }, - remove: (keys, selectedKeySet, getRowKey, handleChange) => { - const newKeys = new Set(selectedKeySet) + ...createStrategy(childrenKey, cachedTargetData, parentKeyMap, dataKeyMap, checkStrategy), + }, + } +} - keys.forEach(key => { - newKeys.delete(key) +const commonRemoveFn = (keys: VKey[], selectedKeySet: VKey[], parentKeyMap: Map) => { + const newKeys = new Set(selectedKeySet) - let currentKey: VKey | undefined = key - while (parentKeyMap.has(currentKey!)) { - currentKey = parentKeyMap.get(currentKey!) + keys.forEach(key => { + let currentKey: VKey | undefined = key + while (currentKey) { + newKeys.delete(currentKey) + currentKey = parentKeyMap.get(currentKey!) + } + }) - if (isNil(currentKey)) { - return - } + return Array.from(newKeys) +} - newKeys.delete(currentKey) - } - }) +function createStrategy( + childrenKey: ComputedRef, + cachedTargetData: Ref[]>, + parentKeyMap: Map, + dataKeyMap: Map>, + checkStrategy: TreeCheckStrategy, +) { + switch (checkStrategy) { + case 'parent': + return createParentStrategy(childrenKey, cachedTargetData, dataKeyMap) + case 'child': + return createChildStrategy(childrenKey, cachedTargetData, parentKeyMap) + case 'all': + default: + return createAllStrategy(childrenKey, cachedTargetData, parentKeyMap) + } +} + +function createAllStrategy( + childrenKey: ComputedRef, + cachedTargetData: Ref[]>, + parentKeyMap: Map, +): TransferDataStrategiesConfig> { + return { + separateDataSource: createSeparateDataSourceFn(childrenKey, cachedTargetData, 'all'), + remove: (keys, selectedKeySet) => commonRemoveFn(keys, selectedKeySet, parentKeyMap), + } +} + +function createChildStrategy( + childrenKey: ComputedRef, + cachedTargetData: Ref[]>, + parentKeyMap: Map, +): TransferDataStrategiesConfig> { + return { + separateDataSource: createSeparateDataSourceFn(childrenKey, cachedTargetData, 'child'), + remove: (keys, selectedKeySet) => commonRemoveFn(keys, selectedKeySet, parentKeyMap), + } +} + +function createParentStrategy( + childrenKey: ComputedRef, + cachedTargetData: Ref[]>, + dataKeyMap: Map>, +): TransferDataStrategiesConfig> { + const separateDataSource = createSeparateDataSourceFn(childrenKey, cachedTargetData, 'parent') + let targetData: TreeTransferData[] = [] + + return { + separateDataSource(data, _, selectedKeySet, getKey) { + const separatedData = separateDataSource(data, _, selectedKeySet, getKey) + targetData = separatedData.targetData + return separatedData + }, + append(keys, selectedKey, getKey) { + const newKeySet = new Set(selectedKey) - if (newKeys.size !== selectedKeySet.size) { - handleChange(Array.from(newKeys)) + keys.forEach(key => { + newKeySet.add(key) + + const children = dataKeyMap.get(key)?.[childrenKey.value] + if (children) { + traverseTree(children, childrenKey.value, item => { + newKeySet.delete(getKey(item)) + }) } - }, - dataFilter: (data, searchValue, searchFn) => { - if (!searchValue) { - return data + }) + + return Array.from(newKeySet) + }, + remove(keys, selectedKey, getKey) { + const keySet = new Set(keys) + const newKeySet = new Set(selectedKey) + const deletedKeys = new Set() + + // store already deleted keys + const deleteKey = (key: VKey) => { + deletedKeys.add(key) + newKeySet.delete(key) + } + + // it is not possible to get the correct selected keys from bottom to top + traverseTree(targetData, childrenKey.value, (item, parents) => { + if (keySet.has(getKey(item))) { + const keysInChain = [item, ...parents].map(getKey) + // if some of the ancestors was selected + // replace parent node with child nodes + if (keysInChain.some(key => newKeySet.has(key))) { + parents.forEach(parent => { + parent[childrenKey.value]?.forEach(child => { + const childKey = getKey(child) + // add only if the child node hasn't been deleted before + if (!deletedKeys.has(childKey)) { + newKeySet.add(childKey) + } + }) + }) + // not one of the ancestors should be selected + keysInChain.forEach(key => deleteKey(key)) + } + } else if (parents.some(parent => keySet.has(getKey(parent)))) { + // if some of the ancestors was removed + // remove the node itself + deleteKey(getKey(item)) } + }) - return filterTree(data, childrenKey.value, item => searchFn(item, searchValue), 'or') - }, + return Array.from(newKeySet) }, } } + +function createSeparateDataSourceFn( + childrenKey: ComputedRef, + cachedTargetData: Ref[]>, + checkStrategy: TreeCheckStrategy, +): Exclude>['separateDataSource'], undefined> { + const getFilterFn = ( + selectedKeySet: Set, + getKey: GetKeyFn, + ): ((data: TreeTransferData[], isSource: boolean) => TreeTransferData[]) => { + // under checkStrategy `parent`, selected child nodes are not in selectedKeys + // so we consider the item is selected when its parent exists in selectedKeys + const filterFn: (item: TreeTransferData, parent: TreeTransferData[], isSource: boolean) => boolean = + checkStrategy === 'all' || checkStrategy === 'child' + ? (item, _, isSource) => selectedKeySet.has(getKey(item)) !== isSource + : (item, parent, isSource) => [item, ...parent].map(getKey).some(key => selectedKeySet.has(key)) !== isSource + + return (data, isSource) => + filterTree(data, childrenKey.value, (item, parent) => filterFn(item, parent, isSource), 'or') + } + + return (data, _, selectedKeySet, getKey) => { + const filterData = getFilterFn(selectedKeySet, getKey) + + const newTargetData = filterData(data, false) + const previousTargetData = filterData(cachedTargetData.value, false) + + // combine new data with previous data + // beacause we intend to cache selected data after dataSource changes + const targetData = combineTrees(newTargetData, previousTargetData, childrenKey.value, getKey) + cachedTargetData.value = targetData + + return { + sourceData: filterData(data, true), + targetData, + } + } +} diff --git a/packages/pro/transfer/src/composables/useTreeExpandedKeys.ts b/packages/pro/transfer/src/composables/useTreeExpandedKeys.ts index 1dde506ac..daebdabef 100644 --- a/packages/pro/transfer/src/composables/useTreeExpandedKeys.ts +++ b/packages/pro/transfer/src/composables/useTreeExpandedKeys.ts @@ -5,12 +5,15 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { ProTransferProps } from '../types' +import type { ProTransferProps, TreeTransferData } from '../types' +import type { GetKeyFn } from '@idux/components/utils' import { type ComputedRef, computed, watch } from 'vue' import { type VKey, useControlledProp } from '@idux/cdk/utils' +import { traverseTree } from '../utils' + export interface TreeExpandedKeysContext { sourceExpandedKeys: ComputedRef targetExpandedKeys: ComputedRef @@ -18,10 +21,13 @@ export interface TreeExpandedKeysContext { handleTargetExpandedChange: (keys: VKey[]) => void } -export function useTreeExpandedKeys( +export function useTreeExpandedKeys( props: ProTransferProps, + childrenKey: ComputedRef, + getKey: ComputedRef, targetKeys: ComputedRef, parentKeyMap: Map, + dataKeyMap: Map>, ): TreeExpandedKeysContext { const [sourceExpandedKeys, setSourceExpandedKeys] = useControlledProp(props, 'sourceExpandedKeys', () => []) const [targetExpandedKeys, setTargetExpandedKeys] = useControlledProp(props, 'targetExpandedKeys', () => []) @@ -29,14 +35,32 @@ export function useTreeExpandedKeys( const sourceExpandedKeySet = computed(() => new Set(sourceExpandedKeys.value)) const targetExpandedKeySet = computed(() => new Set(targetExpandedKeys.value)) - const syncSelectedExpandedState = (key: VKey, expandedKeysSource: Set, expandedKeysTarget: Set) => { + const syncSelectedExpandedState = ( + key: VKey, + expandedKeysSource: Set, + expandedKeysTarget: Set, + remove = false, + ) => { + const processExpandedKey = (key: VKey) => { + if (expandedKeysSource.has(key)) { + !expandedKeysTarget.has(key) && expandedKeysTarget.add(key) + } + } + let currentKey: VKey | undefined = key while (currentKey) { - if (expandedKeysSource.has(currentKey) && !expandedKeysTarget.has(currentKey)) { - expandedKeysTarget.add(currentKey) - } + processExpandedKey(currentKey) currentKey = parentKeyMap.get(currentKey) } + + const children = dataKeyMap.get(key)?.[childrenKey.value] + if (children) { + traverseTree(children, childrenKey.value, item => { + processExpandedKey(getKey.value(item)) + }) + } + + remove && expandedKeysSource.delete(key) } watch(targetKeys, (keys, oldKeys) => { @@ -45,17 +69,16 @@ export function useTreeExpandedKeys( const newTargetExpandedKeySet = new Set(targetExpandedKeySet.value) const newSourceExpandedKeySet = new Set(sourceExpandedKeySet.value) - newKeys?.forEach(key => { - syncSelectedExpandedState(key, sourceExpandedKeySet.value, newTargetExpandedKeySet) - }) + deletedKeys?.forEach(key => { - syncSelectedExpandedState(key, targetExpandedKeySet.value, newSourceExpandedKeySet) + syncSelectedExpandedState(key, newTargetExpandedKeySet, newSourceExpandedKeySet, true) + }) + newKeys?.forEach(key => { + syncSelectedExpandedState(key, newSourceExpandedKeySet, newTargetExpandedKeySet, props.mode !== 'immediate') }) - newTargetExpandedKeySet.size !== targetExpandedKeySet.value.size && - setTargetExpandedKeys(Array.from(newTargetExpandedKeySet)) - newSourceExpandedKeySet.size !== sourceExpandedKeySet.value.size && - setSourceExpandedKeys(Array.from(newSourceExpandedKeySet)) + setTargetExpandedKeys(Array.from(newTargetExpandedKeySet)) + setSourceExpandedKeys(Array.from(newSourceExpandedKeySet)) }) return { diff --git a/packages/pro/transfer/src/types.ts b/packages/pro/transfer/src/types.ts index 15b30e832..c2d535ee5 100644 --- a/packages/pro/transfer/src/types.ts +++ b/packages/pro/transfer/src/types.ts @@ -43,7 +43,9 @@ export type ProTransferTreeProps = Pick< | 'loadChildren' | 'onExpand' | 'onExpandedChange' -> +> & { + cascaderStrategy: TreeProps['checkStrategy'] +} export const proTransferProps = { type: { diff --git a/packages/pro/transfer/src/utils.ts b/packages/pro/transfer/src/utils.ts index cf13a6d58..270b65376 100644 --- a/packages/pro/transfer/src/utils.ts +++ b/packages/pro/transfer/src/utils.ts @@ -13,62 +13,71 @@ import { isArray, isNil } from 'lodash-es' export function traverseTree( data: TreeTransferData[], childrenKey: C, - fn: (item: TreeTransferData, parent: TreeTransferData | undefined) => void, - parent?: TreeTransferData, + fn: (item: TreeTransferData, parents: TreeTransferData[]) => void, ): void { - data.forEach(item => { - fn(item, parent) - if (item[childrenKey]) { - traverseTree(item[childrenKey]!, childrenKey, fn, item) - } - }) + const traverse = (_data: TreeTransferData[], parents: TreeTransferData[]) => { + _data.forEach(item => { + fn(item, parents) + if (item[childrenKey]) { + traverse(item[childrenKey]!, [item, ...parents]) + } + }) + } + + traverse(data, []) } export function mapTree( data: TreeTransferData[], childrenKey: C, - fn: (item: TreeTransferData, parent: TreeTransferData | undefined) => TreeTransferData, - parent?: TreeTransferData, + fn: (item: TreeTransferData, parents: TreeTransferData[]) => TreeTransferData, ): TreeTransferData[] { - return data.map(item => { - const mappedItem = fn(item, parent) - if (item[childrenKey]) { - const mappedChildren = mapTree(item[childrenKey]!, childrenKey, fn, item) - mappedItem[childrenKey] = mappedChildren as TreeTransferData[C] - } + const map = (_data: TreeTransferData[], parents: TreeTransferData[]) => { + return _data.map(item => { + const mappedItem = fn(item, parents) + if (item[childrenKey]) { + const mappedChildren = map(item[childrenKey]!, [item, ...parents]) + mappedItem[childrenKey] = mappedChildren as TreeTransferData[C] + } - return mappedItem - }) + return mappedItem + }) + } + return map(data, []) } export function filterTree( data: TreeTransferData[], childrenKey: C, - fn: (item: TreeTransferData) => boolean, + fn: (item: TreeTransferData, parents: TreeTransferData[]) => boolean, filterStrategy: 'and' | 'or' = 'or', ): TreeTransferData[] { - return data - .map(item => { - let children: TreeTransferData[] | undefined - - if (isArray(item[childrenKey])) { - children = filterTree(item[childrenKey]!, childrenKey, fn, filterStrategy) - } + const _filter = (_data: TreeTransferData[], parents: TreeTransferData[]): TreeTransferData[] => { + return _data + .map(item => { + let children: TreeTransferData[] | undefined - let itemValid = fn(item) - itemValid = - filterStrategy === 'and' - ? !!(children && children.length > 0) && itemValid - : !!(children && children.length > 0) || itemValid - - return ( - itemValid && { - ...item, - [childrenKey]: item[childrenKey] && children, + if (isArray(item[childrenKey])) { + children = _filter(item[childrenKey]!, [item, ...parents]) } - ) - }) - .filter(item => !!item) as TreeTransferData[] + + let itemValid = fn(item, parents) + itemValid = + filterStrategy === 'and' + ? !!(children && children.length > 0) && itemValid + : !!(children && children.length > 0) || itemValid + + return ( + itemValid && { + ...item, + [childrenKey]: item[childrenKey] && children, + } + ) + }) + .filter(Boolean) as TreeTransferData[] + } + + return _filter(data, []) } export function combineTrees(