import { adopt, define, sheet } from "@nibling/web/internals"; import "@nibling/web/tree"; import type { TreeActivateEvent, TreeElement } from "@nibling/web/tree"; import { treeFromPaths } from "@nibling/web/tree/model"; import type { Files } from "../files.ts"; import { stacks, type PanelId } from "../shell/layout.ts"; import type { Unsubscribe, Workbench } from "../shell/workbench.ts"; const explorerSheet = sheet(` :host { display: flex; container: nib-explorer / size; min-width: 0; min-height: 0; } ::slotted(nib-tree) { flex: 1; min-width: 0; } `); const hidden = (segment: string) => segment.startsWith("."); /** The explorer: a `` over `files` whose selection follows the editor the workbench shows. */ export class ExplorerElement extends HTMLElement { readonly #tree: TreeElement = document.createElement("nib-tree"); #workbench: Workbench | undefined; #listing = ""; constructor() { super(); const root = this.attachShadow({ mode: "open", delegatesFocus: true }); adopt(root, explorerSheet); root.append(document.createElement("slot")); this.#tree.label = "Files"; this.#tree.actions = (node) => { if (node.children || !node.id.endsWith(".html")) return; const run = document.createElement("button"); run.textContent = "▶"; run.ariaLabel = `Preview ${node.id}`; run.addEventListener("click", () => this.#workbench?.open({ kind: "preview", input: { path: node.id } })); return [run]; }; this.#tree.addEventListener("activate", (event) => { this.#workbench?.open({ kind: "editor", input: { path: (event as TreeActivateEvent).node.id } }); }); } connectedCallback(): void { if (!this.#tree.isConnected) this.append(this.#tree); } override focus(options?: FocusOptions): void { this.#tree.focus(options); } connect(files: Files, workbench: Workbench): Unsubscribe { this.#workbench = workbench; const sync = () => { const paths = files.list(); // Files reports every write; the tree only changes when the set of paths does. const next = paths.join("\0"); if (next === this.#listing) return; this.#listing = next; this.#tree.nodes = treeFromPaths(paths, { hidden }); }; const follow = () => { const path = shownEditor(workbench); if (path === undefined || path === this.#tree.selected) return; this.#tree.selected = path; this.#tree.reveal(path); }; sync(); follow(); const unwatch = files.watch(sync); const unsubscribe = workbench.subscribe(follow); return () => { unwatch(); unsubscribe(); }; } } /** The editor in the focused stack, or else in the first stack showing one. */ function shownEditor(wb: Workbench): string | undefined { const { layout } = wb; const editorIn = (id: PanelId | null) => { const panel = id ? layout.panels[id] : undefined; return panel?.kind === "editor" ? panel.input.path : undefined; }; const all = stacks(layout.root); const focused = all.find((stack) => stack.id === layout.focus); return editorIn(focused?.active ?? null) ?? all.map((stack) => editorIn(stack.active)).find(Boolean); } declare global { interface HTMLElementTagNameMap { "nib-explorer": ExplorerElement; } } define("nib-explorer", ExplorerElement);