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

259 lines (241 loc) 9.57 kB
import { extend } from '../../../vue' import { EVENT_NAME_ROW_CLICKED, EVENT_NAME_ROW_CONTEXTMENU, EVENT_NAME_ROW_DBLCLICKED, EVENT_NAME_ROW_MIDDLE_CLICKED } from '../../../constants/events' import { CODE_DOWN, CODE_END, CODE_ENTER, CODE_HOME, CODE_SPACE, CODE_UP } from '../../../constants/key-codes' import { PROP_TYPE_ARRAY_OBJECT_STRING } from '../../../constants/props' import { arrayIncludes, from as arrayFrom } from '../../../utils/array' import { attemptFocus, closest, isActiveElement, isElement } from '../../../utils/dom' import { safeVueInstance } from '../../../utils/safe-vue-instance' import { stopEvent } from '../../../utils/events' import { sortKeys } from '../../../utils/object' import { makeProp, pluckProps } from '../../../utils/props' import { BTbody, props as BTbodyProps } from '../tbody' import { filterEvent } from './filter-event' import { textSelectionActive } from './text-selection-active' import { tbodyRowMixin, props as tbodyRowProps } from './mixin-tbody-row' // --- Helper methods --- const getCellSlotName = value => `cell(${value || ''})` // --- Props --- export const props = sortKeys({ ...BTbodyProps, ...tbodyRowProps, tbodyClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING) }) // --- Mixin --- // @vue/component export const tbodyMixin = extend({ mixins: [tbodyRowMixin], props, beforeDestroy() { this.$_bodyFieldSlotNameCache = null }, methods: { // Returns all the item TR elements (excludes detail and spacer rows) // `this.$refs['item-rows']` is an array of item TR components/elements // Rows should all be `<b-tr>` components, but we map to TR elements // Also note that `this.$refs['item-rows']` may not always be in document order getTbodyTrs() { const { $refs } = this const tbody = $refs.tbody ? $refs.tbody.$el || $refs.tbody : null const trs = ($refs['item-rows'] || []).map(tr => tr.$el || tr) return tbody && tbody.children && tbody.children.length > 0 && trs && trs.length > 0 ? arrayFrom(tbody.children).filter(tr => arrayIncludes(trs, tr)) : /* istanbul ignore next */ [] }, // Returns index of a particular TBODY item TR // We set `true` on closest to include self in result getTbodyTrIndex(el) { /* istanbul ignore next: should not normally happen */ if (!isElement(el)) { return -1 } const tr = el.tagName === 'TR' ? el : closest('tr', el, true) return tr ? this.getTbodyTrs().indexOf(tr) : -1 }, // Emits a row event, with the item object, row index and original event emitTbodyRowEvent(type, event) { if (type && this.hasListener(type) && event && event.target) { const rowIndex = this.getTbodyTrIndex(event.target) if (rowIndex > -1) { // The array of TRs correlate to the `computedItems` array const item = this.computedItems[rowIndex] this.$emit(type, item, rowIndex, event) } } }, tbodyRowEventStopped(event) { return this.stopIfBusy && this.stopIfBusy(event) }, // Delegated row event handlers onTbodyRowKeydown(event) { // Keyboard navigation and row click emulation const { target, keyCode } = event if ( this.tbodyRowEventStopped(event) || target.tagName !== 'TR' || !isActiveElement(target) || target.tabIndex !== 0 ) { // Early exit if not an item row TR return } if (arrayIncludes([CODE_ENTER, CODE_SPACE], keyCode)) { // Emulated click for keyboard users, transfer to click handler stopEvent(event) this.onTBodyRowClicked(event) } else if (arrayIncludes([CODE_UP, CODE_DOWN, CODE_HOME, CODE_END], keyCode)) { // Keyboard navigation const rowIndex = this.getTbodyTrIndex(target) if (rowIndex > -1) { stopEvent(event) const trs = this.getTbodyTrs() const shift = event.shiftKey if (keyCode === CODE_HOME || (shift && keyCode === CODE_UP)) { // Focus first row attemptFocus(trs[0]) } else if (keyCode === CODE_END || (shift && keyCode === CODE_DOWN)) { // Focus last row attemptFocus(trs[trs.length - 1]) } else if (keyCode === CODE_UP && rowIndex > 0) { // Focus previous row attemptFocus(trs[rowIndex - 1]) } else if (keyCode === CODE_DOWN && rowIndex < trs.length - 1) { // Focus next row attemptFocus(trs[rowIndex + 1]) } } } }, onTBodyRowClicked(event) { const { $refs } = this const tbody = $refs.tbody ? $refs.tbody.$el || $refs.tbody : null // Don't emit event when the table is busy, the user clicked // on a non-disabled control or is selecting text if ( this.tbodyRowEventStopped(event) || filterEvent(event) || textSelectionActive(tbody || this.$el) ) { return } this.emitTbodyRowEvent(EVENT_NAME_ROW_CLICKED, event) }, onTbodyRowMiddleMouseRowClicked(event) { if (!this.tbodyRowEventStopped(event) && event.which === 2) { this.emitTbodyRowEvent(EVENT_NAME_ROW_MIDDLE_CLICKED, event) } }, onTbodyRowContextmenu(event) { if (!this.tbodyRowEventStopped(event)) { this.emitTbodyRowEvent(EVENT_NAME_ROW_CONTEXTMENU, event) } }, onTbodyRowDblClicked(event) { if (!this.tbodyRowEventStopped(event) && !filterEvent(event)) { this.emitTbodyRowEvent(EVENT_NAME_ROW_DBLCLICKED, event) } }, // Render the tbody element and children // Note: // Row hover handlers are handled by the tbody-row mixin // As mouseenter/mouseleave events do not bubble renderTbody() { const { computedItems: items, renderBusy, renderTopRow, renderEmpty, renderBottomRow, hasSelectableRowClick } = safeVueInstance(this) const h = this.$createElement const hasRowClickHandler = this.hasListener(EVENT_NAME_ROW_CLICKED) || hasSelectableRowClick // Prepare the tbody rows const $rows = [] // Add the item data rows or the busy slot const $busy = renderBusy ? renderBusy() : null if ($busy) { // If table is busy and a busy slot, then return only the busy "row" indicator $rows.push($busy) } else { // Table isn't busy, or we don't have a busy slot // Create a slot cache for improved performance when looking up cell slot names // Values will be keyed by the field's `key` and will store the slot's name // Slots could be dynamic (i.e. `v-if`), so we must compute on each render // Used by tbody-row mixin render helper const cache = {} let defaultSlotName = getCellSlotName() defaultSlotName = this.hasNormalizedSlot(defaultSlotName) ? defaultSlotName : null this.computedFields.forEach(field => { const { key } = field const slotName = getCellSlotName(key) const lowercaseSlotName = getCellSlotName(key.toLowerCase()) cache[key] = this.hasNormalizedSlot(slotName) ? slotName : this.hasNormalizedSlot(lowercaseSlotName) ? /* istanbul ignore next */ lowercaseSlotName : defaultSlotName }) // Created as a non-reactive property so to not trigger component updates // Must be a fresh object each render this.$_bodyFieldSlotNameCache = cache // Add static top row slot (hidden in visibly stacked mode // as we can't control `data-label` attr) $rows.push(renderTopRow ? renderTopRow() : h()) // Render the rows items.forEach((item, rowIndex) => { // Render the individual item row (rows if details slot) $rows.push(this.renderTbodyRow(item, rowIndex)) }) // Empty items / empty filtered row slot (only shows if `items.length < 1`) $rows.push(renderEmpty ? renderEmpty() : h()) // Static bottom row slot (hidden in visibly stacked mode // as we can't control `data-label` attr) $rows.push(renderBottomRow ? renderBottomRow() : h()) } // Note: these events will only emit if a listener is registered const handlers = { auxclick: this.onTbodyRowMiddleMouseRowClicked, // TODO: // Perhaps we do want to automatically prevent the // default context menu from showing if there is a // `row-contextmenu` listener registered contextmenu: this.onTbodyRowContextmenu, // The following event(s) is not considered A11Y friendly dblclick: this.onTbodyRowDblClicked // Hover events (`mouseenter`/`mouseleave`) are handled by `tbody-row` mixin } // Add in click/keydown listeners if needed if (hasRowClickHandler) { handlers.click = this.onTBodyRowClicked handlers.keydown = this.onTbodyRowKeydown } // Assemble rows into the tbody const $tbody = h( BTbody, { class: this.tbodyClass || null, props: pluckProps(BTbodyProps, this.$props), // BTbody transfers all native event listeners to the root element // TODO: Only set the handlers if the table is not busy on: handlers, ref: 'tbody' }, $rows ) // Return the assembled tbody return $tbody } } })