From 434dc523cd6a6b55b281fc02e3a8ee776f3c4238 Mon Sep 17 00:00:00 2001 From: PGray Date: Tue, 3 Mar 2026 13:18:01 +0000 Subject: [PATCH] Remove jQuery and tablesorter CDN dependencies Replace the jQuery tablesorter plugin with a lightweight vanilla TypeScript TableSorter class. This removes two external CDN script tags (jQuery 3.5.1 and jquery.tablesorter 2.31.3) from the page template and replaces the jQuery-dependent calls in MetricsTable with a self-contained implementation. The new TableSorter handles column header click sorting and correctly maintains parent-child row relationships (child-metric rows stay attached to their parent during re-sorts). Numeric columns are sorted numerically using data-text dataset attributes; string columns fall back to locale- aware string comparison. Closes #4272 --- .../detailed_results/metrics_table.tsx | 34 +++--- .../detailed_results/table_sorter.ts | 107 ++++++++++++++++++ ui/index_template.html | 6 - 3 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 ui/core/components/detailed_results/table_sorter.ts diff --git a/ui/core/components/detailed_results/metrics_table.tsx b/ui/core/components/detailed_results/metrics_table.tsx index 544ad33ee2..2315697f3e 100644 --- a/ui/core/components/detailed_results/metrics_table.tsx +++ b/ui/core/components/detailed_results/metrics_table.tsx @@ -5,8 +5,7 @@ import { EventID, TypedEvent } from '../../typed_event.js'; import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; import tippy from 'tippy.js' import { element, fragment, ref } from 'tsx-vanilla' - -declare var $: any; +import { TableSorter } from './table_sorter.js'; export enum ColumnSortType { None, @@ -32,7 +31,8 @@ export abstract class MetricsTable extends ResultComponent { private readonly columnConfigs: Array>; protected readonly tableElem: HTMLElement; - protected readonly bodyElem: HTMLElement; + protected readonly bodyElem: HTMLTableSectionElement; + private readonly sorter: TableSorter; readonly onUpdate = new TypedEvent('MetricsTableUpdate'); @@ -50,10 +50,10 @@ export abstract class MetricsTable extends ResultComponent { ); - this.tableElem = this.rootElem.getElementsByClassName('metrics-table')[0] as HTMLTableSectionElement; - this.bodyElem = this.rootElem.getElementsByClassName('metrics-table-body')[0] as HTMLElement; + this.tableElem = this.rootElem.querySelector('.metrics-table')!; + this.bodyElem = this.rootElem.querySelector('.metrics-table-body')!; - const headerRowElem = this.rootElem.getElementsByClassName('metrics-table-header-row')[0] as HTMLElement; + const headerRowElem = this.rootElem.querySelector('.metrics-table-header-row')!; this.columnConfigs.forEach(columnConfig => { const headerCell = document.createElement('th'); headerCell.classList.add('metrics-table-header-cell'); @@ -73,12 +73,15 @@ export abstract class MetricsTable extends ResultComponent { headerRowElem.appendChild(headerCell); }); - const sortList = this.columnConfigs - .map((config, i) => [i, config.sort == ColumnSortType.Ascending ? 0 : 1]) - .filter(sortData => this.columnConfigs[sortData[0]].sort); - $(this.tableElem).tablesorter({ - sortList: sortList, - cssChildRow: 'child-metric', + const sortCol = this.columnConfigs.findIndex(v => !!v.sort); + + this.sorter = new TableSorter({ + tableHead: headerRowElem, + tableBody: this.bodyElem, + dataSetKey: 'text', + childRowClass: 'child-metric', + defaultSortCol: sortCol !== -1 ? sortCol : 0, + defaultSortDesc: sortCol !== -1 && this.columnConfigs[sortCol].sort == ColumnSortType.Descending, }); } @@ -101,6 +104,9 @@ export abstract class MetricsTable extends ResultComponent { this.columnConfigs.forEach(columnConfig => { const cellElem = document.createElement('td'); + if (columnConfig.getValue) { + cellElem.dataset.text = String(columnConfig.getValue(metric)); + } if (columnConfig.columnClass) { cellElem.classList.add(columnConfig.columnClass); } @@ -128,7 +134,7 @@ export abstract class MetricsTable extends ResultComponent { return; } - // Manually sort because tablesorter doesn't let us apply sorting to child rows. + // Manually sort because the sorter doesn't apply sorting to child rows. this.sortMetrics(metrics); const mergedMetrics = this.mergeMetrics(metrics); @@ -162,7 +168,7 @@ export abstract class MetricsTable extends ResultComponent { } groupedMetrics.forEach(group => this.addGroup(group)); - $(this.tableElem).trigger('update'); + this.sorter.update(); this.onUpdate.emit(resultData.eventID); } diff --git a/ui/core/components/detailed_results/table_sorter.ts b/ui/core/components/detailed_results/table_sorter.ts new file mode 100644 index 0000000000..25d97c055b --- /dev/null +++ b/ui/core/components/detailed_results/table_sorter.ts @@ -0,0 +1,107 @@ +type TableSorterRowData = { + readonly values: ReadonlyArray; + readonly rowElement: HTMLTableRowElement; +}; + +type TableSorterConfig = { + tableHead: HTMLTableRowElement; + tableBody: HTMLTableSectionElement; + dataSetKey: string; + childRowClass: string; + defaultSortCol: number; + defaultSortDesc: boolean; +}; + +export class TableSorter { + private readonly cfg: Readonly; + private readonly rowData: Array }> = []; + private sortCol = -1; + private sortDesc: Array; + + constructor(config: TableSorterConfig) { + if (config.tableHead.cells[config.defaultSortCol] === undefined) + throw new Error('Default sort column must be a valid header cell index!'); + + this.cfg = config; + + this.sortCol = this.cfg.defaultSortCol; + this.sortDesc = Array(config.tableHead.cells.length).fill(true); + this.sortDesc[config.defaultSortCol] = config.defaultSortDesc; + + Array.from(config.tableHead.cells).forEach((cell, i) => { + cell.addEventListener('click', () => this.setSort(i)); + }); + } + + private sortFunc = (a: TableSorterRowData, b: TableSorterRowData) => { + const aValue = a.values[this.sortCol]; + const bValue = b.values[this.sortCol]; + const asc = !this.sortDesc[this.sortCol]; + if (typeof aValue === 'number' && typeof bValue === 'number') { + return asc ? aValue - bValue : bValue - aValue; + } else { + return asc + ? aValue.toString().localeCompare(bValue.toString()) + : bValue.toString().localeCompare(aValue.toString()); + } + }; + + private sort() { + if (!this.rowData.length || !(this.sortCol in this.rowData[0].values)) return; + + const sortedRowElems: Array = []; + + this.rowData.sort(this.sortFunc); + for (const row of this.rowData) { + sortedRowElems.push(row.rowElement); + if (row.children) { + row.children.sort(this.sortFunc); + sortedRowElems.push(...row.children.map(v => v.rowElement)); + } + } + + this.cfg.tableBody.replaceChildren(...sortedRowElems); + } + + /** + * Set column to sort by. If set to the current sort column the order will be reversed. + * @param column If omitted use default column. + */ + setSort(column = -1) { + if (this.sortDesc[column] === undefined) column = this.cfg.defaultSortCol; + this.sortDesc[column] = !this.sortDesc[column]; + this.sortCol = column; + this.sort(); + } + + private parseRowValues(rowElement: HTMLTableRowElement): Array { + const values: Array = []; + for (const cell of rowElement.cells) { + const val = cell.dataset[this.cfg.dataSetKey] ?? cell.innerText; + const numVal = parseFloat(val); + values.push(!isNaN(numVal) ? numVal : val); + } + return values; + } + + /** + * Update internal data structure for changed table data. + */ + update() { + this.rowData.length = 0; + + for (const rowElement of this.cfg.tableBody.rows) { + const values = this.parseRowValues(rowElement); + if (!rowElement.classList.contains(this.cfg.childRowClass)) { + this.rowData.push({ values, rowElement }); + } else { + const parentData = this.rowData[this.rowData.length - 1]; + if (!parentData) throw new Error('Child row has no parent!'); + if (!parentData.children) parentData.children = []; + parentData.children.push({ values, rowElement }); + } + } + + this.sort(); + } +} diff --git a/ui/index_template.html b/ui/index_template.html index 415de489ed..f7ab6b1978 100644 --- a/ui/index_template.html +++ b/ui/index_template.html @@ -19,12 +19,6 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - -