Skip to content

Commit

Permalink
feat(tree-view): add accessor to show node by id
Browse files Browse the repository at this point in the history
  • Loading branch information
metonym committed Nov 12, 2023
1 parent 417102d commit a8bb393
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 13 deletions.
27 changes: 14 additions & 13 deletions COMPONENT_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -4691,19 +4691,20 @@ export interface TreeNode {

### Props

| Prop name | Required | Kind | Reactive | Type | Default value | Description |
| :------------ | :------- | :-------------------- | :------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| expandedIds | No | <code>let</code> | Yes | <code>ReadonlyArray<TreeNodeId></code> | <code>[]</code> | Set the node ids to be expanded |
| selectedIds | No | <code>let</code> | Yes | <code>ReadonlyArray<TreeNodeId></code> | <code>[]</code> | Set the node ids to be selected |
| activeId | No | <code>let</code> | Yes | <code>TreeNodeId</code> | <code>""</code> | Set the current active node id<br />Only one node can be active |
| children | No | <code>let</code> | No | <code>Array<TreeNode></code> | <code>[]</code> | Provide an array of children nodes to render |
| size | No | <code>let</code> | No | <code>"default" &#124; "compact"</code> | <code>"default"</code> | Specify the TreeView size |
| labelText | No | <code>let</code> | No | <code>string</code> | <code>""</code> | Specify the label text |
| hideLabel | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to visually hide the label text |
| expandAll | No | <code>function</code> | No | <code>() => void</code> | <code>() => { expandedIds = [...nodeIds]; }</code> | Programmatically expand all nodes |
| collapseAll | No | <code>function</code> | No | <code>() => void</code> | <code>() => { expandedIds = []; }</code> | Programmatically collapse all nodes |
| expandNodes | No | <code>function</code> | No | <code>(filterId?: (node: TreeNode) => boolean) => void</code> | <code>() => { expandedIds = nodes .filter( (node) => filterNode(node) &#124;&#124; node.children?.some((child) => filterNode(child) && child.children) ) .map((node) => node.id); }</code> | Programmatically expand a subset of nodes.<br />Expands all nodes if no argument is provided |
| collapseNodes | No | <code>function</code> | No | <code>(filterId?: (node: TreeNode) => boolean) => void</code> | <code>() => { expandedIds = nodes .filter((node) => expandedIds.includes(node.id) && !filterNode(node)) .map((node) => node.id); }</code> | Programmatically collapse a subset of nodes.<br />Collapses all nodes if no argument is provided |
| Prop name | Required | Kind | Reactive | Type | Default value | Description |
| :------------ | :------- | :-------------------- | :------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| expandedIds | No | <code>let</code> | Yes | <code>ReadonlyArray<TreeNodeId></code> | <code>[]</code> | Set the node ids to be expanded |
| selectedIds | No | <code>let</code> | Yes | <code>ReadonlyArray<TreeNodeId></code> | <code>[]</code> | Set the node ids to be selected |
| activeId | No | <code>let</code> | Yes | <code>TreeNodeId</code> | <code>""</code> | Set the current active node id<br />Only one node can be active |
| children | No | <code>let</code> | No | <code>Array<TreeNode></code> | <code>[]</code> | Provide an array of children nodes to render |
| size | No | <code>let</code> | No | <code>"default" &#124; "compact"</code> | <code>"default"</code> | Specify the TreeView size |
| labelText | No | <code>let</code> | No | <code>string</code> | <code>""</code> | Specify the label text |
| hideLabel | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to visually hide the label text |
| expandAll | No | <code>function</code> | No | <code>() => void</code> | <code>() => { expandedIds = [...nodeIds]; }</code> | Programmatically expand all nodes |
| collapseAll | No | <code>function</code> | No | <code>() => void</code> | <code>() => { expandedIds = []; }</code> | Programmatically collapse all nodes |
| expandNodes | No | <code>function</code> | No | <code>(filterId?: (node: TreeNode) => boolean) => void</code> | <code>() => { expandedIds = nodes .filter( (node) => filterNode(node) &#124;&#124; node.children?.some((child) => filterNode(child) && child.children) ) .map((node) => node.id); }</code> | Programmatically expand a subset of nodes.<br />Expands all nodes if no argument is provided |
| collapseNodes | No | <code>function</code> | No | <code>(filterId?: (node: TreeNode) => boolean) => void</code> | <code>() => { expandedIds = nodes .filter((node) => expandedIds.includes(node.id) && !filterNode(node)) .map((node) => node.id); }</code> | Programmatically collapse a subset of nodes.<br />Collapses all nodes if no argument is provided |
| show | No | <code>function</code> | No | <code>(id: TreeNodeId) => void</code> | <code>() => { for (const child of children) { 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]; ref?.querySelector(\`[id="${lastId}"]\`)?.focus(); // Break out of the loop if the node is found break; } } }</code> | Programmatically show a node by id.<br />If a matching node is found, it will be expanded, selected, and focused |

### Slots

Expand Down
12 changes: 12 additions & 0 deletions docs/src/COMPONENT_API.json
Original file line number Diff line number Diff line change
Expand Up @@ -14664,6 +14664,18 @@
"isRequired": false,
"constant": false,
"reactive": false
},
{
"name": "show",
"kind": "function",
"description": "Programmatically show a node by id.\nIf a matching node is found, it will be expanded, selected, and focused",
"type": "(id: TreeNodeId) => void",
"value": "() => { for (const child of children) { 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]; ref?.querySelector(`[id=\"${lastId}\"]`)?.focus(); // Break out of the loop if the node is found break; } } }",
"isFunction": true,
"isFunctionDeclaration": true,
"isRequired": false,
"constant": false,
"reactive": false
}
],
"moduleExports": [],
Expand Down
55 changes: 55 additions & 0 deletions src/TreeView/TreeView.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
<script context="module">
/**
* Depth-first search to find a node by id; returns an array
* of nodes from the initial node to the matching leaf.
* @param {TreeNode} node
* @param {TreeNodeId} id
* @returns {null | TreeNode[]}
*/
function findNodeById(node, id) {
if (node === null) return null;
if (node.id === id) return [node];
if (!Array.isArray(node.children)) {
return null;
}
for (const child of node.children) {
const nodes = findNodeById(child, id);
if (Array.isArray(nodes)) {
nodes.unshift(node);
return nodes;
}
}
return null;
}
</script>

<script>
/**
* @typedef {string | number} TreeNodeId
Expand Down Expand Up @@ -86,6 +114,33 @@
.map((node) => node.id);
}
/**
* Programmatically show a node by id.
* If a matching node is found, it will be expanded, selected, and focused
* @type {(id: TreeNodeId) => void}
*/
export function show(id) {
for (const child of children) {
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];
ref?.querySelector(`[id="${lastId}"]`)?.focus();
// Break out of the loop if the node is found
break;
}
}
}
import { createEventDispatcher, setContext, onMount } from "svelte";
import { writable } from "svelte/store";
import TreeViewNodeList from "./TreeViewNodeList.svelte";
Expand Down
1 change: 1 addition & 0 deletions tests/TreeView.test.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
treeview.collapseNodes((node) => {
return node.disabled;
});
treeview.show(1);
}
</script>

Expand Down
6 changes: 6 additions & 0 deletions types/TreeView/TreeView.svelte.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,10 @@ export default class TreeView extends SvelteComponentTyped<
* Collapses all nodes if no argument is provided
*/
collapseNodes: (filterId?: (node: TreeNode) => boolean) => void;

/**
* Programmatically show a node by id.
* If a matching node is found, it will be expanded, selected, and focused
*/
show: (id: TreeNodeId) => void;
}

0 comments on commit a8bb393

Please sign in to comment.