1
1
import stableSort from '../../../utils/stable-sort'
2
2
import startCase from '../../../utils/startcase'
3
- import { arrayIncludes } from '../../../utils/array'
4
- import { isFunction , isUndefinedOrNull } from '../../../utils/inspect'
3
+ import looseEqual from '../../../utils/loose-equal'
4
+ import { arrayIncludes , concat } from '../../../utils/array'
5
+ import { isBoolean , isFunction , isNumber , isUndefined , isUndefinedOrNull } from '../../../utils/inspect'
5
6
import defaultSortCompare from './default-sort-compare'
6
7
7
8
export default {
8
9
props : {
9
10
sortBy : {
10
- type : String ,
11
+ // Array for multi-sort (prop sort-multi must be set to true)
12
+ type : [ String , Array ] ,
11
13
default : ''
12
14
} ,
13
15
sortDesc : {
14
- // TODO: Make this tri-state: true, false, null
16
+ // Array for multi-sort. Must be same length as sortBy
17
+ // (prop sort-multi must be set to true)
18
+ // TODO: Support tri-state: true, false, null
19
+ type : [ Boolean , Array ] ,
20
+ default : false
21
+ } ,
22
+ sortMulti : {
23
+ // Enables multi-columns sorting When set, also implies tri-state sorting
15
24
type : Boolean ,
16
25
default : false
17
26
} ,
18
27
sortDirection : {
19
28
// This prop is named incorrectly
20
- // It should be `initialSortDirection` as it is a bit misleading
21
- // (not to mention it screws up the ARIA label on the headers)
29
+ // It should be `initialSortDirection` as it is misleading
30
+ // (not to mention it screws up computing the ARIA label on the headers)
31
+ // It (last) should probably be deprecated in favour of multi-sort, as
32
+ // 'last' will make multi-sort very bloated!
22
33
type : String ,
23
34
default : 'asc' ,
24
35
validator : direction => arrayIncludes ( [ 'asc' , 'desc' , 'last' ] , direction )
@@ -82,8 +93,8 @@ export default {
82
93
} ,
83
94
data ( ) {
84
95
return {
85
- localSortBy : this . sortBy || '' ,
86
- localSortDesc : this . sortDesc || false
96
+ localSortBy : [ '' ] ,
97
+ localSortDesc : [ false ]
87
98
}
88
99
} ,
89
100
computed : {
@@ -93,48 +104,75 @@ export default {
93
104
isSortable ( ) {
94
105
return this . computedFields . some ( f => f . sortable )
95
106
} ,
107
+ computedSortBy ( ) {
108
+ return this . sortMulti ? this . localSortBy : this . localSortBy . slice ( 0 , 1 )
109
+ } ,
110
+ computedSortDesc ( ) {
111
+ // Ensure values are tri-state (true, false, null), and same length as localSortBy array
112
+ const sortDesc = this . localSortDesc
113
+ return this . computedSortBy . map ( ( _ , idx ) => ( isBoolean ( sortDesc [ idx ] ) ? sortDesc [ idx ] : null ) )
114
+ } ,
115
+ computedSortInfo ( ) {
116
+ // TODO in this PR:
117
+ // Make sortInfo a data property which we update
118
+ // via watchers on sortBy/SortDesc and event handlers.
119
+ // When a column is no longer sorted (null) we remove it from the array
120
+ return this . computedSortBy
121
+ . map ( ( key , idx ) => {
122
+ const field = this . computedFieldsObj [ key ] || { }
123
+ return {
124
+ sortBy : key ,
125
+ sortDesc : this . computedSortDesc [ idx ] ,
126
+ formatter : isFunction ( field . formatter )
127
+ ? field . formatter
128
+ : field . formatter
129
+ ? this . getFieldFormatter ( key )
130
+ : undefined
131
+ }
132
+ } )
133
+ . filter ( x => x . sortBy && isBoolean ( x . sortDesc ) )
134
+ } ,
96
135
sortedItems ( ) {
97
136
// Sorts the filtered items and returns a new array of the sorted items
98
137
// or the original items array if not sorted.
99
138
const items = ( this . filteredItems || this . localItems || [ ] ) . slice ( )
100
- const sortBy = this . localSortBy
101
- const sortDesc = this . localSortDesc
139
+ const sortInfo = this . computedSortInfo
102
140
const sortCompare = this . sortCompare
103
141
const localSorting = this . localSorting
104
142
const sortOptions = { ...this . sortCompareOptions , usage : 'sort' }
105
143
const sortLocale = this . sortCompareLocale || undefined
106
144
const nullLast = this . sortNullLast
107
- if ( sortBy && localSorting ) {
108
- const field = this . computedFieldsObj [ sortBy ] || { }
109
- const sortByFormatted = field . sortByFormatted
110
- const formatter = isFunction ( sortByFormatted )
111
- ? sortByFormatted
112
- : sortByFormatted
113
- ? this . getFieldFormatter ( sortBy )
114
- : undefined
145
+ if ( sortInfo && sortInfo . length > 0 && localSorting ) {
115
146
// `stableSort` returns a new array, and leaves the original array intact
116
147
return stableSort ( items , ( a , b ) => {
117
- let result = null
118
- if ( isFunction ( sortCompare ) ) {
119
- // Call user provided sortCompare routine
120
- result = sortCompare ( a , b , sortBy , sortDesc , formatter , sortOptions , sortLocale )
148
+ let result = 0
149
+ for ( let idx = 0 ; idx < sortInfo . length && ! result ; idx ++ ) {
150
+ const formatter = sortInfo [ idx ] . formatter
151
+ const sortBy = sortInfo [ idx ] . sortBy
152
+ const sortDesc = sortInfo [ idx ] . sortDesc
153
+ let value = null
154
+ if ( isFunction ( sortCompare ) ) {
155
+ // Call user provided sortCompare routine
156
+ value = sortCompare ( a , b , sortBy , sortDesc , formatter , sortOptions , sortLocale )
157
+ }
158
+ if ( ! isNumber ( value ) ) {
159
+ // Fallback to built-in defaultSortCompare if sortCompare
160
+ // is not defined or doesn't return a number
161
+ value = defaultSortCompare (
162
+ a ,
163
+ b ,
164
+ sortBy ,
165
+ sortDesc ,
166
+ formatter ,
167
+ sortOptions ,
168
+ sortLocale ,
169
+ nullLast
170
+ )
171
+ }
172
+ // Negate result if sorting if descending order
173
+ result = ( value || 0 ) * ( sortDesc [ idx ] ? - 1 : 1 )
121
174
}
122
- if ( isUndefinedOrNull ( result ) || result === false ) {
123
- // Fallback to built-in defaultSortCompare if sortCompare
124
- // is not defined or returns null/false
125
- result = defaultSortCompare (
126
- a ,
127
- b ,
128
- sortBy ,
129
- sortDesc ,
130
- formatter ,
131
- sortOptions ,
132
- sortLocale ,
133
- nullLast
134
- )
135
- }
136
- // Negate result if sorting in descending order
137
- return ( result || 0 ) * ( sortDesc ? - 1 : 1 )
175
+ return result
138
176
} )
139
177
}
140
178
return items
@@ -143,37 +181,49 @@ export default {
143
181
watch : {
144
182
isSortable ( newVal , oldVal ) /* istanbul ignore next: pain in the butt to test */ {
145
183
if ( newVal ) {
146
- if ( this . isSortable ) {
147
- this . $on ( 'head-clicked' , this . handleSort )
148
- }
184
+ this . $on ( 'head-clicked' , this . handleSort )
149
185
} else {
150
186
this . $off ( 'head-clicked' , this . handleSort )
151
187
}
152
188
} ,
153
- sortDesc ( newVal , oldVal ) {
154
- if ( newVal === this . localSortDesc ) {
155
- /* istanbul ignore next */
156
- return
189
+ sortBy : {
190
+ immediate : true ,
191
+ handler ( newVal , oldVal ) {
192
+ if ( looseEqual ( newVal , this . localSortBy ) ) {
193
+ /* istanbul ignore next */
194
+ return
195
+ }
196
+ // TODO in this PR:
197
+ // Update sortInfo object
198
+ this . localSortBy = newVal ? concat ( newVal ) : [ ]
157
199
}
158
- this . localSortDesc = newVal || false
159
200
} ,
160
- sortBy ( newVal , oldVal ) {
161
- if ( newVal === this . localSortBy ) {
162
- /* istanbul ignore next */
163
- return
201
+ sortDesc : {
202
+ immediate : true ,
203
+ handler ( newVal , oldVal ) {
204
+ if ( looseEqual ( newVal , this . localSortDesc ) ) {
205
+ /* istanbul ignore next */
206
+ return
207
+ }
208
+ // TODO in this PR:
209
+ // Update sortInfo object
210
+ newVal = isUndefined ( newVal ) ? [ ] : concat ( newVal )
211
+ this . localSortDesc = newVal . map ( d => ( isBoolean ( d ) ? d : null ) )
164
212
}
165
- this . localSortBy = newVal || ''
166
213
} ,
167
214
// Update .sync props
215
+ // TODO in this PR:
216
+ // Instead watch the sortInfo array, and emit the apropriate
217
+ // updated values as needed
168
218
localSortDesc ( newVal , oldVal ) {
169
219
// Emit update to sort-desc.sync
170
- if ( newVal !== oldVal ) {
171
- this . $emit ( 'update:sortDesc' , newVal )
220
+ if ( ! looseEqual ( newVal , oldVal ) ) {
221
+ this . $emit ( 'update:sortDesc' , this . sortMulti ? newVal : newVal [ 0 ] )
172
222
}
173
223
} ,
174
224
localSortBy ( newVal , oldVal ) {
175
- if ( newVal !== oldVal ) {
176
- this . $emit ( 'update:sortBy' , newVal )
225
+ if ( ! looseEqual ( newVal , oldVal ) ) {
226
+ this . $emit ( 'update:sortBy' , this . sortMulti ? newVal : newVal [ 0 ] )
177
227
}
178
228
}
179
229
} ,
@@ -184,7 +234,6 @@ export default {
184
234
} ,
185
235
methods : {
186
236
// Handlers
187
- // Need to move from thead-mixin
188
237
handleSort ( key , field , evt , isFoot ) {
189
238
if ( ! this . isSortable ) {
190
239
/* istanbul ignore next */
@@ -193,35 +242,48 @@ export default {
193
242
if ( isFoot && this . noFooterSorting ) {
194
243
return
195
244
}
196
- // TODO: make this tri-state sorting
245
+ // TODO in this PR:
246
+ // Make a sequence of sorting cycle based on this.sortDirection
247
+ // i.e. [true, false, null] vs [false, true, null]
248
+ // const sequence = this.sortDirection === 'desc' ? [true, false, null] : [false, true, null]
249
+ // TODO in this PR:
250
+ // Handle this via lookup
251
+ const sortBy = this . localSortBy [ 0 ]
252
+ // TODO: make this tri-state sorting (for multi-sort only)
197
253
// cycle desc => asc => none => desc => ...
198
254
let sortChanged = false
199
255
const toggleLocalSortDesc = ( ) => {
200
256
const sortDirection = field . sortDirection || this . sortDirection
201
257
if ( sortDirection === 'asc' ) {
202
- this . localSortDesc = false
258
+ this . localSortDesc = [ false ]
203
259
} else if ( sortDirection === 'desc' ) {
204
- this . localSortDesc = true
260
+ this . localSortDesc = [ true ]
205
261
} else {
206
262
// sortDirection === 'last'
207
263
// Leave at last sort direction from previous column
264
+ // TODO in this PR:
265
+ // If multi-sort, then use sort sequence
208
266
}
209
267
}
210
268
if ( field . sortable ) {
211
- if ( key === this . localSortBy ) {
269
+ // TODO in this PR:
270
+ // handle toggling on the index
271
+ if ( key === sortBy ) {
212
272
// Change sorting direction on current column
213
- this . localSortDesc = ! this . localSortDesc
273
+ this . localSortDesc [ 0 ] = ! this . localSortDesc [ 0 ]
214
274
} else {
215
275
// Start sorting this column ascending
216
- this . localSortBy = key
276
+ this . localSortBy [ 0 ] = key
217
277
// this.localSortDesc = false
218
278
toggleLocalSortDesc ( )
219
279
}
220
280
sortChanged = true
221
281
} else if ( this . localSortBy && ! this . noSortReset ) {
222
- this . localSortBy = ''
282
+ this . localSortBy = [ '' ]
223
283
toggleLocalSortDesc ( )
224
284
sortChanged = true
285
+ // } else {
286
+ // sortChanged = false
225
287
}
226
288
if ( sortChanged ) {
227
289
// Sorting parameters changed
@@ -242,6 +304,19 @@ export default {
242
304
return { }
243
305
}
244
306
const sortable = field . sortable
307
+ // TODO in this PR:
308
+ // This is just temporary. Will need to lookup the index of this
309
+ // key in this.localSortBy or this.localSortInfo
310
+ const sortOrder = 1
311
+ // TODO for this PR:
312
+ // Find index of key in this.computedSortBy (or this.computedSortInfo),
313
+ // and then grab the sorting value
314
+ const sortDesc = this . localSortDesc [ 0 ]
315
+ // TODO in this PR:
316
+ // This is just temporary. Will need to lookup if this column key is
317
+ // in this.localSortBy
318
+ const sortBy = this . localSortBy [ 0 ]
319
+ // Now we get down to business determining the aria-label value
245
320
let ariaLabel = ''
246
321
if ( ( ! field . label || ! field . label . trim ( ) ) && ! field . headerTitle ) {
247
322
// In case field's label and title are empty/blank, we need to
@@ -251,17 +326,18 @@ export default {
251
326
/* istanbul ignore next */
252
327
ariaLabel = startCase ( key )
253
328
}
254
- // The correctness of these labels is very important for screen-reader users.
329
+ // The correctness of these labels is very important for screen-reader users,
330
+ // and should sync up with the aria-sort attributes
255
331
let ariaLabelSorting = ''
256
332
if ( sortable ) {
257
- if ( this . localSortBy === key ) {
333
+ if ( key === sortBy ) {
258
334
// currently sorted sortable column.
259
- ariaLabelSorting = this . localSortDesc ? this . labelSortAsc : this . labelSortDesc
335
+ ariaLabelSorting = sortDesc ? this . labelSortAsc : this . labelSortDesc
260
336
} else {
261
337
// Not currently sorted sortable column.
262
338
// Not using nested ternary's here for clarity/readability
263
339
// Default for ariaLabel
264
- ariaLabelSorting = this . localSortDesc ? this . labelSortDesc : this . labelSortAsc
340
+ ariaLabelSorting = sortDesc ? this . labelSortDesc : this . labelSortAsc
265
341
// Handle sortDirection setting
266
342
const sortDirection = this . sortDirection || field . sortDirection
267
343
if ( sortDirection === 'asc' ) {
@@ -278,18 +354,27 @@ export default {
278
354
ariaLabel = [ ariaLabel . trim ( ) , ariaLabelSorting . trim ( ) ] . filter ( Boolean ) . join ( ': ' )
279
355
// Assemble the aria-sort attribute value
280
356
const ariaSort =
281
- sortable && this . localSortBy === key
282
- ? this . localSortDesc
357
+ sortable && key === sortBy
358
+ ? sortDesc
283
359
? 'descending'
284
360
: 'ascending'
285
361
: sortable
286
362
? 'none'
287
363
: null
288
364
// Return the attributes
289
- // (All the above just to get these two values)
290
365
return {
366
+ // All the above just to get these two values correct :(
291
367
'aria-label' : ariaLabel || null ,
292
- 'aria-sort' : ariaSort
368
+ 'aria-sort' : ariaSort ,
369
+ // Add indication as to which order the columns are sorted by (numeric)
370
+ // This will be placed as a mini pill badge on the top of the field header
371
+ // TODO in this PR:
372
+ // Add sort index (1-based) if multi sort.
373
+ // Also, add prop to disable showing the sort order
374
+ // And figure out how to set a variant for this badge
375
+ // May require additiona CSS generation. or we just make 2 options: dark and light
376
+ // Or use the column text variant (currentColor) to make an outline badge
377
+ 'data-sort-order' : this . sortMulti ? String ( sortOrder ) : null
293
378
}
294
379
}
295
380
}
0 commit comments