155 lines
6.8 KiB
Markdown
155 lines
6.8 KiB
Markdown
# tp-tree-nav
|
|
|
|
`tp-tree-nav` is a low-level, virtualized tree renderer for Lit. It renders what it is given and emits events; parents own state. You can wrap it directly or extend it to add opinionated behavior (selection, expansion, inline actions, empty states, etc.).
|
|
|
|
## 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, inline actions, and context menu actions). Each event carries `originalEvent` so parents can cancel or coordinate.
|
|
- **Opt-in actions**: provide `defaultActions` 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 (icons per node type, state-based styling). Common states like `expanded`/`selected` are parent-managed via `node.states` or managed mode.
|
|
- **Helpers (pure)**: `getNodesWithState`, `clearState`, `applyStateIf` return new data; parents reassign `roots`.
|
|
- **Default icons**: `chevron`, `folder`, `file` are available via `tp-icon`; pass string keys or custom icons.
|
|
|
|
## Quick example (wrap directly)
|
|
```js
|
|
import './tp-tree-nav.js';
|
|
|
|
const tree = document.querySelector('tp-tree-nav');
|
|
|
|
tree.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.defaultActions = [
|
|
{ label: 'Rename', action: 'rename', icon: 'pencil' },
|
|
{ label: 'Delete', action: 'delete', icon: 'delete' },
|
|
];
|
|
|
|
tree.beforeContextMenu = (node, actions) =>
|
|
node.slug === 'project-a' ? actions.filter((a) => a.action !== 'delete') : actions;
|
|
|
|
// simple toggle handler
|
|
const targetPath = (path) => path.join('/');
|
|
tree.addEventListener('node-action', (e) => {
|
|
if (e.detail.action !== 'toggle') return;
|
|
const target = targetPath(e.detail.path);
|
|
tree.roots = tree.applyStateIf('expanded', (_n, p) => targetPath(p) === target);
|
|
});
|
|
```
|
|
|
|
## Two ways to use it
|
|
|
|
### A) Wrap directly (keep tp-tree-nav “dumb”)
|
|
- Manage all state yourself; set `roots` and optional `defaultActions`/`actions`.
|
|
- Listen to `node-action` (chevron/double-click toggles, inline actions, context actions) and `node-click`, mutate your data, then reassign `roots`.
|
|
- Use `renderNode` for custom rows; `renderEmpty` for empty states.
|
|
- Use helpers (`applyStateIf`, `clearState`, `getNodesWithState`) for immutable transforms/queries.
|
|
|
|
### B) Extend tp-tree-nav (add opinionated behavior)
|
|
Leverage managed mode and hooks in a subclass:
|
|
|
|
```js
|
|
import { TpTreeNav } from './tp-tree-nav.js';
|
|
|
|
class MyTree extends TpTreeNav {
|
|
constructor() {
|
|
super();
|
|
this.manageState = true; // base tracks expand/select
|
|
this.autoExpandNew = false; // set true to auto-expand unseen paths
|
|
this.selectionState = 'selected'; // state name for selection
|
|
this.expandOnDoubleClick = true; // toggle expand via double-click
|
|
this.multiSelect = false; // enable multi-select if needed
|
|
this.showActions = true; // render inline actions when present
|
|
this.renderEmpty = () => html`<div class="empty">No items</div>`;
|
|
}
|
|
|
|
set data(items) {
|
|
this.items = items; // raw items with slug/label/icon/children/actions
|
|
}
|
|
|
|
_renderRow(item, meta) {
|
|
const { node } = item;
|
|
const selected = meta.states.includes(this.selectionState);
|
|
return html`
|
|
<div
|
|
class="row ${selected ? 'selected' : ''}"
|
|
style="--depth:${meta.depth}"
|
|
@click=${(e) => this._onRowClick(item, e)}
|
|
>
|
|
<div class="indent"></div>
|
|
<div
|
|
class="chevron-btn ${meta.expanded ? 'expanded' : ''}"
|
|
?hidden=${!meta.hasChildren}
|
|
@click=${(e) => this._onChevronClick(item, e)}
|
|
>
|
|
<tp-icon class="icon" .icon=${TpTreeNav.chevron}></tp-icon>
|
|
</div>
|
|
<tp-icon class="icon" .icon=${this._resolveIcon(node.icon)}></tp-icon>
|
|
<div class="label">${node.label}</div>
|
|
${node.actions?.length
|
|
? html`<div class="actions">${node.actions.map((action) => html`
|
|
<tp-icon
|
|
class="action-icon"
|
|
.icon=${this._resolveIcon(action.icon)}
|
|
@click=${(e) => this._onInlineAction(item, action, e)}
|
|
></tp-icon>
|
|
`)}</div>`
|
|
: null}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('my-tree', MyTree);
|
|
```
|
|
|
|
### Key props when extending
|
|
- `items`: raw data (`slug`, `label`, `icon`, `children`, optional `actions`).
|
|
- `manageState`: base manages expand/select when true.
|
|
- `expandedPaths` / `selectedPaths`: arrays or Sets to seed/restore state.
|
|
- `selectionState`: state name for selection (default `'selected'`).
|
|
- `multiSelect`: allow multiple selections.
|
|
- `expandOnDoubleClick`: toggle expansion on double-click (uses click detail).
|
|
- `autoExpandNew`: auto-expand unseen nodes when first seen (default false).
|
|
- `applyStates`: `(node, pathParts, states) => extraStates[]` to add custom states.
|
|
- `applySelection`: override selection logic `(node, pathParts, states, originalEvent) => nextStates`.
|
|
- `applyToggle`: override expand/collapse logic `(node, pathParts, states, originalEvent) => nextStates`.
|
|
- `showActions`: render inline actions if provided.
|
|
- `renderEmpty`: custom empty renderer.
|
|
|
|
### Events (both modes)
|
|
- `node-click`: emitted on row click.
|
|
- `node-action`: emitted for toggles (`action: 'toggle'`, source chevron/double-click), inline actions, and context menu actions.
|
|
- `node-context`: before showing context menu; `preventDefault` to cancel.
|
|
- `node-drop`: emitted on valid drop (`detail: { source, target, position }`).
|
|
|
|
### Helpers (both modes)
|
|
- `TpTreeNav.buildTree(items, { expandedPaths, selectedPaths, selectionState, applyStates, knownPaths, autoExpandNew })` — pure helper used by managed mode; can be used externally.
|
|
- `applyStateIf(state, predicate)`, `clearState(state)`, `getNodesWithState(state)` for immutable transforms/queries.
|
|
|
|
### Drag and Drop
|
|
Enable drag and drop by setting `allowDragAndDrop = true`.
|
|
- **Validation**: Provide `canDrop(source, target, position)` to allow/deny drops. `position` is `'inside'`, `'before'`, or `'after'`.
|
|
- **Event**: Listen to `node-drop` event (`detail: { source, target, position, originalEvent }`).
|
|
|
|
### Data shape
|
|
- `slug` (unique per sibling), `label`, `icon` (string key or icon data), `children` (array), optional `actions` (`{ action, label?, icon?, tooltip? }`).
|