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 7eef894
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 114 deletions.
128 changes: 63 additions & 65 deletions packages/design/src/components/tree/Tree.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import type { Meta } from '@storybook/react';
import React, { useState } from 'react';

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

const meta: Meta<typeof Tree> = {
Expand All @@ -29,99 +30,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-2',
title: 'node 0-2',
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-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',
title: 'node 0',
key: '0-9',
title: 'node 0-9',
children: [
{ key: '0-0', title: 'node 0-0' },
{ key: '0-1', title: 'node 0-1' },
{ 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[]>([]);

function handleChange(value: string | number | boolean) {
setValue(value);
function handleSelected(node: ITreeNodeProps, result: ITreeNodeProps[]) {
valueGroupSet(result.map((e) => e.key));
// eslint-disable-next-line no-console
console.log('all leafNode', node, result);
}

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

import { CheckMarkSingle, DropdownSingle } from '@univerjs/icons';
import { DropdownSingle } from '@univerjs/icons';
import clsx from 'clsx';
import React, { useEffect, useMemo, useState } from 'react';
import { Checkbox } from '../checkbox';

import styles from './index.module.less';
import { createCacheWithFindNodePathFromTree, filterLeafNode, isIntermediated, mergeTreeSelected } from './util';

export enum TreeSelectionMode {
ONLY_LEAF_NODE,
Expand Down Expand Up @@ -51,87 +53,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 +165,28 @@ 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 7eef894

Please sign in to comment.