2025-12-16 00:50:53 +01:00
2025-10-08 21:38:41 +02:00
2025-10-08 21:38:41 +02:00
2025-10-08 21:38:41 +02:00
2025-12-15 17:08:43 +01:00
2025-12-15 17:08:43 +01:00
2025-12-15 17:08:43 +01:00
wip
2025-12-16 00:50:53 +01:00

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

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.
Description
No description provided
Readme Apache-2.0 58 KiB
Languages
JavaScript 100%