1
+ import KeyCodes from '../../../utils/key-codes'
2
+ import { arrayIncludes } from '../../../utils/array'
3
+ import { closest , isElement } from '../../../utils/dom'
1
4
import { props as tbodyProps , BTbody } from '../tbody'
5
+ import filterEvent from './filter-event'
6
+ import textSelectionActive from './text-selection-active'
2
7
import tbodyRowMixin from './mixin-tbody-row'
3
8
4
9
const props = {
10
+ ...tbodyProps ,
5
11
tbodyClass : {
6
12
type : [ String , Array , Object ]
7
13
// default: undefined
8
- } ,
9
- ...tbodyProps
14
+ }
10
15
}
11
16
12
17
export default {
13
18
mixins : [ tbodyRowMixin ] ,
14
19
props,
15
20
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
16
131
renderTbody ( ) {
17
132
// Render the tbody element and children
18
133
const items = this . computedItems
19
134
// Shortcut to `createElement` (could use `this._c()` instead)
20
135
const h = this . $createElement
136
+ const hasRowClickHandler = this . $listeners [ 'row-clicked' ] || this . isSelectable
21
137
22
138
// Prepare the tbody rows
23
139
const $rows = [ ]
@@ -30,10 +146,10 @@ export default {
30
146
} else {
31
147
// Table isn't busy, or we don't have a busy slot
32
148
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
37
153
const cache = { }
38
154
const defaultSlotName = this . hasNormalizedSlot ( 'cell()' ) ? 'cell()' : null
39
155
this . computedFields . forEach ( field => {
@@ -46,35 +162,57 @@ export default {
46
162
? lowerName
47
163
: defaultSlotName
48
164
} )
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
51
167
this . $_bodyFieldSlotNameCache = cache
52
168
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)
54
171
$rows . push ( this . renderTopRow ? this . renderTopRow ( ) : h ( ) )
55
172
56
- // render the rows
173
+ // Render the rows
57
174
items . forEach ( ( item , rowIndex ) => {
58
175
// Render the individual item row (rows if details slot)
59
176
$rows . push ( this . renderTbodyRow ( item , rowIndex ) )
60
177
} )
61
178
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` )
63
180
$rows . push ( this . renderEmpty ? this . renderEmpty ( ) : h ( ) )
64
181
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)
66
184
$rows . push ( this . renderBottomRow ? this . renderBottomRow ( ) : h ( ) )
67
185
}
68
186
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
+ }
69
203
// Assemble rows into the tbody
70
204
const $tbody = h (
71
205
BTbody ,
72
206
{
207
+ ref : 'tbody' ,
73
208
class : this . tbodyClass || null ,
74
209
props : {
75
210
tbodyTransitionProps : this . tbodyTransitionProps ,
76
211
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
78
216
} ,
79
217
$rows
80
218
)
0 commit comments