22import { computed , ref } from ' vue'
33
44import { getRandomId } from ' @/utils/random-utils'
5- import DsfrPagination , { type Page } from ' ../DsfrPagination/DsfrPagination.vue'
6- import type { DsfrDataTableProps } from ' ./DsfrDataTable.types'
5+ import VIcon from ' ../VIcon/VIcon.vue'
6+ import DsfrPagination from ' ../DsfrPagination/DsfrPagination.vue'
7+
8+ export type Page = { href? : string , label: string , title: string }
9+
10+ export type DsfrDataTableRow = (string | number | boolean | bigint | symbol )[]
11+ | Record <string | symbol | number , unknown >
12+
13+ export type DsfrDataTableHeaderCellObject = { key: string , label: string , headerAttrs? : Record <string , unknown > }
14+ export type DsfrDataTableHeaderCell = (string | DsfrDataTableHeaderCellObject )
15+
16+ export type DsfrDataTableProps = {
17+ id? : string
18+ title: string
19+ rowKey? : string | number
20+ headersRow: DsfrDataTableHeaderCell []
21+ rows: DsfrDataTableRow []
22+ topActionsRow? : string []
23+ bottomActionsRow? : string []
24+ selectableRows? : boolean
25+ sortableRows? : boolean
26+ sorted: string
27+ sortFn? : (a : unknown , b : unknown ) => number
28+ verticalBorders? : boolean
29+ bottomCaption? : boolean
30+ noCaption? : boolean
31+ pages? : Page []
32+ pagination? : boolean
33+ paginationOptions? : number []
34+ currentPage? : number
35+ rowsPerPage? : number
36+ bottomActionBarClass? : string | Record <string , boolean > | Array <string | Record <string , boolean >>
37+ paginationWrapperClass? : string | Record <string , boolean > | Array <string | Record <string , boolean >>
38+ }
739
840const props = withDefaults (defineProps <DsfrDataTableProps >(), {
941 id : () => getRandomId (' table' ),
@@ -32,6 +64,42 @@ const pages = computed<Page[]>(() => props.pages ?? Array.from({ length: pageCou
3264const lowestLimit = computed (() => currentPage .value * rowsPerPage .value )
3365const highestLimit = computed (() => (currentPage .value + 1 ) * rowsPerPage .value )
3466
67+ function defaultSortFn (a , b ) {
68+ const key = props .sorted
69+ if ((a [key ] ?? a ) < (b [key ] ?? b )) {
70+ return - 1
71+ }
72+ if ((a [key ] ?? a ) > (b [key ] ?? b )) {
73+ return 1
74+ }
75+ return 0
76+ }
77+
78+ const sorted = defineModel <string | undefined >(' sorted' , { default: undefined })
79+ const sortedDesc = defineModel (' sortedDesc' , { default: false })
80+ function sortBy (key : string ) {
81+ if (! props .sortableRows ) {
82+ return
83+ }
84+ if (sorted .value === key ) {
85+ if (sortedDesc .value ) {
86+ sorted .value = undefined
87+ sortedDesc .value = false
88+ return
89+ }
90+ sortedDesc .value = true
91+ return
92+ }
93+ sortedDesc .value = false
94+ sorted .value = key
95+ }
96+ const sortedRows = computed (() => {
97+ const _sortedRows = sorted .value ? props .rows .slice ().sort (props .sortFn ?? defaultSortFn ) : props .rows .slice ()
98+ if (sortedDesc .value ) {
99+ _sortedRows .reverse ()
100+ }
101+ return _sortedRows
102+ })
35103const finalRows = computed (() => {
36104 const rowKeys = props .headersRow .map ((header ) => {
37105 if (typeof header !== ' object' ) {
@@ -40,7 +108,7 @@ const finalRows = computed(() => {
40108 return header .key
41109 })
42110
43- const rows = props . rows .map ((row ) => {
111+ const rows = sortedRows . value .map ((row ) => {
44112 if (Array .isArray (row )) {
45113 return row
46114 }
@@ -108,17 +176,34 @@ function onPaginationOptionsChange () {
108176 </div >
109177 </th >
110178 <th
111- v-for =" header of headersRow"
179+ v-for =" ( header, idx) of headersRow"
112180 :key =" typeof header === 'object' ? header.key : header"
113181 scope =" col"
114182 v-bind =" typeof header === 'object' && header.headerAttrs"
183+ :tabindex =" sortableRows ? 0 : undefined"
184+ @click =" sortBy((header as DsfrDataTableHeaderCellObject).key ?? (Array.isArray(rows[0]) ? idx : header))"
185+ @keydown.enter =" sortBy((header as DsfrDataTableHeaderCellObject).key ?? header)"
186+ @keydown.space =" sortBy((header as DsfrDataTableHeaderCellObject).key ?? header)"
115187 >
116- <slot
117- name =" header"
118- v-bind =" typeof header === 'object' ? header : { key: header, label: header }"
188+ <div
189+ :class =" { 'sortable-header': sortableRows }"
119190 >
120- {{ typeof header === 'object' ? header.label : header }}
121- </slot >
191+ <slot
192+ name =" header"
193+ v-bind =" typeof header === 'object' ? header : { key: header, label: header }"
194+ >
195+ {{ typeof header === 'object' ? header.label : header }}
196+ </slot >
197+ <span v-if =" sorted !== ((header as DsfrDataTableHeaderCellObject).key ?? header) && sortableRows" >
198+ <VIcon
199+ name =" ri-sort-asc"
200+ color =" var(--grey-625-425)"
201+ />
202+ </span >
203+ <span v-else-if =" sorted === ((header as DsfrDataTableHeaderCellObject).key ?? header)" >
204+ <VIcon :name =" sortedDesc ? 'ri-sort-desc' : 'ri-sort-asc'" />
205+ </span >
206+ </div >
122207 </th >
123208 </tr >
124209 </thead >
@@ -247,4 +332,9 @@ function onPaginationOptionsChange () {
247332:deep(.fr-pagination__link ) {
248333 margin-bottom : 0 !important ;
249334}
335+ .sortable-header {
336+ display : flex ;
337+ justify-content : space-between ;
338+ cursor : pointer ;
339+ }
250340 </style >
0 commit comments