1+ import KeyCodes from '../../../utils/key-codes'
2+ import { arrayIncludes } from '../../../utils/array'
3+ import { closest , isElement } from '../../../utils/dom'
14import { props as tbodyProps , BTbody } from '../tbody'
5+ import filterEvent from './filter-event'
6+ import textSelectionActive from './text-selection-active'
27import tbodyRowMixin from './mixin-tbody-row'
38
49const props = {
10+ ...tbodyProps ,
511 tbodyClass : {
612 type : [ String , Array , Object ]
713 // default: undefined
8- } ,
9- ...tbodyProps
14+ }
1015}
1116
1217export default {
1318 mixins : [ tbodyRowMixin ] ,
1419 props,
1520 methods : {
21+ // Helper methods
22+ getTbodyTrs ( ) {
23+ // Returns all the item TR elements (excludes detail and spacer rows)
24+ // `this.$refs.itemRows` is an array of item TR components/elements
25+ // Rows should all be B-TR components, but we map to TR elements
26+ // TODO: This may take time for tables many rows, so we may want to cache
27+ // the result of this during each render cycle on a non-reactive
28+ // property. We clear out the cache as each render starts, and
29+ // populate it on first access of this method if null
30+ return ( this . $refs . itemRows || [ ] ) . map ( tr => tr . $el || tr )
31+ } ,
32+ getTbodyTrIndex ( el ) {
33+ // Returns index of a particular TBODY item TR
34+ // We set `true` on closest to include self in result
35+ /* istanbul ignore next: should not normally happen */
36+ if ( ! isElement ( el ) ) {
37+ return - 1
38+ }
39+ const tr = el . tagName === 'TR' ? el : closest ( 'tr' , el , true )
40+ return tr ? this . getTbodyTrs ( ) . indexOf ( tr ) : - 1
41+ } ,
42+ emitTbodyRowEvent ( type , evt ) {
43+ // Emits a row event, with the item object, row index and original event
44+ if ( type && evt && evt . target ) {
45+ const rowIndex = this . getTbodyTrIndex ( evt . target )
46+ if ( rowIndex > - 1 ) {
47+ // The array of TRs correlate to the `computedItems` array
48+ const item = this . computedItems [ rowIndex ]
49+ this . $emit ( type , item , rowIndex , evt )
50+ }
51+ }
52+ } ,
53+ tbodyRowEvtStopped ( evt ) {
54+ return this . stopIfBusy && this . stopIfBusy ( evt )
55+ } ,
56+ // Delegated row event handlers
57+ onTbodyRowKeydown ( evt ) {
58+ // Keyboard navigation and row click emulation
59+ const target = evt . target
60+ if (
61+ this . tbodyRowEvtStopped ( evt ) ||
62+ target . tagName !== 'TR' ||
63+ target !== document . activeElement ||
64+ target . tabIndex !== 0
65+ ) {
66+ // Early exit if not an item row TR
67+ return
68+ }
69+ const keyCode = evt . keyCode
70+ if ( arrayIncludes ( [ KeyCodes . ENTER , KeyCodes . SPACE ] , keyCode ) ) {
71+ // Emulated click for keyboard users, transfer to click handler
72+ evt . stopPropagation ( )
73+ evt . preventDefault ( )
74+ this . onTBodyRowClicked ( evt )
75+ } else if (
76+ arrayIncludes ( [ KeyCodes . UP , KeyCodes . DOWN , KeyCodes . HOME , KeyCodes . END ] , keyCode )
77+ ) {
78+ // Keyboard navigation
79+ const rowIndex = this . getTbodyTrIndex ( target )
80+ if ( rowIndex > - 1 ) {
81+ evt . stopPropagation ( )
82+ evt . preventDefault ( )
83+ const trs = this . getTbodyTrs ( )
84+ const shift = evt . shiftKey
85+ if ( keyCode === KeyCodes . HOME || ( shift && keyCode === KeyCodes . UP ) ) {
86+ // Focus first row
87+ trs [ 0 ] . focus ( )
88+ } else if ( keyCode === KeyCodes . END || ( shift && keyCode === KeyCodes . DOWN ) ) {
89+ // Focus last row
90+ trs [ trs . length - 1 ] . focus ( )
91+ } else if ( keyCode === KeyCodes . UP && rowIndex > 0 ) {
92+ // Focus previous row
93+ trs [ rowIndex - 1 ] . focus ( )
94+ } else if ( keyCode === KeyCodes . DOWN && rowIndex < trs . length - 1 ) {
95+ // Focus next row
96+ trs [ rowIndex + 1 ] . focus ( )
97+ }
98+ }
99+ }
100+ } ,
101+ onTBodyRowClicked ( evt ) {
102+ if ( this . tbodyRowEvtStopped ( evt ) ) {
103+ // If table is busy, then don't propagate
104+ return
105+ } else if ( filterEvent ( evt ) || textSelectionActive ( this . $el ) ) {
106+ // Clicked on a non-disabled control so ignore
107+ // Or user is selecting text, so ignore
108+ return
109+ }
110+ this . emitTbodyRowEvent ( 'row-clicked' , evt )
111+ } ,
112+ onTbodyRowMiddleMouseRowClicked ( evt ) {
113+ if ( ! this . tbodyRowEvtStopped ( evt ) && evt . which === 2 ) {
114+ this . emitTbodyRowEvent ( 'row-middle-clicked' , evt )
115+ }
116+ } ,
117+ onTbodyRowContextmenu ( evt ) {
118+ if ( ! this . tbodyRowEvtStopped ( evt ) ) {
119+ this . emitTbodyRowEvent ( 'row-contextmenu' , evt )
120+ }
121+ } ,
122+ onTbodyRowDblClicked ( evt ) {
123+ if ( ! this . tbodyRowEvtStopped ( evt ) && ! filterEvent ( evt ) ) {
124+ this . emitTbodyRowEvent ( 'row-dblclicked' , evt )
125+ }
126+ } ,
127+ // Note: Row hover handlers are handled by the tbody-row mixin
128+ // As mouseenter/mouseleave events do not bubble
129+ //
130+ // Render Helper
16131 renderTbody ( ) {
17132 // Render the tbody element and children
18133 const items = this . computedItems
19134 // Shortcut to `createElement` (could use `this._c()` instead)
20135 const h = this . $createElement
136+ const hasRowClickHandler = this . $listeners [ 'row-clicked' ] || this . isSelectable
21137
22138 // Prepare the tbody rows
23139 const $rows = [ ]
@@ -30,10 +146,10 @@ export default {
30146 } else {
31147 // Table isn't busy, or we don't have a busy slot
32148
33- // Create a slot cache for improved performace when looking up cell slot names.
34- // Values will be keyed by the field's `key` and will store the slot's name.
35- // Slots could be dynamic (i.e. `v-if`), so we must compute on each render.
36- // Used by tbodyRow mixin render helper.
149+ // Create a slot cache for improved performance when looking up cell slot names
150+ // Values will be keyed by the field's `key` and will store the slot's name
151+ // Slots could be dynamic (i.e. `v-if`), so we must compute on each render
152+ // Used by tbody-row mixin render helper
37153 const cache = { }
38154 const defaultSlotName = this . hasNormalizedSlot ( 'cell()' ) ? 'cell()' : null
39155 this . computedFields . forEach ( field => {
@@ -46,35 +162,57 @@ export default {
46162 ? lowerName
47163 : defaultSlotName
48164 } )
49- // Created as a non-reactive property so to not trigger component updates.
50- // Must be a fresh object each render.
165+ // Created as a non-reactive property so to not trigger component updates
166+ // Must be a fresh object each render
51167 this . $_bodyFieldSlotNameCache = cache
52168
53- // Add static Top Row slot (hidden in visibly stacked mode as we can't control data-label attr)
169+ // Add static top row slot (hidden in visibly stacked mode
170+ // as we can't control `data-label` attr)
54171 $rows . push ( this . renderTopRow ? this . renderTopRow ( ) : h ( ) )
55172
56- // render the rows
173+ // Render the rows
57174 items . forEach ( ( item , rowIndex ) => {
58175 // Render the individual item row (rows if details slot)
59176 $rows . push ( this . renderTbodyRow ( item , rowIndex ) )
60177 } )
61178
62- // Empty Items / Empty Filtered Row slot (only shows if items.length < 1)
179+ // Empty items / empty filtered row slot (only shows if ` items.length < 1` )
63180 $rows . push ( this . renderEmpty ? this . renderEmpty ( ) : h ( ) )
64181
65- // Static bottom row slot (hidden in visibly stacked mode as we can't control data-label attr)
182+ // Static bottom row slot (hidden in visibly stacked mode
183+ // as we can't control `data-label` attr)
66184 $rows . push ( this . renderBottomRow ? this . renderBottomRow ( ) : h ( ) )
67185 }
68186
187+ const handlers = {
188+ // TODO: We may want to to only instantiate these handlers
189+ // if there is an event listener registered
190+ auxclick : this . onTbodyRowMiddleMouseRowClicked ,
191+ // TODO: Perhaps we do want to automatically prevent the
192+ // default context menu from showing if there is
193+ // a `row-contextmenu` listener registered.
194+ contextmenu : this . onTbodyRowContextmenu ,
195+ // The following event(s) is not considered A11Y friendly
196+ dblclick : this . onTbodyRowDblClicked
197+ // hover events (mouseenter/mouseleave) ad handled by tbody-row mixin
198+ }
199+ if ( hasRowClickHandler ) {
200+ handlers . click = this . onTBodyRowClicked
201+ handlers . keydown = this . onTbodyRowKeydown
202+ }
69203 // Assemble rows into the tbody
70204 const $tbody = h (
71205 BTbody ,
72206 {
207+ ref : 'tbody' ,
73208 class : this . tbodyClass || null ,
74209 props : {
75210 tbodyTransitionProps : this . tbodyTransitionProps ,
76211 tbodyTransitionHandlers : this . tbodyTransitionHandlers
77- }
212+ } ,
213+ // BTbody transfers all native event listeners to the root element
214+ // TODO: Only set the handlers if the table is not busy
215+ on : handlers
78216 } ,
79217 $rows
80218 )
0 commit comments