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
)
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4