🌐 AI搜索 & 代理 主页
Skip to content

Commit 110e81f

Browse files
committed
feat(b-table): Add header contextmenu event (fixes #5841)
1 parent 1d59417 commit 110e81f

File tree

4 files changed

+234
-29
lines changed

4 files changed

+234
-29
lines changed

src/components/table/helpers/mixin-thead.js

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Vue } from '../../../vue'
2-
import { EVENT_NAME_HEAD_CLICKED } from '../../../constants/events'
2+
import { EVENT_NAME_HEAD_CLICKED, EVENT_NAME_HEAD_CONTEXTMENU } from '../../../constants/events'
33
import { CODE_ENTER, CODE_SPACE } from '../../../constants/key-codes'
44
import { PROP_TYPE_ARRAY_OBJECT_STRING, PROP_TYPE_STRING } from '../../../constants/props'
55
import { SLOT_NAME_THEAD_TOP } from '../../../constants/slots'
@@ -59,6 +59,21 @@ export const theadMixin = Vue.extend({
5959
stopEvent(event)
6060
this.$emit(EVENT_NAME_HEAD_CLICKED, field.key, field, event, isFoot)
6161
},
62+
headContextMenu(event, field, isFoot) {
63+
if (this.stopIfBusy && this.stopIfBusy(event)) {
64+
// If table is busy (via provider) then don't propagate
65+
return
66+
} else if (filterEvent(event)) {
67+
// Clicked on a non-disabled control so ignore
68+
return
69+
} else if (textSelectionActive(this.$el)) {
70+
// User is selecting text, so ignore
71+
/* istanbul ignore next: JSDOM doesn't support getSelection() */
72+
return
73+
}
74+
stopEvent(event)
75+
this.$emit(EVENT_NAME_HEAD_CONTEXTMENU, field.key, field, event, isFoot)
76+
},
6277
renderThead(isFoot = false) {
6378
const {
6479
computedFields: fields,
@@ -77,7 +92,12 @@ export const theadMixin = Vue.extend({
7792
return h()
7893
}
7994

80-
const hasHeadClickListener = isSortable || this.hasListener(EVENT_NAME_HEAD_CLICKED)
95+
const hasHeadClickListener =
96+
isSortable ||
97+
this.hasListener(EVENT_NAME_HEAD_CLICKED) ||
98+
this.hasListener(EVENT_NAME_HEAD_CONTEXTMENU)
99+
100+
const hasHeadContextMenuListener = this.hasListener(EVENT_NAME_HEAD_CONTEXTMENU)
81101

82102
// Reference to `selectAllRows` and `clearSelected()`, if table is selectable
83103
const selectAllRows = isSelectable ? this.selectAllRows : noop
@@ -108,6 +128,12 @@ export const theadMixin = Vue.extend({
108128
}
109129
}
110130

131+
if (hasHeadContextMenuListener) {
132+
on.contextmenu = event => {
133+
this.headContextMenu(event, field, isFoot)
134+
}
135+
}
136+
111137
const sortAttrs = isSortable ? this.sortTheadThAttrs(key, field, isFoot) : {}
112138
const sortClass = isSortable ? this.sortTheadThClasses(key, field, isFoot) : null
113139
const sortLabel = isSortable ? this.sortTheadThLabel(key, field, isFoot) : null
@@ -159,19 +185,13 @@ export const theadMixin = Vue.extend({
159185
]
160186
}
161187

162-
const scope = {
163-
label,
164-
column: key,
165-
field,
166-
isFoot,
167-
// Add in row select methods
168-
selectAllRows,
169-
clearSelected
170-
}
188+
const scope = { label, column: key, field, isFoot, selectAllRows, clearSelected } // Add in row select methods
171189

172190
const $content =
173191
this.normalizeSlot(slotNames, scope) ||
174-
h('div', { domProps: htmlOrText(labelHtml, label) })
192+
h('div', {
193+
domProps: htmlOrText(labelHtml, label)
194+
})
175195

176196
const $srLabel = sortLabel ? h('span', { staticClass: 'sr-only' }, ` (${sortLabel})`) : null
177197

@@ -200,25 +220,10 @@ export const theadMixin = Vue.extend({
200220
)
201221
)
202222
} else {
203-
const scope = {
204-
columns: fields.length,
205-
fields,
206-
// Add in row select methods
207-
selectAllRows,
208-
clearSelected
209-
}
223+
const scope = { columns: fields.length, fields, selectAllRows, clearSelected } // Add in row select methods
210224
$trs.push(this.normalizeSlot(SLOT_NAME_THEAD_TOP, scope) || h())
211225

212-
$trs.push(
213-
h(
214-
BTr,
215-
{
216-
class: this.theadTrClass,
217-
props: { variant: headRowVariant }
218-
},
219-
$cells
220-
)
221-
)
226+
$trs.push(h(BTr, { class: this.theadTrClass, props: { variant: headRowVariant } }, $cells))
222227
}
223228

224229
return h(

src/components/table/package.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,35 @@
359359
}
360360
]
361361
},
362+
{
363+
"event": "head-contextmenu",
364+
"description": "Emitted when a header or footer cell is context/right clicked. Not applicable for 'custom-foot' slot",
365+
"args": [
366+
{
367+
"arg": "key",
368+
"type": "String",
369+
"description": "Column key clicked (field name)"
370+
},
371+
{
372+
"arg": "field",
373+
"type": "Object",
374+
"description": "Field definition object"
375+
},
376+
{
377+
"arg": "event",
378+
"type": [
379+
"MouseEvent",
380+
"KeyboardEvent"
381+
],
382+
"description": "Native event object"
383+
},
384+
{
385+
"arg": "isFooter",
386+
"type": "Boolean",
387+
"description": "'True' if this event originated from clicking on the footer cell"
388+
}
389+
]
390+
},
362391
{
363392
"event": "refreshed",
364393
"description": "Emitted when the items provider function has returned data"
@@ -1183,6 +1212,35 @@
11831212
}
11841213
]
11851214
},
1215+
{
1216+
"event": "head-contextmenu",
1217+
"description": "Emitted when a header or footer cell is context/right clicked. Not applicable for 'custom-foot' slot",
1218+
"args": [
1219+
{
1220+
"arg": "key",
1221+
"type": "String",
1222+
"description": "Column key clicked (field name)"
1223+
},
1224+
{
1225+
"arg": "field",
1226+
"type": "Object",
1227+
"description": "Field definition object"
1228+
},
1229+
{
1230+
"arg": "event",
1231+
"type": [
1232+
"MouseEvent",
1233+
"KeyboardEvent"
1234+
],
1235+
"description": "Native event object"
1236+
},
1237+
{
1238+
"arg": "isFooter",
1239+
"type": "Boolean",
1240+
"description": "'True' if this event originated from clicking on the footer cell"
1241+
}
1242+
]
1243+
},
11861244
{
11871245
"event": "row-clicked",
11881246
"description": "Emitted when a row is clicked",

src/components/table/table-thead-events.spec.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,28 @@ describe('table > thead events', () => {
2727
expect(wrapper.emitted('head-clicked')).toBeUndefined()
2828
})
2929

30+
it('should not emit head-contextmenu event when a head cell is clicked and no head-contextmenu listener', async () => {
31+
const wrapper = mount(BTable, {
32+
propsData: {
33+
fields: testFields,
34+
items: testItems
35+
},
36+
listeners: {}
37+
})
38+
expect(wrapper).toBeDefined()
39+
const $rows = wrapper.findAll('thead > tr')
40+
expect($rows.length).toBe(1)
41+
const $ths = wrapper.findAll('thead > tr > th')
42+
expect($ths.length).toBe(testFields.length)
43+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
44+
await $ths.at(0).trigger('contextmenu')
45+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
46+
await $ths.at(1).trigger('contextmenu')
47+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
48+
await $ths.at(2).trigger('contextmenu')
49+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
50+
})
51+
3052
it('should emit head-clicked event when a head cell is clicked', async () => {
3153
const wrapper = mount(BTable, {
3254
propsData: {
@@ -62,6 +84,41 @@ describe('table > thead events', () => {
6284
wrapper.destroy()
6385
})
6486

87+
it('should emit head-contextmenu event when a head cell is context clicked', async () => {
88+
const wrapper = mount(BTable, {
89+
propsData: {
90+
fields: testFields,
91+
items: testItems
92+
},
93+
listeners: {
94+
// Head-contextmenu will only be emitted if there is a registered listener
95+
'head-contextmenu': () => {}
96+
}
97+
})
98+
expect(wrapper).toBeDefined()
99+
const $rows = wrapper.findAll('thead > tr')
100+
expect($rows.length).toBe(1)
101+
const $ths = wrapper.findAll('thead > tr > th')
102+
expect($ths.length).toBe(testFields.length)
103+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
104+
await $ths.at(0).trigger('contextmenu')
105+
expect(wrapper.emitted('head-contextmenu')).toBeDefined()
106+
expect(wrapper.emitted('head-contextmenu').length).toBe(1)
107+
expect(wrapper.emitted('head-contextmenu')[0][0]).toEqual(testFields[0].key) // Field key
108+
expect(wrapper.emitted('head-contextmenu')[0][1]).toEqual(testFields[0]) // Field definition
109+
expect(wrapper.emitted('head-contextmenu')[0][2]).toBeInstanceOf(MouseEvent) // Event
110+
expect(wrapper.emitted('head-contextmenu')[0][3]).toBe(false) // Is footer
111+
112+
await $ths.at(2).trigger('contextmenu')
113+
expect(wrapper.emitted('head-contextmenu').length).toBe(2)
114+
expect(wrapper.emitted('head-contextmenu')[1][0]).toEqual(testFields[2].key) // Field key
115+
expect(wrapper.emitted('head-contextmenu')[1][1]).toEqual(testFields[2]) // Field definition
116+
expect(wrapper.emitted('head-contextmenu')[1][2]).toBeInstanceOf(MouseEvent) // Event
117+
expect(wrapper.emitted('head-contextmenu')[1][3]).toBe(false) // Is footer
118+
119+
wrapper.destroy()
120+
})
121+
65122
it('should not emit head-clicked event when prop busy is set', async () => {
66123
const wrapper = mount(BTable, {
67124
propsData: {
@@ -84,6 +141,28 @@ describe('table > thead events', () => {
84141
wrapper.destroy()
85142
})
86143

144+
it('should not emit head-contextmenu event when prop busy is set', async () => {
145+
const wrapper = mount(BTable, {
146+
propsData: {
147+
fields: testFields,
148+
items: testItems,
149+
busy: true
150+
},
151+
listeners: {
152+
// Head-contextmenu will only be emitted if there is a registered listener
153+
'head-contextmenu': () => {}
154+
}
155+
})
156+
expect(wrapper).toBeDefined()
157+
const $ths = wrapper.findAll('thead > tr > th')
158+
expect($ths.length).toBe(testFields.length)
159+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
160+
await $ths.at(0).trigger('contextmenu')
161+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
162+
163+
wrapper.destroy()
164+
})
165+
87166
it('should not emit head-clicked event when vm.localBusy is true', async () => {
88167
const wrapper = mount(BTable, {
89168
propsData: {
@@ -108,6 +187,28 @@ describe('table > thead events', () => {
108187
wrapper.destroy()
109188
})
110189

190+
it('should not emit head-contextmenu event when vm.localBusy is true', async () => {
191+
const wrapper = mount(BTable, {
192+
propsData: {
193+
fields: testFields,
194+
items: testItems
195+
},
196+
listeners: {
197+
// Head-contextmenu will only be emitted if there is a registered listener
198+
'head-contextmenu': () => {}
199+
}
200+
})
201+
await wrapper.setData({ localBusy: true })
202+
expect(wrapper).toBeDefined()
203+
const $ths = wrapper.findAll('thead > tr > th')
204+
expect($ths.length).toBe(testFields.length)
205+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
206+
await $ths.at(0).trigger('contextmenu')
207+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
208+
209+
wrapper.destroy()
210+
})
211+
111212
it('should not emit head-clicked event when clicking on a button or other interactive element', async () => {
112213
const wrapper = mount(BTable, {
113214
propsData: {
@@ -147,4 +248,44 @@ describe('table > thead events', () => {
147248

148249
wrapper.destroy()
149250
})
251+
252+
it('should not emit head-contextmenu event when clicking on a button or other interactive element', async () => {
253+
const wrapper = mount(BTable, {
254+
propsData: {
255+
fields: testFields,
256+
items: testItems
257+
},
258+
listeners: {
259+
// Head-contextmenu will only be emitted if there is a registered listener
260+
'head-contextmenu': () => {}
261+
},
262+
slots: {
263+
// In Vue 2.6x, slots get translated into scopedSlots
264+
'head(a)': '<button id="a">button</button>',
265+
'head(b)': '<input id="b">',
266+
'head(c)': '<a href="#" id="c">link</a>'
267+
}
268+
})
269+
expect(wrapper).toBeDefined()
270+
const $ths = wrapper.findAll('thead > tr > th')
271+
expect($ths.length).toBe(testFields.length)
272+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
273+
274+
const $btn = wrapper.find('button[id="a"]')
275+
expect($btn.exists()).toBe(true)
276+
await $btn.trigger('contextmenu')
277+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
278+
279+
const $input = wrapper.find('input[id="b"]')
280+
expect($input.exists()).toBe(true)
281+
await $input.trigger('contextmenu')
282+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
283+
284+
const $link = wrapper.find('a[id="c"]')
285+
expect($link.exists()).toBe(true)
286+
await $link.trigger('contextmenu')
287+
expect(wrapper.emitted('head-contextmenu')).toBeUndefined()
288+
289+
wrapper.destroy()
290+
})
150291
})

src/constants/events.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const EVENT_NAME_FOCUS = 'focus'
2020
export const EVENT_NAME_FOCUSIN = 'focusin'
2121
export const EVENT_NAME_FOCUSOUT = 'focusout'
2222
export const EVENT_NAME_HEAD_CLICKED = 'head-clicked'
23+
export const EVENT_NAME_HEAD_CONTEXTMENU = 'head-contextmenu'
2324
export const EVENT_NAME_HIDDEN = 'hidden'
2425
export const EVENT_NAME_HIDE = 'hide'
2526
export const EVENT_NAME_IMG_ERROR = 'img-error'

0 commit comments

Comments
 (0)