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
242 lines (231 loc) • 7.68 kB
JavaScript
import { extend } from '../../../vue'
import {
EVENT_NAME_CONTEXT_CHANGED,
EVENT_NAME_FILTERED,
EVENT_NAME_ROW_CLICKED,
EVENT_NAME_ROW_SELECTED
} from '../../../constants/events'
import { PROP_TYPE_BOOLEAN, PROP_TYPE_STRING } from '../../../constants/props'
import { arrayIncludes, createArray } from '../../../utils/array'
import { identity } from '../../../utils/identity'
import { isArray, isNumber } from '../../../utils/inspect'
import { looseEqual } from '../../../utils/loose-equal'
import { mathMax, mathMin } from '../../../utils/math'
import { makeProp } from '../../../utils/props'
import { toString } from '../../../utils/string'
import { sanitizeRow } from './sanitize-row'
// --- Constants ---
const SELECT_MODES = ['range', 'multi', 'single']
const ROLE_GRID = 'grid'
// --- Props ---
export const props = {
// Disable use of click handlers for row selection
noSelectOnClick: makeProp(PROP_TYPE_BOOLEAN, false),
selectMode: makeProp(PROP_TYPE_STRING, 'multi', value => {
return arrayIncludes(SELECT_MODES, value)
}),
selectable: makeProp(PROP_TYPE_BOOLEAN, false),
selectedVariant: makeProp(PROP_TYPE_STRING, 'active')
}
// --- Mixin ---
// @vue/component
export const selectableMixin = extend({
props,
data() {
return {
selectedRows: [],
selectedLastRow: -1
}
},
computed: {
isSelectable() {
return this.selectable && this.selectMode
},
hasSelectableRowClick() {
return this.isSelectable && !this.noSelectOnClick
},
supportsSelectableRows() {
return true
},
selectableHasSelection() {
const { selectedRows } = this
return (
this.isSelectable && selectedRows && selectedRows.length > 0 && selectedRows.some(identity)
)
},
selectableIsMultiSelect() {
return this.isSelectable && arrayIncludes(['range', 'multi'], this.selectMode)
},
selectableTableClasses() {
const { isSelectable } = this
return {
'b-table-selectable': isSelectable,
[`b-table-select-${this.selectMode}`]: isSelectable,
'b-table-selecting': this.selectableHasSelection,
'b-table-selectable-no-click': isSelectable && !this.hasSelectableRowClick
}
},
selectableTableAttrs() {
if (!this.isSelectable) {
return {}
}
const role = this.bvAttrs.role || ROLE_GRID
return {
role,
// TODO:
// Should this attribute not be included when `no-select-on-click` is set
// since this attribute implies keyboard navigation?
'aria-multiselectable': role === ROLE_GRID ? toString(this.selectableIsMultiSelect) : null
}
}
},
watch: {
computedItems(newValue, oldValue) {
// Reset for selectable
let equal = false
if (this.isSelectable && this.selectedRows.length > 0) {
// Quick check against array length
equal = isArray(newValue) && isArray(oldValue) && newValue.length === oldValue.length
for (let i = 0; equal && i < newValue.length; i++) {
// Look for the first non-loosely equal row, after ignoring reserved fields
equal = looseEqual(sanitizeRow(newValue[i]), sanitizeRow(oldValue[i]))
}
}
if (!equal) {
this.clearSelected()
}
},
selectable(newValue) {
this.clearSelected()
this.setSelectionHandlers(newValue)
},
selectMode() {
this.clearSelected()
},
hasSelectableRowClick(newValue) {
this.clearSelected()
this.setSelectionHandlers(!newValue)
},
selectedRows(selectedRows, oldValue) {
if (this.isSelectable && !looseEqual(selectedRows, oldValue)) {
const items = []
// `.forEach()` skips over non-existent indices (on sparse arrays)
selectedRows.forEach((v, idx) => {
if (v) {
items.push(this.computedItems[idx])
}
})
this.$emit(EVENT_NAME_ROW_SELECTED, items)
}
}
},
beforeMount() {
// Set up handlers if needed
if (this.isSelectable) {
this.setSelectionHandlers(true)
}
},
methods: {
// Public methods
selectRow(index) {
// Select a particular row (indexed based on computedItems)
if (
this.isSelectable &&
isNumber(index) &&
index >= 0 &&
index < this.computedItems.length &&
!this.isRowSelected(index)
) {
const selectedRows = this.selectableIsMultiSelect ? this.selectedRows.slice() : []
selectedRows[index] = true
this.selectedLastClicked = -1
this.selectedRows = selectedRows
}
},
unselectRow(index) {
// Un-select a particular row (indexed based on `computedItems`)
if (this.isSelectable && isNumber(index) && this.isRowSelected(index)) {
const selectedRows = this.selectedRows.slice()
selectedRows[index] = false
this.selectedLastClicked = -1
this.selectedRows = selectedRows
}
},
selectAllRows() {
const length = this.computedItems.length
if (this.isSelectable && length > 0) {
this.selectedLastClicked = -1
this.selectedRows = this.selectableIsMultiSelect ? createArray(length, true) : [true]
}
},
isRowSelected(index) {
// Determine if a row is selected (indexed based on `computedItems`)
return !!(isNumber(index) && this.selectedRows[index])
},
clearSelected() {
// Clear any active selected row(s)
this.selectedLastClicked = -1
this.selectedRows = []
},
// Internal private methods
selectableRowClasses(index) {
if (this.isSelectable && this.isRowSelected(index)) {
const variant = this.selectedVariant
return {
'b-table-row-selected': true,
[`${this.dark ? 'bg' : 'table'}-${variant}`]: variant
}
}
return {}
},
selectableRowAttrs(index) {
return {
'aria-selected': !this.isSelectable ? null : this.isRowSelected(index) ? 'true' : 'false'
}
},
setSelectionHandlers(on) {
const method = on && !this.noSelectOnClick ? '$on' : '$off'
// Handle row-clicked event
this[method](EVENT_NAME_ROW_CLICKED, this.selectionHandler)
// Clear selection on filter, pagination, and sort changes
this[method](EVENT_NAME_FILTERED, this.clearSelected)
this[method](EVENT_NAME_CONTEXT_CHANGED, this.clearSelected)
},
selectionHandler(item, index, event) {
/* istanbul ignore if: should never happen */
if (!this.isSelectable || this.noSelectOnClick) {
// Don't do anything if table is not in selectable mode
this.clearSelected()
return
}
const { selectMode, selectedLastRow } = this
let selectedRows = this.selectedRows.slice()
let selected = !selectedRows[index]
// Note 'multi' mode needs no special event handling
if (selectMode === 'single') {
selectedRows = []
} else if (selectMode === 'range') {
if (selectedLastRow > -1 && event.shiftKey) {
// range
for (
let idx = mathMin(selectedLastRow, index);
idx <= mathMax(selectedLastRow, index);
idx++
) {
selectedRows[idx] = true
}
selected = true
} else {
if (!(event.ctrlKey || event.metaKey)) {
// Clear range selection if any
selectedRows = []
selected = true
}
if (selected) this.selectedLastRow = index
}
}
selectedRows[index] = selected
this.selectedRows = selectedRows
}
}
})