This commit is contained in:
2025-12-15 23:12:34 +01:00
parent 0d2f1fb82d
commit d121833c2a

View File

@@ -7,7 +7,7 @@ This program is available under Apache License Version 2.0
import { LitElement, html, css, svg } from 'lit'; import { LitElement, html, css, svg } from 'lit';
import { virtualize } from '@lit-labs/virtualizer/virtualize.js'; import { virtualize } from '@lit-labs/virtualizer/virtualize.js';
class TpTreeNav extends LitElement { export class TpTreeNav extends LitElement {
static get styles() { static get styles() {
return [ return [
css` css`
@@ -31,6 +31,8 @@ class TpTreeNav extends LitElement {
padding: 0 8px; padding: 0 8px;
cursor: default; cursor: default;
user-select: none; user-select: none;
gap: 5px;
width: 100%;
} }
.row:hover { .row:hover {
@@ -66,7 +68,6 @@ class TpTreeNav extends LitElement {
} }
.icon { .icon {
margin-right: 6px;
width: 18px; width: 18px;
height: 18px; height: 18px;
flex: 0 0 auto; flex: 0 0 auto;
@@ -140,6 +141,66 @@ class TpTreeNav extends LitElement {
`; `;
} }
_renderItem(item) {
const { node, depth, hasChildren, expanded } = item;
const icon = this._resolveIcon(node?.icon);
const custom = this.renderNode?.(item, {
depth,
states: Array.isArray(node?.states) ? node.states : [],
path: item.path,
hasChildren,
});
if (custom) {
return custom;
}
return html`
<div
class="row"
part="row"
style="--depth: ${depth}"
@click=${(e) => this._onRowClick(item, e)}
@contextmenu=${(e) => this._onContextMenu(item, e)}
>
<div class="indent"></div>
<div
class="chevron-btn ${expanded ? 'expanded' : ''}"
part="chevron"
?hidden=${!hasChildren}
@click=${(e) => this._onChevronClick(item, e)}
>
<tp-icon class="icon" part="chevron-icon" .icon=${TpTreeNav.chevron}></tp-icon>
</div>
${icon ? html`<tp-icon class="icon" part="icon" .icon=${icon}></tp-icon>` : html`<span class="icon" part="icon" aria-hidden="true"></span>`}
<div class="label" part="label">${node?.label ?? ''}</div>
</div>
`;
}
_renderContextMenu() {
if (!this._contextMenu) return null;
const { x, y, item, actions } = this._contextMenu;
return html`
<div class="context-menu-overlay">
<div class="context-menu" part="context-menu" style="left:${x}px; top:${y}px;">
${actions.map((action) => html`
<button
class="menu-item"
part="context-menu-item"
@click=${(e) => this._onMenuAction(action, item, e)}
>
${action?.icon ? html`<tp-icon class="menu-icon" part="context-menu-icon" .icon=${this._resolveIcon(action.icon)}></tp-icon>` : html`<span class="menu-icon" part="context-menu-icon" aria-hidden="true"></span>`}
<span>${action?.label ?? ''}</span>
</button>
`)}
</div>
</div>
`;
}
static get properties() { static get properties() {
return { return {
roots: { type: Array }, roots: { type: Array },
@@ -217,44 +278,6 @@ class TpTreeNav extends LitElement {
return results; return results;
} }
_renderItem(item) {
const { node, depth, hasChildren, expanded } = item;
const icon = this._resolveIcon(node?.icon);
const custom = this.renderNode?.(node, {
depth,
states: Array.isArray(node?.states) ? node.states : [],
path: item.path,
hasChildren,
});
if (custom) {
return custom;
}
return html`
<div
class="row"
part="row"
style="--depth: ${depth}"
@click=${(e) => this._onRowClick(item, e)}
@contextmenu=${(e) => this._onContextMenu(item, e)}
>
<div class="indent"></div>
<div
class="chevron-btn ${expanded ? 'expanded' : ''}"
part="chevron"
?hidden=${!hasChildren}
@click=${(e) => this._onChevronClick(item, e)}
>
<tp-icon class="icon" part="chevron-icon" .icon=${TpTreeNav.chevron}></tp-icon>
</div>
${icon ? html`<tp-icon class="icon" part="icon" .icon=${icon}></tp-icon>` : html`<span class="icon" part="icon" aria-hidden="true"></span>`}
<div class="label" part="label">${node?.label ?? ''}</div>
</div>
`;
}
_onRowClick(item, originalEvent) { _onRowClick(item, originalEvent) {
const ev = new CustomEvent('node-click', { const ev = new CustomEvent('node-click', {
detail: { node: item.node, path: item.path, originalEvent }, detail: { node: item.node, path: item.path, originalEvent },
@@ -272,8 +295,9 @@ class TpTreeNav extends LitElement {
async _onContextMenu(item, originalEvent) { async _onContextMenu(item, originalEvent) {
originalEvent.preventDefault(); originalEvent.preventDefault();
const contextEvent = new CustomEvent('node-context', { const contextEvent = new CustomEvent('node-context', {
detail: { node: item.node, path: item.path, originalEvent }, detail: { item, originalEvent },
bubbles: true, bubbles: true,
composed: true, composed: true,
cancelable: true, cancelable: true,
@@ -288,7 +312,7 @@ class TpTreeNav extends LitElement {
); );
const maybeModified = this.beforeContextMenu const maybeModified = this.beforeContextMenu
? this.beforeContextMenu(item.node, baseActions) ? this.beforeContextMenu(item, baseActions)
: baseActions; : baseActions;
const actions = await Promise.resolve(maybeModified); const actions = await Promise.resolve(maybeModified);
@@ -305,7 +329,7 @@ class TpTreeNav extends LitElement {
_dispatchAction(action, item, source, originalEvent) { _dispatchAction(action, item, source, originalEvent) {
const ev = new CustomEvent('node-action', { const ev = new CustomEvent('node-action', {
detail: { action, node: item.node, path: item.path, source, originalEvent }, detail: { action, item, source, originalEvent },
bubbles: true, bubbles: true,
composed: true, composed: true,
cancelable: true, cancelable: true,
@@ -313,28 +337,6 @@ class TpTreeNav extends LitElement {
this.dispatchEvent(ev); this.dispatchEvent(ev);
} }
_renderContextMenu() {
if (!this._contextMenu) return null;
const { x, y, item, actions } = this._contextMenu;
return html`
<div class="context-menu-overlay">
<div class="context-menu" part="context-menu" style="left:${x}px; top:${y}px;">
${actions.map((action) => html`
<button
class="menu-item"
part="context-menu-item"
@click=${(e) => this._onMenuAction(action, item, e)}
>
${action?.icon ? html`<tp-icon class="menu-icon" part="context-menu-icon" .icon=${this._resolveIcon(action.icon)}></tp-icon>` : html`<span class="menu-icon" part="context-menu-icon" aria-hidden="true"></span>`}
<span>${action?.label ?? ''}</span>
</button>
`)}
</div>
</div>
`;
}
_onMenuAction(action, item, originalEvent) { _onMenuAction(action, item, originalEvent) {
originalEvent.stopPropagation(); originalEvent.stopPropagation();
this._dispatchAction(action?.action, item, 'context-menu', originalEvent); this._dispatchAction(action?.action, item, 'context-menu', originalEvent);
@@ -456,17 +458,21 @@ class TpTreeNav extends LitElement {
const slug = node?.slug ?? `${index}`; const slug = node?.slug ?? `${index}`;
const nextPath = [...path, slug]; const nextPath = [...path, slug];
const mappedNode = mapper(node, nextPath) ?? node; const mappedNode = mapper(node, nextPath) ?? node;
const { nodes: childNodes, changed: childChanged } = this._mapTree( const { nodes: childNodes, changed: childChanged } = this._mapTree(
node?.children, node?.children,
mapper, mapper,
nextPath nextPath
); );
const children = childChanged ? childNodes : node?.children; const children = childChanged ? childNodes : node?.children;
const nodeChanged = mappedNode !== node || childChanged; const nodeChanged = mappedNode !== node || childChanged;
if (nodeChanged) { if (nodeChanged) {
changed = true; changed = true;
return { ...mappedNode, children }; return { ...mappedNode, children };
} }
return mappedNode; return mappedNode;
}); });
return { nodes: mapped, changed }; return { nodes: mapped, changed };