UNPKG

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
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 } } }