Something went wrong. Try again.
My theme for forester (+plugins)
Something went wrong. Try again.
forest-theme tsvtable.js
5.0 kB · 203 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204class TsvTable extends HTMLElement { static get observedAttributes() { return ["src"]; }
constructor() { super(); this.attachShadow({ mode: "open" });
// Styles const style = document.createElement("style"); style.textContent = ` :host { display: block; font-family: sans-serif; } input[type="search"] { margin-bottom: 8px; padding: 4px 6px; width: 100%; box-sizing: border-box; font-size: 14px; } table { border-collapse: collapse; width: 100%; } th, td { padding: 4px 8px; border: 1px solid #ccc; } th { cursor: pointer; background: #f5f5f5; user-select: none; } .sort-btn { cursor: pointer; font-size: 0.8em; margin-left: 6px; opacity: 0.6; } .sort-btn:hover { opacity: 1; } .no-results { padding: 8px; color: #777; font-style: italic; } `; this.shadowRoot.appendChild(style);
// Search box this.searchInput = document.createElement("input"); this.searchInput.type = "search"; this.searchInput.placeholder = "Search…"; this.searchInput.addEventListener("input", () => this.applyFilter()); this.shadowRoot.appendChild(this.searchInput);
// Table this.table = document.createElement("table"); this.shadowRoot.appendChild(this.table);
// State this.rows = []; // raw TSV data this.filteredRows = []; // after search this.sortState = {}; // column → "asc"/"desc" }
connectedCallback() { if (this.getAttribute("src")) { this.loadTsv(); } }
attributeChangedCallback(name, oldValue, newValue) { if (name === "src" && newValue !== oldValue && this.isConnected) { this.loadTsv(); } }
async loadTsv() { try { const url = this.getAttribute("src"); const response = await fetch(url); const text = await response.text(); this.rows = this.parseTsv(text); this.filteredRows = this.rows; // no filter yet this.renderTable(this.filteredRows); } catch (err) { console.error("TSV load error:", err); this.table.innerHTML = "<tr><td>Error loading TSV</td></tr>"; } }
parseTsv(text) { return text .trim() .split("\n") .map(row => row.split("\t").map(cell => cell.trim())); }
applyFilter() { const query = this.searchInput.value.toLowerCase().trim();
if (!query) { this.filteredRows = this.rows; } else { const header = this.rows[0]; const body = this.rows.slice(1);
const filteredBody = body.filter(row => row.some(cell => cell.toLowerCase().includes(query)) );
this.filteredRows = [header, ...filteredBody]; }
this.renderTable(this.filteredRows); }
sortByColumn(index) { const header = this.filteredRows[0]; const body = this.filteredRows.slice(1);
// Toggle sort direction const current = this.sortState[index] || "none"; const direction = current === "asc" ? "desc" : "asc"; this.sortState[index] = direction;
body.sort((a, b) => { const A = a[index]; const B = b[index];
const numA = parseFloat(A); const numB = parseFloat(B);
if (!isNaN(numA) && !isNaN(numB)) { return direction === "asc" ? numA - numB : numB - numA; }
return direction === "asc" ? A.localeCompare(B) : B.localeCompare(A); });
this.filteredRows = [header, ...body]; this.renderTable(this.filteredRows); }
renderTable(rows) { if (!rows.length) { this.table.innerHTML = "<tr><td class='no-results'>No results</td></tr>"; return; }
const header = rows[0]; const body = rows.slice(1);
this.table.innerHTML = "";
// --- Header --- const thead = document.createElement("thead"); const headerRow = document.createElement("tr");
header.forEach((h, index) => { const th = document.createElement("th");
const label = document.createElement("span"); label.textContent = h;
const btn = document.createElement("span"); btn.textContent = "↕"; btn.className = "sort-btn"; btn.addEventListener("click", () => this.sortByColumn(index));
th.appendChild(label); th.appendChild(btn);
headerRow.appendChild(th); });
thead.appendChild(headerRow); this.table.appendChild(thead);
// --- Body --- const tbody = document.createElement("tbody"); body.forEach(row => { const tr = document.createElement("tr"); row.forEach(cell => { const td = document.createElement("td"); td.textContent = cell; tr.appendChild(td); }); tbody.appendChild(tr); });
this.table.appendChild(tbody); }}
customElements.define("tsv-table", TsvTable);