bootstrap-vue
Version:
With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extens
296 lines (294 loc) • 9.51 kB
JavaScript
import stableSort from '../../../utils/stable-sort'
import { arrayIncludes } from '../../../utils/array'
import { isFunction, isUndefinedOrNull } from '../../../utils/inspect'
import { trim } from '../../../utils/string'
import defaultSortCompare from './default-sort-compare'
export default {
props: {
sortBy: {
type: String,
default: ''
},
sortDesc: {
// TODO: Make this tri-state: true, false, null
type: Boolean,
default: false
},
sortDirection: {
// This prop is named incorrectly
// It should be `initialSortDirection` as it is a bit misleading
// (not to mention it screws up the ARIA label on the headers)
type: String,
default: 'asc',
validator: direction => arrayIncludes(['asc', 'desc', 'last'], direction)
},
sortCompare: {
type: Function
// default: null
},
sortCompareOptions: {
// Supported localCompare options, see `options` section of:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
type: Object,
default: () => {
return { numeric: true }
}
},
sortCompareLocale: {
// String: locale code
// Array: array of Locale strings
type: [String, Array]
// default: undefined
},
sortNullLast: {
// Sort null and undefined to appear last
type: Boolean,
default: false
},
noSortReset: {
// Another prop that should have had a better name.
// It should be noSortClear (on non-sortable headers).
// We will need to make sure the documentation is clear on what
// this prop does (as well as in the code for future reference)
type: Boolean,
default: false
},
labelSortAsc: {
type: String,
default: 'Click to sort Ascending'
},
labelSortDesc: {
type: String,
default: 'Click to sort Descending'
},
labelSortClear: {
type: String,
default: 'Click to clear sorting'
},
noLocalSorting: {
type: Boolean,
default: false
},
noFooterSorting: {
type: Boolean,
default: false
},
sortIconLeft: {
// Place the sorting icon on the left of the header cells
type: Boolean,
default: false
}
},
data() {
return {
localSortBy: this.sortBy || '',
localSortDesc: this.sortDesc || false
}
},
computed: {
localSorting() {
return this.hasProvider ? !!this.noProviderSorting : !this.noLocalSorting
},
isSortable() {
return this.computedFields.some(f => f.sortable)
},
sortedItems() {
// Sorts the filtered items and returns a new array of the sorted items
// or the original items array if not sorted.
const items = (this.filteredItems || this.localItems || []).slice()
const sortBy = this.localSortBy
const sortDesc = this.localSortDesc
const sortCompare = this.sortCompare
const localSorting = this.localSorting
const sortOptions = { ...this.sortCompareOptions, usage: 'sort' }
const sortLocale = this.sortCompareLocale || undefined
const nullLast = this.sortNullLast
if (sortBy && localSorting) {
const field = this.computedFieldsObj[sortBy] || {}
const sortByFormatted = field.sortByFormatted
const formatter = isFunction(sortByFormatted)
? /* istanbul ignore next */ sortByFormatted
: sortByFormatted
? this.getFieldFormatter(sortBy)
: undefined
// `stableSort` returns a new array, and leaves the original array intact
return stableSort(items, (a, b) => {
let result = null
if (isFunction(sortCompare)) {
// Call user provided sortCompare routine
result = sortCompare(a, b, sortBy, sortDesc, formatter, sortOptions, sortLocale)
}
if (isUndefinedOrNull(result) || result === false) {
// Fallback to built-in defaultSortCompare if sortCompare
// is not defined or returns null/false
result = defaultSortCompare(
a,
b,
sortBy,
sortDesc,
formatter,
sortOptions,
sortLocale,
nullLast
)
}
// Negate result if sorting in descending order
return (result || 0) * (sortDesc ? -1 : 1)
})
}
return items
}
},
watch: {
/* istanbul ignore next: pain in the butt to test */
isSortable(newVal) /* istanbul ignore next: pain in the butt to test */ {
if (newVal) {
if (this.isSortable) {
this.$on('head-clicked', this.handleSort)
}
} else {
this.$off('head-clicked', this.handleSort)
}
},
sortDesc(newVal) {
if (newVal === this.localSortDesc) {
/* istanbul ignore next */
return
}
this.localSortDesc = newVal || false
},
sortBy(newVal) {
if (newVal === this.localSortBy) {
/* istanbul ignore next */
return
}
this.localSortBy = newVal || ''
},
// Update .sync props
localSortDesc(newVal, oldVal) {
// Emit update to sort-desc.sync
if (newVal !== oldVal) {
this.$emit('update:sortDesc', newVal)
}
},
localSortBy(newVal, oldVal) {
if (newVal !== oldVal) {
this.$emit('update:sortBy', newVal)
}
}
},
created() {
if (this.isSortable) {
this.$on('head-clicked', this.handleSort)
}
},
methods: {
// Handlers
// Need to move from thead-mixin
handleSort(key, field, evt, isFoot) {
if (!this.isSortable) {
/* istanbul ignore next */
return
}
if (isFoot && this.noFooterSorting) {
return
}
// TODO: make this tri-state sorting
// cycle desc => asc => none => desc => ...
let sortChanged = false
const toggleLocalSortDesc = () => {
const sortDirection = field.sortDirection || this.sortDirection
if (sortDirection === 'asc') {
this.localSortDesc = false
} else if (sortDirection === 'desc') {
this.localSortDesc = true
} else {
// sortDirection === 'last'
// Leave at last sort direction from previous column
}
}
if (field.sortable) {
const sortKey = !this.localSorting && field.sortKey ? field.sortKey : key
if (this.localSortBy === sortKey) {
// Change sorting direction on current column
this.localSortDesc = !this.localSortDesc
} else {
// Start sorting this column ascending
this.localSortBy = sortKey
// this.localSortDesc = false
toggleLocalSortDesc()
}
sortChanged = true
} else if (this.localSortBy && !this.noSortReset) {
this.localSortBy = ''
toggleLocalSortDesc()
sortChanged = true
}
if (sortChanged) {
// Sorting parameters changed
this.$emit('sort-changed', this.context)
}
},
// methods to compute classes and attrs for thead>th cells
sortTheadThClasses(key, field, isFoot) {
return {
// If sortable and sortIconLeft are true, then place sort icon on the left
'b-table-sort-icon-left':
field.sortable && this.sortIconLeft && !(isFoot && this.noFooterSorting)
}
},
sortTheadThAttrs(key, field, isFoot) {
if (!this.isSortable || (isFoot && this.noFooterSorting)) {
// No attributes if not a sortable table
return {}
}
const sortable = field.sortable
// Assemble the aria-sort attribute value
const ariaSort =
sortable && this.localSortBy === key
? this.localSortDesc
? 'descending'
: 'ascending'
: sortable
? 'none'
: null
// Return the attribute
return {
'aria-sort': ariaSort
}
},
sortTheadThLabel(key, field, isFoot) {
// A label to be placed in an `.sr-only` element in the header cell
if (!this.isSortable || (isFoot && this.noFooterSorting)) {
// No label if not a sortable table
return null
}
const sortable = field.sortable
// The correctness of these labels is very important for screen-reader users.
let labelSorting = ''
if (sortable) {
if (this.localSortBy === key) {
// currently sorted sortable column.
labelSorting = this.localSortDesc ? this.labelSortAsc : this.labelSortDesc
} else {
// Not currently sorted sortable column.
// Not using nested ternary's here for clarity/readability
// Default for ariaLabel
labelSorting = this.localSortDesc ? this.labelSortDesc : this.labelSortAsc
// Handle sortDirection setting
const sortDirection = this.sortDirection || field.sortDirection
if (sortDirection === 'asc') {
labelSorting = this.labelSortAsc
} else if (sortDirection === 'desc') {
labelSorting = this.labelSortDesc
}
}
} else if (!this.noSortReset) {
// Non sortable column
labelSorting = this.localSortBy ? this.labelSortClear : ''
}
// Return the sr-only sort label or null if no label
return trim(labelSorting) || null
}
}
}