90 lines
3.7 KiB
Markdown
90 lines
3.7 KiB
Markdown
# tp-tree-nav
|
||
|
||
`tp-tree-nav` is a low-level, virtualized tree renderer for Lit. It is intentionally “dumb”: it only renders what it is given and emits events. Parents own all state (expanded/selected/custom), perform mutations, and pass updated data back in. This makes it a flexible foundation for building higher-level trees (e.g., VS Code–style explorer, Git ignore/selected indicators, custom icon trees).
|
||
|
||
## Key ideas
|
||
- **Multiple roots**: pass a `roots` array; each node may have `children`.
|
||
- **Virtualized list**: uses `@lit-labs/virtualizer` to render a flat, indented list for large trees.
|
||
- **Events only**: emits `node-click`, `node-context`, and `node-action` (for chevron toggle and context menu actions). Every event includes `originalEvent` so parents can cancel or coordinate.
|
||
- **Opt-in actions**: provide `defaultActions` (array of action objects), and per-node `actions`; node actions override defaults on the same `action` key. Use `beforeContextMenu(node, actions)` to modify or block the menu.
|
||
- **Custom render**: `renderNode(node, meta)` lets you override row rendering (e.g., icons per node type, state-based styling). Common states like `expanded`/`collapsed` are parent-managed via `node.states`.
|
||
- **Helper methods (pure)**: `getNodesWithState`, `clearState`, `applyStateIf` return data; they don’t mutate internal state—parents update `roots`.
|
||
- **Default icons**: `chevron`, `folder`, `file` are available via `tp-icon`; pass string keys or custom icons.
|
||
|
||
## Usage example
|
||
```js
|
||
import './tp-tree-nav.js';
|
||
|
||
const tree = document.querySelector('tp-tree-nav');
|
||
|
||
const roots = [
|
||
{
|
||
label: 'Project A',
|
||
slug: 'project-a',
|
||
states: ['expanded'],
|
||
icon: 'folder',
|
||
children: [
|
||
{ label: 'main.js', slug: 'main-js', icon: 'file', states: [] },
|
||
{
|
||
label: 'src',
|
||
slug: 'src',
|
||
icon: 'folder',
|
||
states: ['expanded'],
|
||
children: [
|
||
{ label: 'index.js', slug: 'index-js', icon: 'file', states: [] },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
tree.roots = roots;
|
||
tree.defaultActions = [
|
||
{ label: 'Rename', action: 'rename', icon: 'pencil' },
|
||
{ label: 'Delete', action: 'delete', icon: 'delete' },
|
||
];
|
||
|
||
tree.beforeContextMenu = (node, actions) => {
|
||
// Example: hide delete on roots
|
||
if (node.slug === 'project-a') {
|
||
return actions.filter((a) => a.action !== 'delete');
|
||
}
|
||
return actions;
|
||
};
|
||
|
||
tree.renderNode = (node, { depth, states, path, hasChildren }) => {
|
||
const selected = states.includes('selected');
|
||
return html`
|
||
<div
|
||
class="row ${selected ? 'selected' : ''}"
|
||
style="--depth: ${depth}"
|
||
@click=${(e) => tree.dispatchEvent(new CustomEvent('node-click', { detail: { node, path, originalEvent: e }, bubbles: true, composed: true, cancelable: true }))}
|
||
>
|
||
<div class="indent"></div>
|
||
${hasChildren
|
||
? html`<tp-icon class="icon" .icon=${TpTreeNav.chevron}></tp-icon>`
|
||
: html`<span class="icon" aria-hidden="true"></span>`}
|
||
<tp-icon class="icon" .icon=${tree._resolveIcon(node.icon)}></tp-icon>
|
||
<div class="label">${node.label}</div>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
// Listen to actions (toggle, context menu items, etc.)
|
||
tree.addEventListener('node-action', (e) => {
|
||
const { action, node, path } = e.detail;
|
||
if (action === 'toggle') {
|
||
// Parent mutates data: flip expanded state and reassign roots
|
||
const next = tree.applyStateIf('expanded', (n, p) => p.join('/') === path.join('/'));
|
||
tree.roots = next;
|
||
}
|
||
});
|
||
```
|
||
|
||
## Building higher-level trees
|
||
Wrap `tp-tree-nav` in a specialized component (e.g., file explorer) that:
|
||
- Tracks expansion/selection state on nodes.
|
||
- Supplies icons per node type via `renderNode` or `icon` strings.
|
||
- Defines default/context actions relevant to the domain.
|
||
- Handles action events to mutate the source data and pass updated `roots` back.
|