44 <table class =" afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText overflow-x-auto" >
55 <thead class =" afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText" >
66 <tr >
7- <th scope =" col" class =" px-6 py-3" ref =" headerRefs" :key =" `header-${column.fieldName}`"
7+ <th
8+ scope =" col"
9+ class =" px-6 py-3"
10+ ref =" headerRefs"
11+ :key =" `header-${column.fieldName}`"
812 v-for =" column in columns"
13+ :aria-sort =" getAriaSort(column)"
14+ :class =" { 'cursor-pointer select-none afcl-table-header-sortable': isColumnSortable(column) }"
15+ @click =" onHeaderClick(column)"
916 >
1017 <slot v-if =" $slots[`header:${column.fieldName}`]" :name =" `header:${column.fieldName}`" :column =" column" />
11-
12- <span v-else >
18+
19+ <span v-else class = " inline-flex items-center " >
1320 {{ column.label }}
21+ <span v-if =" isColumnSortable(column)" class =" text-lightTableHeadingText dark:text-darkTableHeadingText" >
22+ <!-- Unsorted indicator -->
23+ <svg v-if =" !isSorted(column)" class =" w-3 h-3 ms-1.5 opacity-30" aria-hidden =" true" xmlns =" http://www.w3.org/2000/svg" fill =" currentColor" viewBox =" 0 0 24 24" ><path d =" M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" ></path ></svg >
24+ <!-- Sorted ascending indicator -->
25+ <svg v-else-if =" currentSortDirection === 'asc'" class =" w-3 h-3 ms-1.5" fill =" currentColor" viewBox =" 0 0 24 24" ><path d =" M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 0z" ></path ></svg >
26+ <!-- Sorted descending indicator (rotated) -->
27+ <svg v-else class =" w-3 h-3 ms-1.5 rotate-180" fill =" currentColor" viewBox =" 0 0 24 24" ><path d =" M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 0z" ></path ></svg >
28+ </span >
1429 </span >
1530 </th >
1631 </tr >
141156 columns: {
142157 label: string ,
143158 fieldName: string ,
159+ sortable? : boolean ,
144160 }[],
145161 data: {
146162 [key : string ]: any ,
147- }[] | ((params : { offset: number , limit: number }) => Promise <{data: {[key : string ]: any }[], total: number }>),
163+ }[] | ((params : { offset: number , limit: number , sortField ? : string , sortDirection ? : ' asc ' | ' desc ' }) => Promise <{data: {[key : string ]: any }[], total: number }>),
148164 evenHighlights? : boolean ,
149165 pageSize? : number ,
150166 isLoading? : boolean ,
167+ sortable? : boolean , // enable/disable sorting globally
168+ defaultSortField? : string ,
169+ defaultSortDirection? : ' asc' | ' desc' ,
151170 }>(), {
152171 evenHighlights: true ,
153172 pageSize: 5 ,
173+ sortable: true ,
154174 }
155175 );
156176
163183 const isLoading = ref (false );
164184 const dataResult = ref <{data: {[key : string ]: any }[], total: number }>({data: [], total: 0 });
165185 const isAtLeastOneLoading = ref <boolean []>([false ]);
186+ const currentSortField = ref <string | undefined >(props .defaultSortField );
187+ const currentSortDirection = ref <' asc' | ' desc' >(props .defaultSortDirection ?? ' asc' );
166188
167189 onMounted (() => {
168190 refresh ();
181203 emit (' update:tableLoading' , isLoading .value || props .isLoading );
182204 });
183205
206+ watch ([() => currentSortField .value , () => currentSortDirection .value ], () => {
207+ // reset to first page on sort change
208+ if (currentPage .value !== 1 ) currentPage .value = 1 ;
209+ refresh ();
210+ emit (' update:sortField' , currentSortField .value );
211+ emit (' update:sortDirection' , currentSortField .value ? currentSortDirection .value : undefined as any );
212+ emit (' sort-change' , { field: currentSortField .value , direction: currentSortDirection .value });
213+ });
214+
184215 const totalPages = computed (() => {
185216 return dataResult .value ?.total ? Math .ceil (dataResult .value .total / props .pageSize ) : 1 ;
186217 });
196227
197228 const emit = defineEmits ([
198229 ' update:tableLoading' ,
230+ ' update:sortField' ,
231+ ' update:sortDirection' ,
232+ ' sort-change' ,
199233 ]);
200234
201235 function onPageInput(event : any ) {
231265 isLoading .value = true ;
232266 const currentLoadingIndex = currentPage .value ;
233267 isAtLeastOneLoading .value [currentLoadingIndex ] = true ;
234- const result = await props .data ({ offset: (currentLoadingIndex - 1 ) * props .pageSize , limit: props .pageSize });
268+ const result = await props .data ({
269+ offset: (currentLoadingIndex - 1 ) * props .pageSize ,
270+ limit: props .pageSize ,
271+ sortField: currentSortField .value ,
272+ sortDirection: currentSortDirection .value ,
273+ });
235274 isAtLeastOneLoading .value [currentLoadingIndex ] = false ;
236275 if (isAtLeastOneLoading .value .every (v => v === false )) {
237276 isLoading .value = false ;
240279 } else if (typeof props .data === ' object' && Array .isArray (props .data )) {
241280 const start = (currentPage .value - 1 ) * props .pageSize ;
242281 const end = start + props .pageSize ;
243- dataResult .value = { data: props .data .slice (start , end ), total: props .data .length };
282+ const total = props .data .length ;
283+ const sorted = sortArrayData (props .data , currentSortField .value , currentSortDirection .value );
284+ dataResult .value = { data: sorted .slice (start , end ), total };
244285 }
245286 }
246287
252293 }
253294 }
254295
296+ function isColumnSortable(column : { fieldName: string ; sortable? : boolean }) {
297+ return !! props .sortable && column .sortable !== false ;
298+ }
299+
300+ function isSorted(column : { fieldName: string }) {
301+ return currentSortField .value === column .fieldName ;
302+ }
303+
304+ function getAriaSort(column : { fieldName: string ; sortable? : boolean }) {
305+ if (! isColumnSortable (column )) return undefined ;
306+ if (! isSorted (column )) return ' none' ;
307+ return currentSortDirection .value === ' asc' ? ' ascending' : ' descending' ;
308+ }
309+
310+ function onHeaderClick(column : { fieldName: string ; sortable? : boolean }) {
311+ if (! isColumnSortable (column )) return ;
312+ if (currentSortField .value !== column .fieldName ) {
313+ currentSortField .value = column .fieldName ;
314+ currentSortDirection .value = props .defaultSortDirection ?? ' asc' ;
315+ } else {
316+ if (currentSortDirection .value === ' asc' ) {
317+ currentSortDirection .value = ' desc' ;
318+ } else if (currentSortDirection .value === ' desc' ) {
319+ currentSortField .value = undefined ;
320+ currentSortDirection .value = props .defaultSortDirection ?? ' asc' ;
321+ } else {
322+ currentSortDirection .value = ' asc' ;
323+ }
324+ }
325+ }
326+
327+ function getValueByPath(obj : any , path : string | undefined ) {
328+ if (! path ) return undefined ;
329+ return path .split (' .' ).reduce ((acc : any , key : string ) => (acc == null ? acc : acc [key ]), obj );
330+ }
331+
332+ function compareValues(a : any , b : any ) {
333+ if (a == null && b == null ) return 0 ;
334+ if (a == null ) return 1 ;
335+ if (b == null ) return - 1 ;
336+ if (typeof a === ' number' && typeof b === ' number' ) return a - b ;
337+ const aDate = a instanceof Date ? a : undefined ;
338+ const bDate = b instanceof Date ? b : undefined ;
339+ if (aDate && bDate ) return aDate .getTime () - bDate .getTime ();
340+ return String (a ).localeCompare (String (b ), undefined , { numeric: true , sensitivity: ' base' });
341+ }
342+
343+ function sortArrayData(data : { [key : string ]: any }[], sortField ? : string , sortDirection : ' asc' | ' desc' = ' asc' ) {
344+ if (! props .sortable || ! sortField ) return data ;
345+ const copy = data .slice ();
346+ copy .sort ((rowA , rowB ) => {
347+ const aVal = getValueByPath (rowA , sortField );
348+ const bVal = getValueByPath (rowB , sortField );
349+ const cmp = compareValues (aVal , bVal );
350+ return sortDirection === ' asc' ? cmp : - cmp ;
351+ });
352+ return copy ;
353+ }
354+
255355 </script >
0 commit comments