table.ts (1855B)
1 export interface Column<T> { 2 key: string; 3 label: string; 4 render: (item: T) => string; 5 sortValue?: (item: T) => number | string; 6 } 7 8 export function renderSortableTable<T>( 9 items: T[], 10 columns: Column<T>[], 11 onRowClick: (item: T) => void, 12 sortKey = 'score', 13 sortAsc = false, 14 ): HTMLElement { 15 const wrap = document.createElement('div'); 16 wrap.className = 'table-wrap'; 17 18 let currentSort = sortKey; 19 let asc = sortAsc; 20 21 function render() { 22 const sorted = [...items]; 23 const col = columns.find(c => c.key === currentSort); 24 if (col?.sortValue) { 25 sorted.sort((a, b) => { 26 const va = col.sortValue!(a); 27 const vb = col.sortValue!(b); 28 if (typeof va === 'number' && typeof vb === 'number') return asc ? va - vb : vb - va; 29 return asc ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va)); 30 }); 31 } 32 33 wrap.innerHTML = `<table> 34 <thead><tr>${columns.map(c => 35 `<th data-key="${c.key}" class="${c.key === currentSort ? 'sorted' + (asc ? ' asc' : '') : ''}">${c.label}</th>` 36 ).join('')}</tr></thead> 37 <tbody>${sorted.map((item, i) => 38 `<tr data-idx="${i}">${columns.map(c => `<td${c.key === 'score' ? ' class="score"' : ''}>${c.render(item)}</td>`).join('')}</tr>` 39 ).join('')}</tbody> 40 </table>`; 41 42 wrap.querySelectorAll('thead th').forEach(th => { 43 th.addEventListener('click', () => { 44 const key = (th as HTMLElement).dataset.key!; 45 if (currentSort === key) asc = !asc; 46 else { currentSort = key; asc = false; } 47 render(); 48 }); 49 }); 50 51 wrap.querySelectorAll('tbody tr').forEach(tr => { 52 tr.addEventListener('click', () => { 53 const idx = parseInt((tr as HTMLElement).dataset.idx!); 54 onRowClick(sorted[idx]); 55 }); 56 }); 57 } 58 59 render(); 60 return wrap; 61 }