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

361 lines (339 loc) 13.8 kB
import { extend, REF_FOR_KEY } from '../../../vue' import { EVENT_NAME_ROW_CLICKED, EVENT_NAME_ROW_HOVERED, EVENT_NAME_ROW_UNHOVERED } from '../../../constants/events' import { PROP_TYPE_ARRAY_OBJECT_STRING, PROP_TYPE_FUNCTION, PROP_TYPE_OBJECT_FUNCTION } from '../../../constants/props' import { SLOT_NAME_ROW_DETAILS } from '../../../constants/slots' import { useParentMixin } from '../../../mixins/use-parent' import { get } from '../../../utils/get' import { isFunction, isString, isUndefinedOrNull } from '../../../utils/inspect' import { makeProp } from '../../../utils/props' import { safeVueInstance } from '../../../utils/safe-vue-instance' import { toString } from '../../../utils/string' import { BTr } from '../tr' import { BTd } from '../td' import { BTh } from '../th' import { FIELD_KEY_CELL_VARIANT, FIELD_KEY_ROW_VARIANT, FIELD_KEY_SHOW_DETAILS } from './constants' // --- Props --- export const props = { detailsTdClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING), tbodyTrAttr: makeProp(PROP_TYPE_OBJECT_FUNCTION), tbodyTrClass: makeProp([...PROP_TYPE_ARRAY_OBJECT_STRING, PROP_TYPE_FUNCTION]) } // --- Mixin --- // @vue/component export const tbodyRowMixin = extend({ mixins: [useParentMixin], props, methods: { // Methods for computing classes, attributes and styles for table cells getTdValues(item, key, tdValue, defaultValue) { const { bvParent } = this if (tdValue) { const value = get(item, key, '') if (isFunction(tdValue)) { return tdValue(value, key, item) } else if (isString(tdValue) && isFunction(bvParent[tdValue])) { return bvParent[tdValue](value, key, item) } return tdValue } return defaultValue }, getThValues(item, key, thValue, type, defaultValue) { const { bvParent } = this if (thValue) { const value = get(item, key, '') if (isFunction(thValue)) { return thValue(value, key, item, type) } else if (isString(thValue) && isFunction(bvParent[thValue])) { return bvParent[thValue](value, key, item, type) } return thValue } return defaultValue }, // Method to get the value for a field getFormattedValue(item, field) { const { key } = field const formatter = this.getFieldFormatter(key) let value = get(item, key, null) if (isFunction(formatter)) { value = formatter(value, key, item) } return isUndefinedOrNull(value) ? '' : value }, // Factory function methods toggleDetailsFactory(hasDetailsSlot, item) { // Returns a function to toggle a row's details slot return () => { if (hasDetailsSlot) { this.$set(item, FIELD_KEY_SHOW_DETAILS, !item[FIELD_KEY_SHOW_DETAILS]) } } }, // Row event handlers rowHovered(event) { // `mouseenter` handler (non-bubbling) // `this.tbodyRowEventStopped` from tbody mixin if (!this.tbodyRowEventStopped(event)) { // `this.emitTbodyRowEvent` from tbody mixin this.emitTbodyRowEvent(EVENT_NAME_ROW_HOVERED, event) } }, rowUnhovered(event) { // `mouseleave` handler (non-bubbling) // `this.tbodyRowEventStopped` from tbody mixin if (!this.tbodyRowEventStopped(event)) { // `this.emitTbodyRowEvent` from tbody mixin this.emitTbodyRowEvent(EVENT_NAME_ROW_UNHOVERED, event) } }, // Renders a TD or TH for a row's field renderTbodyRowCell(field, colIndex, item, rowIndex) { const { isStacked } = this const { key, label, isRowHeader } = field const h = this.$createElement const hasDetailsSlot = this.hasNormalizedSlot(SLOT_NAME_ROW_DETAILS) const formatted = this.getFormattedValue(item, field) const stickyColumn = !isStacked && (this.isResponsive || this.stickyHeader) && field.stickyColumn // We only uses the helper components for sticky columns to // improve performance of BTable/BTableLite by reducing the // total number of vue instances created during render const cellTag = stickyColumn ? (isRowHeader ? BTh : BTd) : isRowHeader ? 'th' : 'td' const cellVariant = item[FIELD_KEY_CELL_VARIANT] && item[FIELD_KEY_CELL_VARIANT][key] ? item[FIELD_KEY_CELL_VARIANT][key] : field.variant || null const data = { // For the Vue key, we concatenate the column index and // field key (as field keys could be duplicated) // TODO: Although we do prevent duplicate field keys... // So we could change this to: `row-${rowIndex}-cell-${key}` class: [field.class ? field.class : '', this.getTdValues(item, key, field.tdClass, '')], props: {}, attrs: { 'aria-colindex': String(colIndex + 1), ...(isRowHeader ? this.getThValues(item, key, field.thAttr, 'row', {}) : this.getTdValues(item, key, field.tdAttr, {})) }, key: `row-${rowIndex}-cell-${colIndex}-${key}` } if (stickyColumn) { // We are using the helper BTd or BTh data.props = { stackedHeading: isStacked ? label : null, stickyColumn: true, variant: cellVariant } } else { // Using native TD or TH element, so we need to // add in the attributes and variant class data.attrs['data-label'] = isStacked && !isUndefinedOrNull(label) ? toString(label) : null data.attrs.role = isRowHeader ? 'rowheader' : 'cell' data.attrs.scope = isRowHeader ? 'row' : null // Add in the variant class if (cellVariant) { data.class.push(`${this.dark ? 'bg' : 'table'}-${cellVariant}`) } } const slotScope = { item, index: rowIndex, field, unformatted: get(item, key, ''), value: formatted, toggleDetails: this.toggleDetailsFactory(hasDetailsSlot, item), detailsShowing: Boolean(item[FIELD_KEY_SHOW_DETAILS]) } // If table supports selectable mode, then add in the following scope // this.supportsSelectableRows will be undefined if mixin isn't loaded if (safeVueInstance(this).supportsSelectableRows) { slotScope.rowSelected = this.isRowSelected(rowIndex) slotScope.selectRow = () => this.selectRow(rowIndex) slotScope.unselectRow = () => this.unselectRow(rowIndex) } // The new `v-slot` syntax doesn't like a slot name starting with // a square bracket and if using in-document HTML templates, the // v-slot attributes are lower-cased by the browser. // Switched to round bracket syntax to prevent confusion with // dynamic slot name syntax. // We look for slots in this order: `cell(${key})`, `cell(${key.toLowerCase()})`, 'cell()' // Slot names are now cached by mixin tbody in `this.$_bodyFieldSlotNameCache` // Will be `null` if no slot (or fallback slot) exists const slotName = this.$_bodyFieldSlotNameCache[key] let $childNodes = slotName ? this.normalizeSlot(slotName, slotScope) : toString(formatted) if (this.isStacked) { // We wrap in a DIV to ensure rendered as a single cell when visually stacked! $childNodes = [h('div', [$childNodes])] } // Render either a td or th cell return h(cellTag, data, [$childNodes]) }, // Renders an item's row (or rows if details supported) renderTbodyRow(item, rowIndex) { const { computedFields: fields, striped, primaryKey, currentPage, perPage, tbodyTrClass, tbodyTrAttr, hasSelectableRowClick } = safeVueInstance(this) const h = this.$createElement const hasDetailsSlot = this.hasNormalizedSlot(SLOT_NAME_ROW_DETAILS) const rowShowDetails = item[FIELD_KEY_SHOW_DETAILS] && hasDetailsSlot const hasRowClickHandler = this.$listeners[EVENT_NAME_ROW_CLICKED] || hasSelectableRowClick // We can return more than one TR if rowDetails enabled const $rows = [] // Details ID needed for `aria-details` when details showing // We set it to `null` when not showing so that attribute // does not appear on the element const detailsId = rowShowDetails ? this.safeId(`_details_${rowIndex}_`) : null // For each item data field in row const $tds = fields.map((field, colIndex) => { return this.renderTbodyRowCell(field, colIndex, item, rowIndex) }) // Calculate the row number in the dataset (indexed from 1) let ariaRowIndex = null if (currentPage && perPage && perPage > 0) { ariaRowIndex = String((currentPage - 1) * perPage + rowIndex + 1) } // Create a unique :key to help ensure that sub components are re-rendered rather than // re-used, which can cause issues. If a primary key is not provided we use the rendered // rows index within the tbody. // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/2410 const primaryKeyValue = toString(get(item, primaryKey)) || null const rowKey = primaryKeyValue || toString(rowIndex) // If primary key is provided, use it to generate a unique ID on each tbody > tr // In the format of '{tableId}__row_{primaryKeyValue}' const rowId = primaryKeyValue ? this.safeId(`_row_${primaryKeyValue}`) : null // Selectable classes and attributes const selectableClasses = safeVueInstance(this).selectableRowClasses ? this.selectableRowClasses(rowIndex) : {} const selectableAttrs = safeVueInstance(this).selectableRowAttrs ? this.selectableRowAttrs(rowIndex) : {} // Additional classes and attributes const userTrClasses = isFunction(tbodyTrClass) ? tbodyTrClass(item, 'row') : tbodyTrClass const userTrAttrs = isFunction(tbodyTrAttr) ? /* istanbul ignore next */ tbodyTrAttr(item, 'row') : tbodyTrAttr // Add the item row $rows.push( h( BTr, { class: [userTrClasses, selectableClasses, rowShowDetails ? 'b-table-has-details' : ''], props: { variant: item[FIELD_KEY_ROW_VARIANT] || null }, attrs: { id: rowId, ...userTrAttrs, // Users cannot override the following attributes tabindex: hasRowClickHandler ? '0' : null, 'data-pk': primaryKeyValue || null, 'aria-details': detailsId, 'aria-owns': detailsId, 'aria-rowindex': ariaRowIndex, ...selectableAttrs }, on: { // Note: These events are not A11Y friendly! mouseenter: this.rowHovered, mouseleave: this.rowUnhovered }, key: `__b-table-row-${rowKey}__`, ref: 'item-rows', [REF_FOR_KEY]: true }, $tds ) ) // Row Details slot if (rowShowDetails) { const detailsScope = { item, index: rowIndex, fields, toggleDetails: this.toggleDetailsFactory(hasDetailsSlot, item) } // If table supports selectable mode, then add in the following scope // this.supportsSelectableRows will be undefined if mixin isn't loaded if (safeVueInstance(this).supportsSelectableRows) { detailsScope.rowSelected = this.isRowSelected(rowIndex) detailsScope.selectRow = () => this.selectRow(rowIndex) detailsScope.unselectRow = () => this.unselectRow(rowIndex) } // Render the details slot in a TD const $details = h( BTd, { props: { colspan: fields.length }, class: this.detailsTdClass }, [this.normalizeSlot(SLOT_NAME_ROW_DETAILS, detailsScope)] ) // Add a hidden row to keep table row striping consistent when details showing // Only added if the table is striped if (striped) { $rows.push( // We don't use `BTr` here as we don't need the extra functionality h('tr', { staticClass: 'd-none', attrs: { 'aria-hidden': 'true', role: 'presentation' }, key: `__b-table-details-stripe__${rowKey}` }) ) } // Add the actual details row const userDetailsTrClasses = isFunction(this.tbodyTrClass) ? /* istanbul ignore next */ this.tbodyTrClass(item, SLOT_NAME_ROW_DETAILS) : this.tbodyTrClass const userDetailsTrAttrs = isFunction(this.tbodyTrAttr) ? /* istanbul ignore next */ this.tbodyTrAttr(item, SLOT_NAME_ROW_DETAILS) : this.tbodyTrAttr $rows.push( h( BTr, { staticClass: 'b-table-details', class: [userDetailsTrClasses], props: { variant: item[FIELD_KEY_ROW_VARIANT] || null }, attrs: { ...userDetailsTrAttrs, // Users cannot override the following attributes id: detailsId, tabindex: '-1' }, key: `__b-table-details__${rowKey}` }, [$details] ) ) } else if (hasDetailsSlot) { // Only add the placeholder if a the table has a row-details slot defined (but not shown) $rows.push(h()) if (striped) { // Add extra placeholder if table is striped $rows.push(h()) } } // Return the row(s) return $rows } } })