diff --git a/devui/tree/src/composables/use-checked.ts b/devui/tree/src/composables/use-checked.ts new file mode 100644 index 0000000000000000000000000000000000000000..b22d9927887fa34430543a0a3d3b41e831829b6d --- /dev/null +++ b/devui/tree/src/composables/use-checked.ts @@ -0,0 +1,144 @@ +import type { SetupContext, Ref } from 'vue' +import { unref, ref } from 'vue' +import { + TreeItem, + SelectType, + ReverseTree, + CheckableRelationType, +} from '../tree-types' +import { flatten } from '../util' + +export default function useChecked( + cbr: Ref, + ctx: SetupContext, + data: any[] +) { + const selected = ref({}) + const flatData = flatten(data) + + const findPedigreeById = (id: string) => { + const treeData = data + let parentLevel: ReverseTree = {} + let childLevel: string[] = [] + let target = undefined + const ergodic = (curr: any[], parentNode: ReverseTree) => { + curr.every(({ children, id: itemId }) => { + if (target) { + return false + } + if (itemId === id) { + parentLevel = parentNode + childLevel = Array.isArray(children) + ? flatten(children).map(({ id: key }) => key) + : [] + target = itemId + return false + } + if (Array.isArray(children)) { + ergodic(children, { + id: itemId, + children: children.map(({ id: key }) => key), + parent: parentNode, + }) + } + return true + }) + } + ergodic(treeData, {}) + return { + parentLevel, + childLevel, + } + } + + const generateParentObject = ( + parentLevel: ReverseTree, + isSelected: boolean, + checkId: string + ): SelectType => { + const state: SelectType = {} + const currentSelected = unref(selected) + const ergodic = (currObj: ReverseTree, isOverflow = false) => { + const { id, children, parent } = currObj + if (!parent) { + return + } + if (isSelected) { + const optional = children.filter( + (item) => !currentSelected[item] || currentSelected[item] === 'none' + ) + if (optional.length <= 1) { + if (optional[0] === checkId) { + state[id] = 'select' + } else if (isOverflow) { + state[id] = 'half' + } + } else { + state[id] = 'half' + } + ergodic(parent, state[id] === 'select') + } else { + const optional = children.filter( + (item) => currentSelected[item] && currentSelected[item] !== 'none' + ) + if (optional.length <= 1) { + if (optional[0] === checkId || isOverflow) { + state[id] = 'none' + } + } else { + state[id] = 'half' + } + ergodic(parent, state[id] === 'none') + } + } + ergodic(parentLevel) + return state + } + + const onNodeClick = (item: TreeItem) => { + const { id } = item + let currentSelected = Object.assign({}, unref(selected)) + const isSelected = currentSelected[id] === 'none' || !currentSelected[id] + if (cbr.value === 'none') { + currentSelected = Object.assign(currentSelected, { + [id]: isSelected ? 'select' : 'none', + }) + } else if (cbr.value === 'both') { + const { parentLevel, childLevel } = findPedigreeById(id) + currentSelected = Object.assign( + currentSelected, + Object.fromEntries( + childLevel.map((key) => [key, isSelected ? 'select' : 'none']) + ), + generateParentObject(parentLevel, isSelected, id), + { [id]: isSelected ? 'select' : 'none' } + ) + } else if (cbr.value === 'upward') { + const { parentLevel } = findPedigreeById(id) + currentSelected = Object.assign( + currentSelected, + generateParentObject(parentLevel, isSelected, id), + { [id]: isSelected ? 'select' : 'none' } + ) + } else if (cbr.value === 'downward') { + const { childLevel } = findPedigreeById(id) + currentSelected = Object.assign( + currentSelected, + Object.fromEntries( + childLevel.map((key) => [key, isSelected ? 'select' : 'none']) + ), + { [id]: isSelected ? 'select' : 'none' } + ) + } + selected.value = currentSelected + const currentSelectedItem = flatData.filter( + ({ id }) => currentSelected[id] && currentSelected[id] !== 'none' + ) + ctx.emit('nodeSelected', currentSelectedItem) + } + + return { + selected, + onNodeClick, + } +} diff --git a/devui/tree/src/composables/use-merge-node.ts b/devui/tree/src/composables/use-merge-node.ts index b5c8db7d460823f1bdbc4cd34e46395912f86527..705cf07809a726a03854203c13b513b818d2d6b2 100644 --- a/devui/tree/src/composables/use-merge-node.ts +++ b/devui/tree/src/composables/use-merge-node.ts @@ -1,4 +1,4 @@ -import { ref } from 'vue'; +import { ref } from 'vue' export default function useMergeNode(data: Array): any { @@ -7,8 +7,8 @@ export default function useMergeNode(data: Array): any { treeItem, childName = 'children', labelName = 'label' - ): any => { - const { [childName]: children, [labelName]: label } = treeItem; + ) => { + const { [childName]: children, [labelName]: label } = treeItem if ( Array.isArray(children) && children.length === 1 && @@ -19,10 +19,10 @@ export default function useMergeNode(data: Array): any { Object.assign({}, children[0], { [labelName]: `${label} \\ ${children[0][labelName]}`, }) - ); + ) } - return treeItem; - }; + return treeItem + } const mergeNode = ( tree: Array, @@ -31,24 +31,24 @@ export default function useMergeNode(data: Array): any { labelName = 'label' ): Array => { return tree.map((item) => { - const { [childName]: children } = item; + const { [childName]: children } = item if (!Array.isArray(children) || !children.length) { - return Object.assign({}, item, { level: level + 1 }); + return Object.assign({}, item, { level: level + 1 }) } - let currentObject = item; + let currentObject = item if (children.length === 1) { - currentObject = mergeObject(item); + currentObject = mergeObject(item) } return Object.assign({}, currentObject, { [childName]: mergeNode(currentObject[childName], level + 1, childName, labelName), level: level + 1, - }); - }); - }; + }) + }) + } - const mergeData = ref(mergeNode(data)); + const mergeData = ref(mergeNode(data)) return { mergeData, - }; + } } diff --git a/devui/tree/src/config.ts b/devui/tree/src/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbd834d3d802d9abfe6d304c07e3688df292c8f5 --- /dev/null +++ b/devui/tree/src/config.ts @@ -0,0 +1,9 @@ +export const CHECK_CONFIG = { + none: {}, + half: { + halfchecked: true + }, + select: { + checked: true, + }, +} diff --git a/devui/tree/src/tree-node-content.tsx b/devui/tree/src/tree-node-content.tsx index f6a3b2eabb9adcd97006acd0685e2d12e246d6fa..3882736d4259f4328157874df81948bab3309f92 100644 --- a/devui/tree/src/tree-node-content.tsx +++ b/devui/tree/src/tree-node-content.tsx @@ -1,5 +1,5 @@ -import { defineComponent, h, inject } from 'vue'; -import { TreeRootType } from './tree-types'; +import { defineComponent, h, inject } from 'vue' +import { TreeRootType } from './tree-types' export default defineComponent({ name: 'DTreeNodeContent', @@ -10,13 +10,13 @@ export default defineComponent({ }, }, setup(props) { - const tree = inject('treeRoot'); + const tree = inject('treeRoot') return () => { - const node = props.node; - const { disabled, label } = node; + const node = props.node + const { disabled, label } = node return tree.ctx.slots.default ? tree.ctx.slots.default({ node }) - : { label }; - }; + : { label } + } }, -}); +}) diff --git a/devui/tree/src/tree-types.ts b/devui/tree/src/tree-types.ts index cec8a0d9cd733d54b90f5ac744fc12f3428020dd..03c9940125596cc1e483e802e7a4a219f3c76c31 100644 --- a/devui/tree/src/tree-types.ts +++ b/devui/tree/src/tree-types.ts @@ -9,14 +9,33 @@ export interface TreeItem { children?: TreeData [key: string]: any } +export interface SelectType { + [key: string]: 'none' | 'half' | 'select' +} + +export interface ReverseTree { + id?: string + children?: string[] + parent?: ReverseTree +} export type TreeData = Array +export type CheckableRelationType = 'downward' | 'upward' | 'both' | 'none' + export const treeProps = { data: { type: Array as PropType, required: true, default: () => [], + }, + checkable: { + type: Boolean, + default: false + }, + checkableRelation: { + type: String as () => CheckableRelationType, + default: 'none', } } as const diff --git a/devui/tree/src/tree.tsx b/devui/tree/src/tree.tsx index 0ea203d101e6bf4ea1229810de8675bd484f28d3..f9b45da9ea20e9deb5026317eeb991a2320e0e6e 100644 --- a/devui/tree/src/tree.tsx +++ b/devui/tree/src/tree.tsx @@ -1,10 +1,14 @@ import { defineComponent, toRefs, provide } from 'vue' +import type { SetupContext } from 'vue' import { treeProps, TreeProps, TreeItem, TreeRootType } from './tree-types' -import { flatten, precheckTree } from './util' +import { CHECK_CONFIG } from './config' +import { precheckTree } from './util' import Loading from '../../loading/src/service' +import Checkbox from '../../checkbox/src/checkbox' import useToggle from './composables/use-toggle' import useMergeNode from './composables/use-merge-node' import useHighlightNode from './composables/use-highlight' +import useChecked from './composables/use-checked' import useLazy from './composables/use-lazy' import IconOpen from './assets/open.svg' import IconClose from './assets/close.svg' @@ -14,20 +18,20 @@ import './tree.scss' export default defineComponent({ name: 'DTree', props: treeProps, - emits: [], - setup(props: TreeProps, ctx) { - const { data } = toRefs({ ...props, data: precheckTree(props.data) }) - const flatData = flatten(data.value) - + emits: ['nodeSelected'], + setup(props: TreeProps, ctx: SetupContext) { + const { data, checkable, checkableRelation: cbr } = toRefs({ ...props, data: precheckTree(props.data) }) const { mergeData } = useMergeNode(data.value) const { openedData, toggle } = useToggle(mergeData.value) const { nodeClassNameReflect, handleInitNodeClassNameReflect, handleClickOnNode } = useHighlightNode() const { lazyNodesReflect, handleInitLazyNodeReflect, getLazyData } = useLazy() + const { selected, onNodeClick } = useChecked(cbr, ctx, data.value) - provide('treeRoot', { ctx, props }); + provide('treeRoot', { ctx, props }) const Indent = () => { - return + return } + const renderNode = (item: TreeItem) => { const { id = '', label, disabled, open, isParent, level, children } = item handleInitNodeClassNameReflect(disabled, id) @@ -74,6 +78,7 @@ export default defineComponent({ : ) } + const checkState = CHECK_CONFIG[selected.value[id] ?? 'none'] return (
{renderNodeWithIcon(item)} + {checkable.value && onNodeClick(item)} disabled={disabled} {...checkState} />} {item.isParent &&
}
diff --git a/docs/components/tree/index.md b/docs/components/tree/index.md index 3599119ae00ee1781136cbfd32d7c273ac55ba07..3251f14788864f4fdd0d6b834c757e800c390137 100644 --- a/docs/components/tree/index.md +++ b/docs/components/tree/index.md @@ -304,6 +304,7 @@ export default defineComponent({