My theme for forester (+plugins)
at main 203 lines 5.0 kB view raw
1class TsvTable extends HTMLElement { 2 static get observedAttributes() { 3 return ["src"]; 4 } 5 6 constructor() { 7 super(); 8 this.attachShadow({ mode: "open" }); 9 10 // Styles 11 const style = document.createElement("style"); 12 style.textContent = ` 13 :host { 14 display: block; 15 font-family: sans-serif; 16 } 17 input[type="search"] { 18 margin-bottom: 8px; 19 padding: 4px 6px; 20 width: 100%; 21 box-sizing: border-box; 22 font-size: 14px; 23 } 24 table { 25 border-collapse: collapse; 26 width: 100%; 27 } 28 th, td { 29 padding: 4px 8px; 30 border: 1px solid #ccc; 31 } 32 th { 33 cursor: pointer; 34 background: #f5f5f5; 35 user-select: none; 36 } 37 .sort-btn { 38 cursor: pointer; 39 font-size: 0.8em; 40 margin-left: 6px; 41 opacity: 0.6; 42 } 43 .sort-btn:hover { 44 opacity: 1; 45 } 46 .no-results { 47 padding: 8px; 48 color: #777; 49 font-style: italic; 50 } 51 `; 52 this.shadowRoot.appendChild(style); 53 54 // Search box 55 this.searchInput = document.createElement("input"); 56 this.searchInput.type = "search"; 57 this.searchInput.placeholder = "Search…"; 58 this.searchInput.addEventListener("input", () => this.applyFilter()); 59 this.shadowRoot.appendChild(this.searchInput); 60 61 // Table 62 this.table = document.createElement("table"); 63 this.shadowRoot.appendChild(this.table); 64 65 // State 66 this.rows = []; // raw TSV data 67 this.filteredRows = []; // after search 68 this.sortState = {}; // column → "asc"/"desc" 69 } 70 71 connectedCallback() { 72 if (this.getAttribute("src")) { 73 this.loadTsv(); 74 } 75 } 76 77 attributeChangedCallback(name, oldValue, newValue) { 78 if (name === "src" && newValue !== oldValue && this.isConnected) { 79 this.loadTsv(); 80 } 81 } 82 83 async loadTsv() { 84 try { 85 const url = this.getAttribute("src"); 86 const response = await fetch(url); 87 const text = await response.text(); 88 this.rows = this.parseTsv(text); 89 this.filteredRows = this.rows; // no filter yet 90 this.renderTable(this.filteredRows); 91 } catch (err) { 92 console.error("TSV load error:", err); 93 this.table.innerHTML = "<tr><td>Error loading TSV</td></tr>"; 94 } 95 } 96 97 parseTsv(text) { 98 return text 99 .trim() 100 .split("\n") 101 .map(row => row.split("\t").map(cell => cell.trim())); 102 } 103 104 applyFilter() { 105 const query = this.searchInput.value.toLowerCase().trim(); 106 107 if (!query) { 108 this.filteredRows = this.rows; 109 } else { 110 const header = this.rows[0]; 111 const body = this.rows.slice(1); 112 113 const filteredBody = body.filter(row => 114 row.some(cell => cell.toLowerCase().includes(query)) 115 ); 116 117 this.filteredRows = [header, ...filteredBody]; 118 } 119 120 this.renderTable(this.filteredRows); 121 } 122 123 sortByColumn(index) { 124 const header = this.filteredRows[0]; 125 const body = this.filteredRows.slice(1); 126 127 // Toggle sort direction 128 const current = this.sortState[index] || "none"; 129 const direction = current === "asc" ? "desc" : "asc"; 130 this.sortState[index] = direction; 131 132 body.sort((a, b) => { 133 const A = a[index]; 134 const B = b[index]; 135 136 const numA = parseFloat(A); 137 const numB = parseFloat(B); 138 139 if (!isNaN(numA) && !isNaN(numB)) { 140 return direction === "asc" ? numA - numB : numB - numA; 141 } 142 143 return direction === "asc" 144 ? A.localeCompare(B) 145 : B.localeCompare(A); 146 }); 147 148 this.filteredRows = [header, ...body]; 149 this.renderTable(this.filteredRows); 150 } 151 152 renderTable(rows) { 153 if (!rows.length) { 154 this.table.innerHTML = "<tr><td class='no-results'>No results</td></tr>"; 155 return; 156 } 157 158 const header = rows[0]; 159 const body = rows.slice(1); 160 161 this.table.innerHTML = ""; 162 163 // --- Header --- 164 const thead = document.createElement("thead"); 165 const headerRow = document.createElement("tr"); 166 167 header.forEach((h, index) => { 168 const th = document.createElement("th"); 169 170 const label = document.createElement("span"); 171 label.textContent = h; 172 173 const btn = document.createElement("span"); 174 btn.textContent = "↕"; 175 btn.className = "sort-btn"; 176 btn.addEventListener("click", () => this.sortByColumn(index)); 177 178 th.appendChild(label); 179 th.appendChild(btn); 180 181 headerRow.appendChild(th); 182 }); 183 184 thead.appendChild(headerRow); 185 this.table.appendChild(thead); 186 187 // --- Body --- 188 const tbody = document.createElement("tbody"); 189 body.forEach(row => { 190 const tr = document.createElement("tr"); 191 row.forEach(cell => { 192 const td = document.createElement("td"); 193 td.textContent = cell; 194 tr.appendChild(td); 195 }); 196 tbody.appendChild(tr); 197 }); 198 199 this.table.appendChild(tbody); 200 } 201} 202 203customElements.define("tsv-table", TsvTable);