Skip to content

Commit

Permalink
feat(design): support multiple tree
Browse files Browse the repository at this point in the history
  • Loading branch information
Gggpound committed May 16, 2024
1 parent 62ee96e commit 34b0cbd
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 117 deletions.
129 changes: 63 additions & 66 deletions packages/design/src/components/tree/Tree.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import type { Meta } from '@storybook/react';
import React, { useState } from 'react';

import { Tree, TreeSelectionMode } from './Tree';
import { Tree, TreeSelectionMode, ITreeNodeProps } from './Tree';

Check failure on line 20 in packages/design/src/components/tree/Tree.stories.tsx

View workflow job for this annotation

GitHub Actions / eslint

Imports "ITreeNodeProps" are only used as type

Check failure on line 20 in packages/design/src/components/tree/Tree.stories.tsx

View workflow job for this annotation

GitHub Actions / eslint

Member 'ITreeNodeProps' of the import declaration should be sorted alphabetically

const meta: Meta<typeof Tree> = {
title: 'Components / Tree',
Expand All @@ -29,99 +29,96 @@ const meta: Meta<typeof Tree> = {
};

export default meta;

export const TreeBasic = {
render() {
const [value, setValue] = useState<string | number | boolean>();

const data = [
const data = [
{
key: '0',
title: 'node 0',
children: [
{ key: '0-0', title: 'node 0-0' },
{ key: '0-1', title: 'node 0-1' },
{
key: '0',
title: 'node 0',
key: '0-2',
title: 'node 0-2',
children: [
{ key: '0-0', title: 'node 0-0' },
{ key: '0-1', title: 'node 0-1' },
{ key: '0-2-0', title: 'node 0-2-0' },
{ key: '0-2-1', title: 'node 0-2-1' },
{ key: '0-2-2', title: 'node 0-2-2' },
],
},
{ key: '0-3', title: 'node 0-3' },
{ key: '0-4', title: 'node 0-4' },
{ key: '0-5', title: 'node 0-5' },
{ key: '0-6', title: 'node 0-6' },
{ key: '0-7', title: 'node 0-7' },
{ key: '0-8', title: 'node 0-8' },
{
key: '0-9',
title: 'node 0-9',
children: [
{ key: '0-9-0', title: 'node 0-9-0' },
{
key: '0-2',
title: 'node 0-2',
key: '0-9-1',
title: 'node 0-9-1',
children: [
{ key: '0-2-0', title: 'node 0-2-0' },
{ key: '0-2-1', title: 'node 0-2-1' },
{ key: '0-2-2', title: 'node 0-2-2' },
{ key: '0-9-1-0', title: 'node 0-9-1-0' },
{ key: '0-9-1-1', title: 'node 0-9-1-1' },
{ key: '0-9-1-2', title: 'node 0-9-1-2' },
{ key: '0-9-1-3', title: 'node 0-9-1-3' },
{ key: '0-9-1-4', title: 'node 0-9-1-4' },
],
},
{ key: '0-3', title: 'node 0-3' },
{ key: '0-4', title: 'node 0-4' },
{ key: '0-5', title: 'node 0-5' },
{ key: '0-6', title: 'node 0-6' },
{ key: '0-7', title: 'node 0-7' },
{ key: '0-8', title: 'node 0-8' },
{
key: '0-9',
title: 'node 0-9',
key: '0-9-2',
title: 'node 0-9-2',
children: [
{ key: '0-9-0', title: 'node 0-9-0' },
{
key: '0-9-1',
title: 'node 0-9-1',
children: [
{ key: '0-9-1-0', title: 'node 0-9-1-0' },
{ key: '0-9-1-1', title: 'node 0-9-1-1' },
{ key: '0-9-1-2', title: 'node 0-9-1-2' },
{ key: '0-9-1-3', title: 'node 0-9-1-3' },
{ key: '0-9-1-4', title: 'node 0-9-1-4' },
],
},
{
key: '0-9-2',
title: 'node 0-9-2',
children: [
{ key: '0-9-2-0', title: 'node 0-9-2-0' },
{ key: '0-9-2-1', title: 'node 0-9-2-1' },
],
},
{ key: '0-9-2-0', title: 'node 0-9-2-0' },
{ key: '0-9-2-1', title: 'node 0-9-2-1' },
],
},
],
},
],
},
{
key: '1',
title: 'node 1',
children: [
{
key: '1',
title: 'node 1',
// children: new Array(1000)
// .fill(null)
// .map((_, index) => ({ title: `auto ${index}`, key: `auto-${index}` })),
key: '1-0',
title: 'node 1-0',
children: [
{ key: '1-0-0', title: 'node 1-0-0' },
{
key: '1-0',
title: 'node 1-0',
key: '1-0-1',
title: 'node 1-0-1',
children: [
{ key: '1-0-0', title: 'node 1-0-0' },
{
key: '1-0-1',
title: 'node 1-0-1',
children: [
{ key: '1-0-1-0', title: 'node 1-0-1-0' },
{ key: '1-0-1-1', title: 'node 1-0-1-1' },
],
},
{ key: '1-0-2', title: 'node 1-0-2' },
{ key: '1-0-1-0', title: 'node 1-0-1-0' },
{ key: '1-0-1-1', title: 'node 1-0-1-1' },
],
},
{ key: '1-0-2', title: 'node 1-0-2' },
],
},
];
],
},
];
export const TreeBasic = {
render() {
const [valueGroup, valueGroupSet] = useState<string[]>([]);


Check failure on line 109 in packages/design/src/components/tree/Tree.stories.tsx

View workflow job for this annotation

GitHub Actions / eslint

More than 1 blank line not allowed
function handleChange(value: string | number | boolean) {
setValue(value);
function handleSelected(node: ITreeNodeProps, result: ITreeNodeProps[]) {
valueGroupSet(result.map(e => e.key));

Check failure on line 111 in packages/design/src/components/tree/Tree.stories.tsx

View workflow job for this annotation

GitHub Actions / eslint

Expected parentheses around arrow function argument
console.log('all leafNode', node, result)

Check failure on line 112 in packages/design/src/components/tree/Tree.stories.tsx

View workflow job for this annotation

GitHub Actions / eslint

Unexpected console statement

Check failure on line 112 in packages/design/src/components/tree/Tree.stories.tsx

View workflow job for this annotation

GitHub Actions / eslint

Missing semicolon
}

return (
<Tree
data={data}
defaultExpandAll
selectionMode={TreeSelectionMode.ONLY_LEAF_NODE}
value={value}
onChange={handleChange}
valueGroup={valueGroup}
onChange={handleSelected}
/>
);
},
Expand Down
119 changes: 74 additions & 45 deletions packages/design/src/components/tree/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
* limitations under the License.
*/

import { CheckMarkSingle, DropdownSingle } from '@univerjs/icons';
import { DropdownSingle } from '@univerjs/icons';
import clsx from 'clsx';
import { Checkbox } from '../checkbox'

Check failure on line 19 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

Missing semicolon
import React, { useEffect, useMemo, useState } from 'react';

Check failure on line 20 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

`react` import should occur before import of `../checkbox`

import styles from './index.module.less';

import { isIntermediated, createCacheWithFindNodePathFromTree, mergeTreeSelected, filterLeafNode } from './util'

Check failure on line 23 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

Expected 1 empty line after import statement not followed by another import

Check failure on line 23 in packages/design/src/components/tree/Tree.tsx

View workflow job for this annotation

GitHub Actions / eslint

Member 'createCacheWithFindNodePathFromTree' of the import declaration should be sorted alphabetically
export enum TreeSelectionMode {
ONLY_LEAF_NODE,

Expand Down Expand Up @@ -51,87 +52,107 @@ export interface ITreeProps {
selectionMode?: TreeSelectionMode;

/**
* Used for setting the currently selected value
* Used for setting the currently selected value,leaf node.
*/
value?: string | number | boolean;
valueGroup?: string[];

/**
* Set the handler to handle `click` event
*/
onChange?: (value: string | number | boolean) => void;
onChange?: (node: ITreeNodeProps, allSelectedNode: ITreeNodeProps[]) => void;

onExpend?: (value: string) => void
}

type TreeItemProps = ITreeNodeProps & {
_selected?: boolean;
_expand?: boolean;
_intermediated?: boolean
};

/**
* Tree Component
*/
export function Tree(props: ITreeProps) {
const { data = [], defaultExpandAll = false, selectionMode = TreeSelectionMode.ALL, value, onChange } = props;

const [expandKeys, setExpandKeys] = useState<Array<string | number | boolean>>([]);
const { data = [], defaultExpandAll = false, selectionMode = TreeSelectionMode.ALL, valueGroup = [], onChange, onExpend } = props;
const [update, forceUpdate] = useState({});
const expandKeySet = useMemo(() => {
return new Set<string>()
}, [data]);

const findNode = useMemo(() => createCacheWithFindNodePathFromTree(data), [data])

const selectedNodeKeySet = useMemo(() => {
const set = new Set<string>()
valueGroup.forEach(key => {
const path = findNode.findNodePathFromTreeWithCache(key)
path.forEach(k => set.add(k))
})
return set
}, [valueGroup, findNode])

useEffect(() => {
function walkData(item: ITreeNodeProps) {
setExpandKeys((prev) => [...prev, item.key]);

expandKeySet.add(item.key)
item.children?.forEach(walkData);
}
if (defaultExpandAll) {
data.forEach(walkData);
}
forceUpdate({})
}, [defaultExpandAll, data]);

const computedData = useMemo(() => {
return data.map(function walkData(item): TreeItemProps {
const { title, key, children } = item;

return {
title,
key,
children: children && children.map(walkData),
_selected: key === value,
_expand: expandKeys.includes(key),
};
return data.map((item) => {
function walkData(item: ITreeNodeProps): TreeItemProps {
const { title, key, children } = item;
const isExpand = expandKeySet.has(key)
const isSelected = selectedNodeKeySet.has(key)
const intermediated = isIntermediated(selectedNodeKeySet, item)

return {
title,
key,
children: children && children.map((item) => walkData(item)),
_selected: isSelected,
_expand: isExpand,
_intermediated: intermediated
};
}
return walkData(item)
});
}, [value, expandKeys]);

function handleSelectItem(treeItem: ITreeNodeProps) {
if (treeItem.children) {
setExpandKeys((prev) => {
const index = prev.findIndex((key) => key === treeItem.key);
}, [selectedNodeKeySet, expandKeySet, update]);

if (index === -1) {
return [...prev, treeItem.key];
}
function handleChange(treeItem: TreeItemProps) {
const path: string[] = findNode.findNodePathFromTreeWithCache(treeItem.key)
const result = mergeTreeSelected(data, [...selectedNodeKeySet], path)
onChange?.(treeItem, filterLeafNode(data, result))
}

return [...prev.slice(0, index), ...prev.slice(index + 1)];
});
function handleExpendItem(treeItem: ITreeNodeProps) {
if (treeItem.children?.length) {
if (expandKeySet.has(treeItem.key)) {
expandKeySet.delete(treeItem.key)
} else {
expandKeySet.add(treeItem.key)
}
forceUpdate({})
}

if (selectionMode === TreeSelectionMode.ONLY_LEAF_NODE) {
if (treeItem.children) {
return;
}
}

onChange?.(treeItem.key);
onExpend?.(treeItem.key)
}

function walkTree(treeItem: TreeItemProps) {
const { title, key, children, _selected, _expand } = treeItem;

const { title, key, children, _selected, _expand, _intermediated } = treeItem;
return (
<li
key={key}
className={styles.treeListItem}
onClick={(e) => {
e.stopPropagation();
handleSelectItem(treeItem);
}}
>
<a
className={clsx(styles.treeListItemContent, {
Expand All @@ -143,16 +164,24 @@ export function Tree(props: ITreeProps) {
className={clsx(styles.treeIcon, {
[styles.treeIconExpand]: _expand,
})}
onClick={(e) => {
e.stopPropagation();
handleExpendItem(treeItem);
}}
>
<DropdownSingle />
</span>
)}
{_selected && (
<span className={styles.treeListItemContentSelectedIcon}>
<CheckMarkSingle />
</span>
)}
<span>{title}</span>
<Checkbox
checked={_selected && !_intermediated}
indeterminate={_selected && _intermediated}
onChange={() => {
handleChange(treeItem)
}} />
<span onClick={(e) => {
e.stopPropagation();
handleExpendItem(treeItem);
}}>{title}</span>
</a>
{children && (
<ul
Expand Down
Loading

0 comments on commit 34b0cbd

Please sign in to comment.