Files
tp-tree-nav/README.md
2025-12-15 17:08:43 +01:00

90 lines
3.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Codestyle 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 dont 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.