diff --git a/COMPONENT_INDEX.md b/COMPONENT_INDEX.md index 3cd4849125..ab5811074e 100644 --- a/COMPONENT_INDEX.md +++ b/COMPONENT_INDEX.md @@ -4699,20 +4699,21 @@ export interface TreeNode { ### Props -| Prop name | Required | Kind | Reactive | Type | Default value | Description | -| :------------ | :------- | :-------------------- | :------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| expandedIds | No | let | Yes | ReadonlyArray | [] | Set the node ids to be expanded | -| selectedIds | No | let | Yes | ReadonlyArray | [] | Set the node ids to be selected | -| activeId | No | let | Yes | TreeNodeId | "" | Set the current active node id
Only one node can be active | -| nodes | No | let | No | Array | [] | Provide an array of nodes to render | -| size | No | let | No | "default" | "compact" | "default" | Specify the TreeView size | -| labelText | No | let | No | string | "" | Specify the label text | -| hideLabel | No | let | No | boolean | false | Set to `true` to visually hide the label text | -| expandAll | No | function | No | () => void | () => { expandedIds = [...nodeIds]; } | Programmatically expand all nodes | -| collapseAll | No | function | No | () => void | () => { expandedIds = []; } | Programmatically collapse all nodes | -| expandNodes | No | function | No | (filterId?: (node: TreeNode) => boolean) => void | () => { expandedIds = flattenedNodes .filter( (node) => filterNode(node) || node.nodes?.some((child) => filterNode(child) && child.nodes), ) .map((node) => node.id); } | Programmatically expand a subset of nodes.
Expands all nodes if no argument is provided | -| collapseNodes | No | function | No | (filterId?: (node: TreeNode) => boolean) => void | () => { expandedIds = flattenedNodes .filter((node) => expandedIds.includes(node.id) && !filterNode(node)) .map((node) => node.id); } | Programmatically collapse a subset of nodes.
Collapses all nodes if no argument is provided | -| showNode | No | function | No | (id: TreeNodeId) => void | () => { for (const child of nodes) { const nodes = findNodeById(child, id); if (nodes) { const ids = nodes.map((node) => node.id); const nodeIds = new Set(ids); expandNodes((node) => nodeIds.has(node.id)); const lastId = ids[ids.length - 1]; activeId = lastId; selectedIds = [lastId]; tick().then(() => { ref?.querySelector(\`[id="${lastId}"]\`)?.focus(); }); break; } } } | Programmatically show a node by `id`.
The matching node will be expanded, selected, and focused | +| Prop name | Required | Kind | Reactive | Type | Default value | Description | +| :------------ | :------- | :-------------------- | :------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| expandedIds | No | let | Yes | ReadonlyArray | [] | Set the node ids to be expanded | +| selectedIds | No | let | Yes | ReadonlyArray | [] | Set the node ids to be selected | +| activeId | No | let | Yes | TreeNodeId | "" | Set the current active node id
Only one node can be active | +| nodes | No | let | No | Array | [] | Provide a nested array of nodes to render | +| size | No | let | No | "default" | "compact" | "default" | Specify the TreeView size | +| labelText | No | let | No | string | "" | Specify the label text | +| hideLabel | No | let | No | boolean | false | Set to `true` to visually hide the label text | +| expandAll | No | function | No | () => void | () => { expandedIds = [...nodeIds]; } | Programmatically expand all nodes | +| collapseAll | No | function | No | () => void | () => { expandedIds = []; } | Programmatically collapse all nodes | +| toHierarchy | No | function | No | (flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[] | () => { return th(flatArray); } | Create a nested array from a flat array | +| expandNodes | No | function | No | (filterId?: (node: TreeNode) => boolean) => void | () => { expandedIds = flattenedNodes .filter( (node) => filterNode(node) || node.nodes?.some((child) => filterNode(child) && child.nodes), ) .map((node) => node.id); } | Programmatically expand a subset of nodes.
Expands all nodes if no argument is provided | +| collapseNodes | No | function | No | (filterId?: (node: TreeNode) => boolean) => void | () => { expandedIds = flattenedNodes .filter((node) => expandedIds.includes(node.id) && !filterNode(node)) .map((node) => node.id); } | Programmatically collapse a subset of nodes.
Collapses all nodes if no argument is provided | +| showNode | No | function | No | (id: TreeNodeId) => void | () => { for (const child of nodes) { const nodes = findNodeById(child, id); if (nodes) { const ids = nodes.map((node) => node.id); const nodeIds = new Set(ids); expandNodes((node) => nodeIds.has(node.id)); const lastId = ids[ids.length - 1]; activeId = lastId; selectedIds = [lastId]; tick().then(() => { ref?.querySelector(\`[id="${lastId}"]\`)?.focus(); }); break; } } } | Programmatically show a node by `id`.
The matching node will be expanded, selected, and focused | ### Slots diff --git a/docs/package-lock.json b/docs/package-lock.json index aa37ed8078..81fdc5494b 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -24,7 +24,7 @@ } }, "..": { - "version": "0.86.1", + "version": "0.86.2", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", diff --git a/docs/src/COMPONENT_API.json b/docs/src/COMPONENT_API.json index 469dea2d09..962830d49a 100644 --- a/docs/src/COMPONENT_API.json +++ b/docs/src/COMPONENT_API.json @@ -3235,7 +3235,10 @@ "ts": "interface DataTableCell<\n Row = DataTableRow,\n> {\n key:\n | DataTableKey\n | (string & {});\n value: DataTableValue;\n display?: (\n item: DataTableValue,\n row: DataTableRow,\n ) => DataTableValue;\n}\n" } ], - "generics": ["Row", "Row extends DataTableRow = DataTableRow"], + "generics": [ + "Row", + "Row extends DataTableRow = DataTableRow" + ], "rest_props": { "type": "Element", "name": "div" @@ -17767,7 +17770,7 @@ { "name": "nodes", "kind": "let", - "description": "Provide an array of nodes to render", + "description": "Provide a nested array of nodes to render", "type": "Array", "value": "[]", "isFunction": false, @@ -17872,6 +17875,18 @@ "constant": false, "reactive": false }, + { + "name": "toHierarchy", + "kind": "function", + "description": "Create a nested array from a flat array", + "type": "(flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[]", + "value": "() => {\n return th(flatArray);\n}", + "isFunction": true, + "isFunctionDeclaration": true, + "isRequired": false, + "constant": false, + "reactive": false + }, { "name": "expandNodes", "kind": "function", @@ -18063,4 +18078,4 @@ } } ] -} +} \ No newline at end of file diff --git a/docs/src/pages/components/TreeView.svx b/docs/src/pages/components/TreeView.svx index c7c97c92de..5c639924c2 100644 --- a/docs/src/pages/components/TreeView.svx +++ b/docs/src/pages/components/TreeView.svx @@ -64,6 +64,7 @@ Expanded nodes can be set using `expandedIds`. + ## Initial multiple selected nodes Initial multiple selected nodes can be set using `selectedIds`. @@ -107,3 +108,13 @@ Use the `TreeView.showNode` method to show a specific node. If a matching node is found, it will be expanded, selected, and focused. + +## Flat data structure + +Use the `toHierarchy` method to provide a flat data structure to the `nodes` property. + +This method will transform a flat array of objects into the hierarchical array as expected by `nodes`. +The child objects in the flat array need to have a `pid` property to reference its parent. +When `pid` is not provided the object is assumed to be at the root of the tree. + + \ No newline at end of file diff --git a/docs/src/pages/framed/TreeView/TreeViewFlatArray.svelte b/docs/src/pages/framed/TreeView/TreeViewFlatArray.svelte new file mode 100644 index 0000000000..0f14cc36ef --- /dev/null +++ b/docs/src/pages/framed/TreeView/TreeViewFlatArray.svelte @@ -0,0 +1,69 @@ + + + console.log("select", detail)} + on:toggle={({ detail }) => console.log("toggle", detail)} + on:focus={({ detail }) => console.log("focus", detail)} +/> + +
Active node id: {activeId}
+
Selected ids: {JSON.stringify(selectedIds)}
+ + diff --git a/src/TreeView/TreeView.svelte b/src/TreeView/TreeView.svelte index 31e3f26795..7f3a028ec6 100644 --- a/src/TreeView/TreeView.svelte +++ b/src/TreeView/TreeView.svelte @@ -37,7 +37,7 @@ */ /** - * Provide an array of nodes to render + * Provide a nested array of nodes to render * @type {Array} */ export let nodes = []; @@ -89,6 +89,14 @@ expandedIds = []; } + /** + * Create a nested array from a flat array + * @type {(flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[]} + */ + export function toHierarchy(flatArray) { + return th(flatArray); + } + /** * Programmatically expand a subset of nodes. * Expands all nodes if no argument is provided @@ -147,6 +155,7 @@ import { createEventDispatcher, setContext, onMount, tick } from "svelte"; import { writable } from "svelte/store"; import TreeViewNodeList from "./TreeViewNodeList.svelte"; + import { toHierarchy as th } from "./treeview"; const dispatch = createEventDispatcher(); const labelId = `label-${Math.random().toString(36)}`; diff --git a/src/TreeView/index.d.ts b/src/TreeView/index.d.ts new file mode 100644 index 0000000000..52e3204e61 --- /dev/null +++ b/src/TreeView/index.d.ts @@ -0,0 +1,2 @@ +export { default as TreeView } from "./TreeView.svelte"; +export { toHierarchy } from "./treeview"; diff --git a/src/TreeView/index.js b/src/TreeView/index.js index 59f96be0f3..52e3204e61 100644 --- a/src/TreeView/index.js +++ b/src/TreeView/index.js @@ -1 +1,2 @@ export { default as TreeView } from "./TreeView.svelte"; +export { toHierarchy } from "./treeview"; diff --git a/src/TreeView/treeview.d.ts b/src/TreeView/treeview.d.ts new file mode 100644 index 0000000000..9506bbdc5e --- /dev/null +++ b/src/TreeView/treeview.d.ts @@ -0,0 +1,9 @@ +import { type TreeNode } from "./TreeView.svelte"; +/** + * Create a nested array from a flat array + */ +export function toHierarchy( + flatArray: TreeNode[] & { pid?: any }[], +): TreeNode[]; + +export default toHierarchy; diff --git a/src/TreeView/treeview.js b/src/TreeView/treeview.js new file mode 100644 index 0000000000..5804ae92d2 --- /dev/null +++ b/src/TreeView/treeview.js @@ -0,0 +1,40 @@ +/** + * Create a nested array from a flat array + * @type {(flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[]} + */ +export function toHierarchy(flatArray) { + /** @type TreeNode[] */ + const tree = []; + /** @type TreeNode[] */ + const childrenOf = []; + + flatArray.forEach((dstItem) => { + const { id, pid } = dstItem; + childrenOf[id] = childrenOf[id] || []; + dstItem["nodes"] = childrenOf[id]; + + if (pid) { + // objects without pid are root level objects. + childrenOf[pid] = childrenOf[pid] || []; + delete dstItem.pid; // TreeNode type doesn't have pid. + childrenOf[pid].push(dstItem); + } else { + delete dstItem.pid; + tree.push(dstItem); + } + }); + + // Remove the empty nodes props that make TreeView render a twistie. + function removeEmptyNodes(element) { + element.forEach((elmt) => { + if (elmt.nodes?.length === 0) delete elmt.nodes; + else { + removeEmptyNodes(elmt.nodes); + } + }); + } + removeEmptyNodes(tree); + return tree; +} + +export default toHierarchy; diff --git a/src/index.js b/src/index.js index ea1a84cf46..75a2c8dcc9 100644 --- a/src/index.js +++ b/src/index.js @@ -127,6 +127,7 @@ export { Tooltip, TooltipFooter } from "./Tooltip"; export { TooltipDefinition } from "./TooltipDefinition"; export { TooltipIcon } from "./TooltipIcon"; export { TreeView } from "./TreeView"; +export { toHierarchy } from "./TreeView/treeview"; export { Truncate } from "./Truncate"; export { default as truncate } from "./Truncate/truncate"; export { diff --git a/tests/TreeView/TreeView.test.ts b/tests/TreeView/TreeView.test.ts index b2a0a6a3f2..0e29a9d9c4 100644 --- a/tests/TreeView/TreeView.test.ts +++ b/tests/TreeView/TreeView.test.ts @@ -1,6 +1,7 @@ import { render, screen } from "@testing-library/svelte"; import { user } from "../setup-tests"; import TreeView from "./TreeView.test.svelte"; +import TreeViewFlatArray from "./TreeViewFlatArray.test.svelte"; describe("TreeView", () => { const getItemByName = (name: RegExp) => { @@ -37,4 +38,14 @@ describe("TreeView", () => { text: "AI / Machine learning", }); }); + + it("can turn flat array into nested array", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(TreeViewFlatArray); + const firstItem = getItemByName(/Blockchain/); + expect(firstItem).toBeInTheDocument(); + await user.click(firstItem); + expect(getSelectedItemByName(/Blockchain/)).toBeInTheDocument(); + expect(consoleLog).toBeCalledWith("selectedIds", [7]); + }); }); diff --git a/tests/TreeView/TreeViewFlatArray.test.svelte b/tests/TreeView/TreeViewFlatArray.test.svelte new file mode 100644 index 0000000000..005394724b --- /dev/null +++ b/tests/TreeView/TreeViewFlatArray.test.svelte @@ -0,0 +1,77 @@ + + + console.log("select", detail)} + on:toggle={({ detail }) => console.log("toggle", detail)} + on:focus={({ detail }) => console.log("focus", detail)} + let:node +> + {node.text} + + + diff --git a/types/TreeView/TreeView.svelte.d.ts b/types/TreeView/TreeView.svelte.d.ts index 342f24c95b..b70f211992 100644 --- a/types/TreeView/TreeView.svelte.d.ts +++ b/types/TreeView/TreeView.svelte.d.ts @@ -15,7 +15,7 @@ type $RestProps = SvelteHTMLElements["ul"]; type $Props = { /** - * Provide an array of nodes to render + * Provide a nested array of nodes to render * @default [] */ nodes?: Array; @@ -94,6 +94,11 @@ export default class TreeView extends SvelteComponentTyped< */ collapseAll: () => void; + /** + * Create a nested array from a flat array + */ + toHierarchy: (flatArray: TreeNode[] & { pid?: any }[]) => TreeNode[]; + /** * Programmatically expand a subset of nodes. * Expands all nodes if no argument is provided diff --git a/types/TreeView/index.d.ts b/types/TreeView/index.d.ts new file mode 100644 index 0000000000..52e3204e61 --- /dev/null +++ b/types/TreeView/index.d.ts @@ -0,0 +1,2 @@ +export { default as TreeView } from "./TreeView.svelte"; +export { toHierarchy } from "./treeview"; diff --git a/types/TreeView/treeview.d.ts b/types/TreeView/treeview.d.ts new file mode 100644 index 0000000000..9506bbdc5e --- /dev/null +++ b/types/TreeView/treeview.d.ts @@ -0,0 +1,9 @@ +import { type TreeNode } from "./TreeView.svelte"; +/** + * Create a nested array from a flat array + */ +export function toHierarchy( + flatArray: TreeNode[] & { pid?: any }[], +): TreeNode[]; + +export default toHierarchy; diff --git a/types/index.d.ts b/types/index.d.ts index 1174ce26c1..d48b80fcb6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -143,6 +143,7 @@ export { default as TooltipFooter } from "./Tooltip/TooltipFooter.svelte"; export { default as TooltipDefinition } from "./TooltipDefinition/TooltipDefinition.svelte"; export { default as TooltipIcon } from "./TooltipIcon/TooltipIcon.svelte"; export { default as TreeView } from "./TreeView/TreeView.svelte"; +export { default as toHierarchy } from "./TreeView/treeview"; export { default as Truncate } from "./Truncate/Truncate.svelte"; export { default as truncate } from "./Truncate/truncate"; export { default as Header } from "./UIShell/Header.svelte";