🌐 AI搜索 & 代理 主页
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 66 additions & 17 deletions src/components/table/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1403,6 +1403,25 @@ When a table is `selectable` and the user clicks on a row, `<b-table>` will emit
event, passing a single argument which is the complete list of selected items. **Treat this argument
as read-only.**

Rows can also be programmatically selected and unselected via the following exposed methods on the
`<b-table>` instance (i.e. via a reference to the table instance via `this.$refs`):

| Method | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
| `selectRow(index)` | Selects a row with the given `index` number. |
| `unselectRow(index)` | Unselects a row with the given `index` number. |
| `selectAllRows()` | Selects all rows in the table, except in `single` mode in which case only the first row is selected. |
| `clearSelected()` | Unselects all rows. |
| `isRowSelected(index)` | Returns `true` if the row with the given `index` is selected, otherwise it returns `false`. |

Programmatic selection notes:

- `index` the zero-based index of the table's **visible rows**, after filtering, sorting, and
pagination have been applied.
- In `single` mode, `selectRow(index)` will unselect any previous selected row.
- Attempting to `selectRow(index)` or `unselectRow(index)` on a non-existent row will be ignored.
- The table must be `selectable` for any of these methods to have effect.

```html
<template>
<div>
Expand All @@ -1411,30 +1430,37 @@ as read-only.**
</b-form-group>

<b-table
ref="selectableTable"
selectable
:select-mode="selectMode"
selectedVariant="success"
selected-variant="success"
:items="items"
:fields="fields"
@row-selected="rowSelected"
@row-selected="onRowSelected"
responsive="sm"
>
<!-- We use colgroup to set some widths for styling only -->
<template slot="table-colgroup">
<col style="width: 75px;">
<col style="width: 125px;">
<col style="width: 75px;">
<col>
<col>
</template>
<!-- Example scoped slot for select state illustrative purposes -->
<template slot="[selected]" slot-scope="{ rowSelected }">
<span v-if="rowSelected">☑</span>
<span v-else>☐</span>
<template v-if="rowSelected">
<span aria-hidden="true">&check;</span>
<span class="sr-only">Selected</span>
</template>
<template v-else>
<span aria-hidden="true">&nbsp;</span>
<span class="sr-only">Not selected</span>
</template>
</template>
</b-table>

{{ selected }}
<p>
<b-button size="sm" @click="selectAllRows">Select all</b-button>
<b-button size="sm" @click="clearSelected">Clear selected</b-button>
<b-button size="sm" @click="selectThirdRow">Select 3rd row</b-button>
<b-button size="sm" @click="unselectThirdRow">Unselect 3rd row</b-button>
</p>
<p>
Selected Rows:<br>
{{ selected }}
</p>
</div>
</template>

Expand All @@ -1455,8 +1481,22 @@ as read-only.**
}
},
methods: {
rowSelected(items) {
onRowSelected(items) {
this.selected = items
},
selectAllRows() {
this.$refs.selectableTable.selectAllRows()
},
clearSelected() {
this.$refs.selectableTable.clearSelected()
},
selectThirdRow() {
// Rows are indexed from 0, so the third row is index 2
this.$refs.selectableTable.selectRow(2)
},
unselectThirdRow() {
// Rows are indexed from 0, so the third row is index 2
this.$refs.selectableTable.unselectRow(2)
}
}
}
Expand All @@ -1465,7 +1505,7 @@ as read-only.**
<!-- b-table-selectable.vue -->
```

When table is selectable, it will have class `b-table-selectable`, and one of the following three
When a table is selectable, it will have class `b-table-selectable`, and one of the following three
classes (depending on which mode is in use), on the `<table>` element:

- `b-table-select-single`
Expand All @@ -1475,13 +1515,22 @@ classes (depending on which mode is in use), on the `<table>` element:
When at least one row is selected the class `b-table-selecting` will be active on the `<table>`
element.

Use the prop `selected-variant` to apply a Bootstrap theme color to the selected row(s). Note, due
to the order that the table variants are defined in Bootstrap's CSS, any row-variant's may take
precedence over the `selected-variant`. You can set `selected-variant` to an empty string if you
will be using other means to convey that a row is selected (such as a scoped field slot in the above
example).

**Notes:**

- Paging, filtering, or sorting will clear the selection. The `row-selected` event will be emitted
with an empty array if needed.
- Selected rows will have a class of `b-row-selected` added to them.
- When the table is in `selectable` mode, all data item `<tr>` elements will be in the document tab
sequence (`tabindex="0"`) for accessibility reasons.
sequence (`tabindex="0"`) for [accessibility](#accessibility) reasons, and will have the attribute
`aria-selected` set to either `'true'` or `'false'` depending on the selected state of the row.
- When a table is `selectable`, the table will have the attribute `aria-multiselect` set to either
`'false'` for `single` mode, and `'true'` for either `multi` or `range` modes.

### Table body transition support

Expand Down
131 changes: 86 additions & 45 deletions src/components/table/helpers/mixin-selectable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import looseEqual from '../../../utils/loose-equal'
import range from '../../../utils/range'
import { isArray, arrayIncludes } from '../../../utils/array'
import { getComponentConfig } from '../../../utils/config'
import { isNumber } from '../../../utils/inspect'
import sanitizeRow from './sanitize-row'

export default {
Expand All @@ -11,7 +13,8 @@ export default {
},
selectMode: {
type: String,
default: 'multi'
default: 'multi',
validator: val => arrayIncludes(['range', 'multi', 'single'], val)
},
selectedVariant: {
type: String,
Expand All @@ -25,36 +28,42 @@ export default {
}
},
computed: {
isSelectable() {
return this.selectable && this.selectMode
},
selectableHasSelection() {
return (
this.isSelectable &&
this.selectedRows &&
this.selectedRows.length > 0 &&
this.selectedRows.some(Boolean)
)
},
selectableIsMultiSelect() {
return this.isSelectable && arrayIncludes(['range', 'multi'], this.selectMode)
},
selectableTableClasses() {
const selectable = this.selectable
const isSelecting = selectable && this.selectedRows && this.selectedRows.some(Boolean)
return {
'b-table-selectable': selectable,
[`b-table-select-${this.selectMode}`]: selectable,
'b-table-selecting': isSelecting
'b-table-selectable': this.isSelectable,
[`b-table-select-${this.selectMode}`]: this.isSelectable,
'b-table-selecting': this.selectableHasSelection
}
},
selectableTableAttrs() {
return {
'aria-multiselectable': this.selectableIsMultiSelect
}
},
selectableIsMultiSelect() {
if (this.selectable) {
return arrayIncludes(['range', 'multi'], this.selectMode) ? 'true' : 'false'
} else {
return null
'aria-multiselectable': !this.isSelectable
? null
: this.selectableIsMultiSelect
? 'true'
: 'false'
}
}
},
watch: {
computedItems(newVal, oldVal) {
// Reset for selectable
// TODO: Should selectedLastClicked be reset here?
// As changes to _showDetails would trigger it to reset
this.selectedLastRow = -1
let equal = false
if (this.selectable && this.selectedRows.length > 0) {
if (this.isSelectable && this.selectedRows.length > 0) {
// Quick check against array length
equal = isArray(newVal) && isArray(oldVal) && newVal.length === oldVal.length
for (let i = 0; equal && i < newVal.length; i++) {
Expand All @@ -74,9 +83,9 @@ export default {
this.clearSelected()
},
selectedRows(selectedRows, oldVal) {
if (this.selectable && !looseEqual(selectedRows, oldVal)) {
if (this.isSelectable && !looseEqual(selectedRows, oldVal)) {
const items = []
// forEach skips over non-existant indicies (on sparse arrays)
// `.forEach()` skips over non-existent indices (on sparse arrays)
selectedRows.forEach((v, idx) => {
if (v) {
items.push(this.computedItems[idx])
Expand All @@ -88,35 +97,67 @@ export default {
},
beforeMount() {
// Set up handlers
if (this.selectable) {
if (this.isSelectable) {
this.setSelectionHandlers(true)
}
},
methods: {
isRowSelected(idx) {
return Boolean(this.selectedRows[idx])
// 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
}
},
selectableRowClasses(idx) {
const rowSelected = this.isRowSelected(idx)
const base = this.dark ? 'bg' : 'table'
const variant = this.selectedVariant
return {
'b-table-row-selected': this.selectable && rowSelected,
[`${base}-${variant}`]: this.selectable && rowSelected && variant
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
}
},
selectableRowAttrs(idx) {
return {
'aria-selected': !this.selectable ? null : this.isRowSelected(idx) ? 'true' : 'false'
selectAllRows() {
const length = this.computedItems.length
if (this.isSelectable && length > 0) {
this.selectedLastClicked = -1
this.selectedRows = this.selectableIsMultiSelect ? range(length).map(i => true) : [true]
}
},
isRowSelected(index) {
// Determine if a row is selected (indexed based on `computedItems`)
return Boolean(isNumber(index) && this.selectedRows[index])
},
clearSelected() {
const hasSelection = this.selectedRows.reduce((prev, v) => {
return prev || v
}, false)
if (hasSelection) {
this.selectedLastClicked = -1
this.selectedRows = []
// 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
}
} else {
return {}
}
},
selectableRowAttrs(index) {
return {
'aria-selected': !this.isSelectable ? null : this.isRowSelected(index) ? 'true' : 'false'
}
},
setSelectionHandlers(on) {
Expand All @@ -129,20 +170,20 @@ export default {
},
selectionHandler(item, index, evt) {
/* istanbul ignore if: should never happen */
if (!this.selectable) {
if (!this.isSelectable) {
// Don't do anything if table is not in selectable mode
/* istanbul ignore next: should never happen */
this.clearSelected()
/* istanbul ignore next: should never happen */
return
}
const selectMode = this.selectMode
let selectedRows = this.selectedRows.slice()
let selected = !selectedRows[index]
const mode = this.selectMode
// Note 'multi' mode needs no special handling
if (mode === 'single') {
// Note 'multi' mode needs no special event handling
if (selectMode === 'single') {
selectedRows = []
} else if (mode === 'range') {
} else if (selectMode === 'range') {
if (this.selectedLastRow > -1 && evt.shiftKey) {
// range
for (
Expand All @@ -155,7 +196,7 @@ export default {
selected = true
} else {
if (!(evt.ctrlKey || evt.metaKey)) {
// clear range selection if any
// Clear range selection if any
selectedRows = []
selected = true
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/table/helpers/mixin-tbody-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export default {
}
if (this.selectedRows) {
// Add in rowSelected scope property if selectable rows supported
slotScope.rowSelected = Boolean(this.selectedRows[rowIndex])
slotScope.rowSelected = this.isRowSelected(rowIndex)
}
// TODO:
// Using `field.key` as scoped slot name is deprecated, to be removed in future release
Expand All @@ -203,7 +203,7 @@ export default {
const tableStriped = this.striped
const hasDetailsSlot = this.hasNormalizedSlot(detailsSlotName)
const rowShowDetails = Boolean(item._showDetails && hasDetailsSlot)
const hasRowClickHandler = this.$listeners['row-clicked'] || this.selectable
const hasRowClickHandler = this.$listeners['row-clicked'] || this.isSelectable

// We can return more than one TR if rowDetails enabled
const $rows = []
Expand Down
4 changes: 4 additions & 0 deletions src/components/table/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export declare class BTable extends BvComponent {
// Public methods
refresh: () => void
clearSelected: () => void
selectAllRows: () => void
isRowSelected: (index: number) => boolean
selectRow: (index: number) => void
unselectRow: (index: number) => void
// Props
id?: string
items: Array<any> | BvTableProviderCallback
Expand Down
Loading