/** * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes * Copyright 2025 by Keenthemes Inc */ // Sorting logic for KTDataTable import { KTDataTableConfigInterface, KTDataTableSortOrderInterface, KTDataTableDataInterface, } from './types'; export interface KTDataTableSortAPI { initSort(): void; sortData( data: T[], sortField: keyof T | number, sortOrder: KTDataTableSortOrderInterface, ): T[]; toggleSortOrder( currentField: keyof T | number, currentOrder: KTDataTableSortOrderInterface, newField: keyof T | number, ): KTDataTableSortOrderInterface; setSortIcon( sortField: keyof T, sortOrder: KTDataTableSortOrderInterface, ): void; } export function createSortHandler( config: KTDataTableConfigInterface, theadElement: HTMLTableSectionElement, getState: () => { sortField: keyof T | number; sortOrder: KTDataTableSortOrderInterface; }, setState: ( field: keyof T | number, order: KTDataTableSortOrderInterface, ) => void, fireEvent: (eventName: string, eventData?: object) => void, dispatchEvent: (eventName: string, eventData?: object) => void, updateData: () => void, ): KTDataTableSortAPI { // Helper to compare values for sorting (string) function compareValues( a: unknown, b: unknown, sortOrder: KTDataTableSortOrderInterface, ): number { const aText = String(a).replace(/<[^>]*>| /g, ''); const bText = String(b).replace(/<[^>]*>| /g, ''); return aText > bText ? sortOrder === 'asc' ? 1 : -1 : aText < bText ? sortOrder === 'asc' ? -1 : 1 : 0; } // Parse value for numeric sort: strip currency/commas, then parseFloat function parseNumeric(value: unknown): number { if (value === null || value === undefined || value === '') { return Number.NaN; } const s = String(value).replace(/[^0-9.-]/g, ''); const n = parseFloat(s); return Number.isNaN(n) ? Number.NaN : n; } // Compare two numbers; NaN sorts to the end for both asc and desc function compareNumeric( aNum: number, bNum: number, sortOrder: KTDataTableSortOrderInterface, ): number { const aNaN = Number.isNaN(aNum); const bNaN = Number.isNaN(bNum); if (aNaN && bNaN) return 0; if (aNaN) return 1; if (bNaN) return -1; if (aNum < bNum) return sortOrder === 'asc' ? -1 : 1; if (aNum > bNum) return sortOrder === 'asc' ? 1 : -1; return 0; } function getColumnDef(sortField: keyof T | number): | { sortType?: 'string' | 'numeric'; sortValue?: ( cellValue: | KTDataTableDataInterface[keyof KTDataTableDataInterface] | string, rowData: KTDataTableDataInterface, ) => number | string; } | undefined { const columns = config.columns; if (!columns) return undefined; const key = typeof sortField === 'number' ? (Object.keys(columns)[sortField] as keyof T | undefined) : sortField; return key !== undefined ? columns[key as string] : undefined; } function sortData( data: T[], sortField: keyof T | number, sortOrder: KTDataTableSortOrderInterface, ): T[] { const columnDef = getColumnDef(sortField); const sortValueFn = columnDef?.sortValue; const useNumeric = !sortValueFn && columnDef?.sortType === 'numeric'; return data.sort((a, b) => { const aRaw = a[sortField as keyof T] as unknown; const bRaw = b[sortField as keyof T] as unknown; if (typeof sortValueFn === 'function') { const aVal = sortValueFn( aRaw as | KTDataTableDataInterface[keyof KTDataTableDataInterface] | string, a as KTDataTableDataInterface, ); const bVal = sortValueFn( bRaw as | KTDataTableDataInterface[keyof KTDataTableDataInterface] | string, b as KTDataTableDataInterface, ); const aNum = typeof aVal === 'number' ? aVal : parseNumeric(aVal); const bNum = typeof bVal === 'number' ? bVal : parseNumeric(bVal); if (typeof aVal === 'number' && typeof bVal === 'number') { return compareNumeric(aNum, bNum, sortOrder); } return compareValues(aVal, bVal, sortOrder); } if (useNumeric) { const aNum = parseNumeric( aRaw as | KTDataTableDataInterface[keyof KTDataTableDataInterface] | string, ); const bNum = parseNumeric( bRaw as | KTDataTableDataInterface[keyof KTDataTableDataInterface] | string, ); return compareNumeric(aNum, bNum, sortOrder); } return compareValues(aRaw, bRaw, sortOrder); }); } function toggleSortOrder( currentField: keyof T | number, currentOrder: KTDataTableSortOrderInterface, newField: keyof T | number, ): KTDataTableSortOrderInterface { if (currentField === newField) { switch (currentOrder) { case 'asc': return 'desc'; case 'desc': return ''; default: return 'asc'; } } return 'asc'; } function setSortIcon( sortField: keyof T, sortOrder: KTDataTableSortOrderInterface, ): void { const baseClass = config.sort?.classes?.base || ''; const sortClass = sortOrder ? sortOrder === 'asc' ? config.sort?.classes?.asc || '' : config.sort?.classes?.desc || '' : ''; // Clear all headers: remove sort state so only the active column shows highlighted arrow const allTh = theadElement.querySelectorAll('th'); allTh.forEach((header) => { const el = header as HTMLElement; el.setAttribute('aria-sort', 'none'); const sortElement = header.querySelector(`.${baseClass}`) as HTMLElement; if (sortElement) { sortElement.className = baseClass; } }); // Apply sort state to the active column so table.css [aria-sort='asc'] / [aria-sort='desc'] can highlight the arrow const th = typeof sortField === 'number' ? allTh[sortField] : (theadElement.querySelector( `th[data-kt-datatable-column="${String(sortField)}"], th[data-kt-datatable-column-sort="${String(sortField)}"]`, ) as HTMLElement); if (th) { const sortElement = th.querySelector(`.${baseClass}`) as HTMLElement; if (sortElement) { sortElement.className = `${baseClass} ${sortClass}`.trim(); } if (sortOrder) { th.setAttribute('aria-sort', sortOrder); } else { th.setAttribute('aria-sort', 'none'); } } } function initSort(): void { if (!theadElement) return; // Set the initial sort icon setSortIcon(getState().sortField as keyof T, getState().sortOrder); // Get all the table headers const headers = Array.from(theadElement.querySelectorAll('th')); headers.forEach((header) => { // If the sort class is not found, it's not a sortable column if (!header.querySelector(`.${config.sort?.classes?.base}`)) return; // Check if sorting is disabled for this column const sortDisabled = header.getAttribute('data-kt-datatable-column-sort') === 'false'; if (sortDisabled) return; const sortAttribute = header.getAttribute('data-kt-datatable-column-sort') || header.getAttribute('data-kt-datatable-column'); const sortField = sortAttribute ? (sortAttribute as keyof T) : (header.cellIndex as keyof T); header.addEventListener('click', () => { const state = getState(); const sortOrder = toggleSortOrder( state.sortField, state.sortOrder, sortField, ); setSortIcon(sortField, sortOrder); setState(sortField, sortOrder); fireEvent('sort', { field: sortField, order: sortOrder }); dispatchEvent('sort', { field: sortField, order: sortOrder }); updateData(); }); }); } return { initSort, sortData, toggleSortOrder, setSortIcon }; }