2025-12-17 22:42:25 +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-16 13:57:23 +01:00
2025-12-17 22:42:25 +01:00
2025-12-17 22:17:40 +01:00
2025-12-17 22:42:25 +01:00

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)

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:

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