// Reusable file listing table shared by the Local and Remote panes — mirrors // the old src/ui/panels/file_pane module (table.rs/row.rs/sort.rs collapsed // into one file since there's no split-module pressure in JS). import { t } from "../i18n.js"; import { iconMarkup, fileIconFor } from "../icons.js"; import { formatSize, formatTime } from "../format.js"; import { makeDraggable, makeDropTarget } from "../dnd.js"; import { showContextMenu } from "./contextMenu.js"; import { escapeHtml } from "../dom.js"; function sortEntries(entries, sort) { const dotdotIdx = entries.findIndex((e) => e.name === ".."); const dotdot = dotdotIdx >= 0 ? entries[dotdotIdx] : null; const rest = entries.filter((e) => e.name !== ".."); rest.sort((a, b) => { const aDir = a.kind === "dir"; const bDir = b.kind === "dir"; if (aDir !== bDir) return aDir ? -1 : 1; let cmp = 0; switch (sort.col) { case "size": cmp = (a.size ?? 0) - (b.size ?? 0); break; case "kind": cmp = a.kind.localeCompare(b.kind); break; case "modified": cmp = (a.modified ?? 0) - (b.modified ?? 0); break; default: cmp = a.name.toLowerCase().localeCompare(b.name.toLowerCase()); } return sort.dir === "desc" ? -cmp : cmp; }); return dotdot ? [dotdot, ...rest] : rest; } function rowIcon(entry) { if (entry.name === "..") return ["arrowUp", "icon--inactive"]; if (entry.kind === "dir") return ["folder", "icon--important"]; if (entry.kind === "symlink") return ["link", "icon--important"]; return fileIconFor(entry.name); } function typeLabel(entry) { if (entry.kind === "dir") return t("panels.typeDir"); if (entry.kind === "symlink") return t("panels.typeLink"); return t("panels.typeFile"); } function sortHeaderHtml(label, col, sort) { const active = sort.col === col; const arrow = active ? (sort.dir === "asc" ? " ↑" : " ↓") : ""; return ``; } /** * opts: * - entries: FileEntry[] * - selected: string[] (names of selected entries) * - anchor: string|null (name shift-click range selection extends from) * - sort: { col, dir } * - onSort(nextSort) * - onSelect(names, anchor) * - onOpen(name) * - dragPayload(entry) => payload|null * - onDropOnEntry(entry, payload) only called for dir/".." rows * - onHoverOpen(entry) spring-loaded open: fires after a drag lingers on * a dir/".." row for HOVER_OPEN_DELAY, without * ending the drag -- only called for dir/".." rows * - contextMenuItems(entry, selectedEntries) => menu items for showContextMenu, or null to skip */ export function renderFileTable(container, opts) { const scrollPos = container.querySelector(".file-table-body")?.scrollTop ?? 0; container.innerHTML = ""; container.classList.add("file-table-wrap"); const header = document.createElement("div"); header.className = "file-table-header"; header.innerHTML = ` ${sortHeaderHtml(t("panels.colName"), "name", opts.sort)} ${sortHeaderHtml(t("panels.colModified"), "modified", opts.sort)} ${sortHeaderHtml(t("panels.colType"), "kind", opts.sort)} ${sortHeaderHtml(t("panels.colSize"), "size", opts.sort)} `; header.querySelectorAll(".sort-header").forEach((btn) => { btn.addEventListener("click", () => { const col = btn.dataset.col; const next = opts.sort.col === col ? { col, dir: opts.sort.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }; opts.onSort(next); }); }); container.appendChild(header); const body = document.createElement("div"); body.className = "file-table-body"; container.appendChild(body); const sorted = sortEntries(opts.entries, opts.sort); const selectableNames = sorted.filter((e) => e.name !== "..").map((e) => e.name); for (const entry of sorted) { const row = document.createElement("div"); row.className = "file-row"; if (opts.selected.includes(entry.name)) row.classList.add("selected"); const [iconName, iconColor] = rowIcon(entry); const nameCol = entry.name === ".." ? "text-hint" : "text-primary"; row.innerHTML = `