Skip to content

Commit ddcf11b

Browse files
authored
Merge pull request #755 from devhus/main
BTable event filter, BFormInput highlighting animation
2 parents f65b39b + f6ca936 commit ddcf11b

File tree

7 files changed

+286
-79
lines changed

7 files changed

+286
-79
lines changed

packages/bootstrap-vue-3/src/components/BFormInput/BFormInput.vue

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
</template>
2626

2727
<script lang="ts">
28-
import type {InputType} from '../../types'
29-
import {computed, defineComponent, PropType} from 'vue'
28+
import {computed, defineComponent, PropType, ref} from 'vue'
3029
import {COMMON_INPUT_PROPS, useFormInput} from '../../composables'
30+
import type {InputType} from '../../types'
3131
3232
const allowedTypes = [
3333
'text',
@@ -59,13 +59,15 @@ export default defineComponent({
5959
},
6060
emits: ['update:modelValue', 'change', 'blur', 'input'],
6161
setup(props, {emit}) {
62-
const {input, computedId, computedAriaInvalid, onInput, onChange, onBlur, focus, blur} =
63-
useFormInput(props, emit)
62+
const {input, computedId, computedAriaInvalid, onInput, onChange, onBlur, focus, blur} = useFormInput(props, emit)
63+
64+
const isHighlighted = ref(false)
6465
6566
const computedClasses = computed(() => {
6667
const isRange = props.type === 'range'
6768
const isColor = props.type === 'color'
6869
return {
70+
'form-control-highlighted': isHighlighted.value,
6971
'form-range': isRange,
7072
'form-control': isColor || (!props.plaintext && !isRange),
7173
'form-control-color': isColor,
@@ -80,6 +82,14 @@ export default defineComponent({
8082
allowedTypes.includes(props.type) ? props.type : 'text'
8183
)
8284
85+
const highlight = () => {
86+
if (isHighlighted.value === true) return
87+
isHighlighted.value = true
88+
setTimeout(() => {
89+
isHighlighted.value = false
90+
}, 2000)
91+
}
92+
8393
return {
8494
computedClasses,
8595
localType,
@@ -91,6 +101,7 @@ export default defineComponent({
91101
onBlur,
92102
focus,
93103
blur,
104+
highlight,
94105
}
95106
},
96107
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.form-control-highlighted {
2+
border-color: $input-focus-border-color;
3+
4+
transition: box-shadow 0.3s ease-in-out;
5+
animation: form-control-highlighted-blink 1s infinite;
6+
}
7+
8+
@keyframes form-control-highlighted-blink {
9+
0% {
10+
box-shadow: 0 0 0 0 transparent;
11+
}
12+
13+
50% {
14+
@if $enable-shadows {
15+
@include box-shadow($input-box-shadow, $input-focus-box-shadow);
16+
} @else {
17+
// Avoid using mixin so we can pass custom focus shadow properly
18+
box-shadow: $input-focus-box-shadow;
19+
}
20+
}
21+
22+
100% {
23+
box-shadow: 0 0 0 0 transparent;
24+
}
25+
}

packages/bootstrap-vue-3/src/components/BTable/BTable.vue

Lines changed: 74 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@
7272
<template v-for="(item, itemIndex) in computedItems" :key="itemIndex">
7373
<tr
7474
:class="getRowClasses(item)"
75-
@click="onRowClick(item, itemIndex, $event)"
76-
@dblclick="onRowDblClick(item, itemIndex, $event)"
77-
@mouseenter="onRowMouseEnter(item, itemIndex, $event)"
78-
@mouseleave="onRowMouseLeave(item, itemIndex, $event)"
75+
@click="!filterEvent($event) && onRowClick(item, itemIndex, $event)"
76+
@dblclick="!filterEvent($event) && onRowDblClick(item, itemIndex, $event)"
77+
@mouseenter="!filterEvent($event) && onRowMouseEnter(item, itemIndex, $event)"
78+
@mouseleave="!filterEvent($event) && onRowMouseLeave(item, itemIndex, $event)"
7979
>
8080
<td
8181
v-if="addSelectableCell"
@@ -181,6 +181,7 @@ import {useBooleanish} from '../../composables'
181181
import {cloneDeepAsync} from '../../utils/object'
182182
import {titleCase} from '../../utils/stringUtils'
183183
import BSpinner from '../BSpinner.vue'
184+
184185
import type {
185186
Booleanish,
186187
ColorVariant,
@@ -191,6 +192,7 @@ import type {
191192
} from '../../types'
192193
import type {BTableProvider, BTableSortCompare} from '../../types/components'
193194
import BTableSimple from './BTableSimple.vue'
195+
import {filterEvent} from './helpers/filter-event'
194196
import useItemHelper from './itemHelper'
195197
196198
type NoProviderTypes = 'paging' | 'sorting' | 'filtering'
@@ -297,7 +299,6 @@ interface BTableEmits {
297299
}
298300
299301
const emit = defineEmits<BTableEmits>()
300-
301302
const slots = useSlots()
302303
303304
const itemHelper = useItemHelper()
@@ -314,7 +315,17 @@ const noProviderSortingBoolean = useBooleanish(toRef(props, 'showEmpty'))
314315
const noProviderFilteringBoolean = useBooleanish(toRef(props, 'showEmpty'))
315316
316317
const internalBusyFlag = ref(busyBoolean.value)
318+
itemHelper.filterEvent.value = async (items) => {
319+
if (usesProvider.value) {
320+
await callItemsProvider()
321+
return
322+
}
323+
const clone = await cloneDeepAsync(items)
324+
emit('filtered', clone)
325+
}
326+
317327
const selectedItems = ref<Set<TableItem>>(new Set([]))
328+
const isSelecting = computed(() => selectedItems.value.size > 0)
318329
319330
const tableClasses = computed(() => ({
320331
[`align-${props.align}`]: props.align !== undefined,
@@ -348,11 +359,18 @@ const computedFieldsTotal = computed(
348359
)
349360
350361
const isFilterableTable = computed(() => props.filter !== undefined && props.filter !== '')
351-
352362
const usesProvider = computed(() => props.provider !== undefined)
353363
354-
const requireItemsMapping = computed(() => isSortable.value && sortInternalBoolean.value === true)
364+
const addSelectableCell = computed(
365+
() => selectableBoolean.value && (!!props.selectHead || slots.selectHead !== undefined)
366+
)
367+
368+
const isSortable = computed(
369+
() =>
370+
props.fields.filter((field) => (typeof field === 'string' ? false : field.sortable)).length > 0
371+
)
355372
373+
const requireItemsMapping = computed(() => isSortable.value && sortInternalBoolean.value === true)
356374
const computedItems = computed(() => {
357375
if (usesProvider.value) return itemHelper.internalItems.value
358376
return requireItemsMapping.value
@@ -364,53 +382,6 @@ const computedItems = computed(() => {
364382
: props.items
365383
})
366384
367-
const addSelectableCell = computed(
368-
() => selectableBoolean.value && (!!props.selectHead || slots.selectHead !== undefined)
369-
)
370-
const isSortable = computed(
371-
() =>
372-
props.fields.filter((field) => (typeof field === 'string' ? false : field.sortable)).length > 0
373-
)
374-
const isSelecting = computed(() => selectedItems.value.size > 0)
375-
376-
watch(
377-
() => props.filter,
378-
(filter, oldFilter) => {
379-
if (filter === oldFilter || usesProvider.value) return
380-
if (!filter) {
381-
cloneDeepAsync(props.items).then((item) => emit('filtered', item))
382-
}
383-
}
384-
)
385-
watch(
386-
() => internalBusyFlag.value,
387-
() => internalBusyFlag.value !== busyBoolean.value && emit('update:busy', internalBusyFlag.value)
388-
)
389-
watch(
390-
() => busyBoolean.value,
391-
() => internalBusyFlag.value !== busyBoolean.value && (internalBusyFlag.value = busyBoolean.value)
392-
)
393-
watch(
394-
() => props.filter,
395-
(val, oldVal) => providerPropsWatch('filter', val, oldVal)
396-
)
397-
watch(
398-
() => props.currentPage,
399-
(val, oldVal) => providerPropsWatch('currentPage', val, oldVal)
400-
)
401-
watch(
402-
() => props.perPage,
403-
(val, oldVal) => providerPropsWatch('perPage', val, oldVal)
404-
)
405-
watch(
406-
() => props.sortBy,
407-
(val, oldVal) => providerPropsWatch('sortBy', val, oldVal)
408-
)
409-
watch(
410-
() => props.sortDesc,
411-
(val, oldVal) => providerPropsWatch('sortDesc', val, oldVal)
412-
)
413-
414385
const getFieldHeadLabel = (field: TableField) => {
415386
if (typeof field === 'string') return titleCase(field)
416387
if (field.label !== undefined) return field.label
@@ -430,7 +401,6 @@ const onRowClick = (row: TableItem, index: number, e: MouseEvent) => {
430401
431402
handleRowSelection(row, index, e.shiftKey)
432403
}
433-
434404
const onRowDblClick = (row: TableItem, index: number, e: MouseEvent) =>
435405
emit('rowDblClicked', row, index, e)
436406
@@ -440,15 +410,6 @@ const onRowMouseEnter = (row: TableItem, index: number, e: MouseEvent) =>
440410
const onRowMouseLeave = (row: TableItem, index: number, e: MouseEvent) =>
441411
emit('rowUnhovered', row, index, e)
442412
443-
itemHelper.filterEvent.value = async (items) => {
444-
if (usesProvider.value) {
445-
await callItemsProvider()
446-
return
447-
}
448-
const clone = await cloneDeepAsync(items)
449-
emit('filtered', clone)
450-
}
451-
452413
const handleFieldSorting = (field: TableField) => {
453414
if (!isSortable.value) return
454415
@@ -554,29 +515,32 @@ const toggleRowDetails = (tr: TableItem) => {
554515
const getFieldColumnClasses = (field: TableFieldObject) => [
555516
field.class,
556517
field.thClass,
518+
field.variant ? `table-${field.variant}` : undefined,
557519
{
558-
[`table-${field.variant}`]: field.variant !== undefined,
559520
'b-table-sortable-column': isSortable.value && field.sortable,
560521
'b-table-sticky-column': field.stickyColumn,
561522
},
562523
]
524+
563525
const getFieldRowClasses = (field: TableFieldObject, tr: TableItem) => [
564526
field.class,
565527
field.tdClass,
528+
field.variant ? `table-${field.variant}` : undefined,
566529
tr?._cellVariants && tr?._cellVariants[field.key]
567530
? `table-${tr?._cellVariants[field.key]}`
568531
: undefined,
569532
{
570-
[`table-${field.variant}`]: field.variant !== undefined,
571533
'b-table-sticky-column': field.stickyColumn,
572534
},
573535
]
574536
575-
const getRowClasses = (item: TableItem) => ({
576-
[`table-${item._rowVariant}`]: item._rowVariant !== undefined,
577-
[`selected table-${props.selectionVariant}`]:
578-
selectableBoolean.value && selectedItems.value.has(item),
579-
})
537+
const getRowClasses = (item: TableItem) => [
538+
item._rowVariant ? `table-${item._rowVariant}` : null,
539+
item._rowVariant ? `table-${item._rowVariant}` : null,
540+
selectableBoolean.value && selectedItems.value.has(item)
541+
? `selected table-${props.selectionVariant}`
542+
: null,
543+
]
580544
581545
const selectAllRows = () => {
582546
if (!selectableBoolean.value) return
@@ -638,6 +602,45 @@ const providerPropsWatch = async (prop: string, val: any, oldVal: any) => {
638602
await callItemsProvider()
639603
}
640604
605+
watch(
606+
() => props.filter,
607+
(filter, oldFilter) => {
608+
if (filter === oldFilter || usesProvider.value) return
609+
if (!filter) {
610+
cloneDeepAsync(props.items).then((item) => emit('filtered', item))
611+
}
612+
}
613+
)
614+
615+
watch(
616+
() => internalBusyFlag.value,
617+
() => internalBusyFlag.value !== busyBoolean.value && emit('update:busy', internalBusyFlag.value)
618+
)
619+
watch(
620+
() => busyBoolean.value,
621+
() => internalBusyFlag.value !== busyBoolean.value && (internalBusyFlag.value = busyBoolean.value)
622+
)
623+
watch(
624+
() => props.filter,
625+
(val, oldVal) => providerPropsWatch('filter', val, oldVal)
626+
)
627+
watch(
628+
() => props.currentPage,
629+
(val, oldVal) => providerPropsWatch('currentPage', val, oldVal)
630+
)
631+
watch(
632+
() => props.perPage,
633+
(val, oldVal) => providerPropsWatch('perPage', val, oldVal)
634+
)
635+
watch(
636+
() => props.sortBy,
637+
(val, oldVal) => providerPropsWatch('sortBy', val, oldVal)
638+
)
639+
watch(
640+
() => props.sortDesc,
641+
(val, oldVal) => providerPropsWatch('sortDesc', val, oldVal)
642+
)
643+
641644
onMounted(() => {
642645
if (usesProvider.value) {
643646
callItemsProvider()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {closest, getAttr, getById, matches, select} from '../../../utils/dom'
2+
3+
const TABLE_TAG_NAMES = ['TD', 'TH', 'TR']
4+
5+
// Filter CSS selector for click/dblclick/etc. events
6+
// If any of these selectors match the clicked element, we ignore the event
7+
export const EVENT_FILTER = [
8+
'a',
9+
'a *', // Include content inside links
10+
'button',
11+
'button *', // Include content inside buttons
12+
'input:not(.disabled):not([disabled])',
13+
'select:not(.disabled):not([disabled])',
14+
'textarea:not(.disabled):not([disabled])',
15+
'[role="link"]',
16+
'[role="link"] *',
17+
'[role="button"]',
18+
'[role="button"] *',
19+
'[tabindex]:not(.disabled):not([disabled])',
20+
].join(',')
21+
22+
// Returns `true` if we should ignore the click/double-click/keypress event
23+
// Avoids having the user need to use `@click.stop` on the form control
24+
export const filterEvent = (event: Event) => {
25+
// Exit early when we don't have a target element
26+
if (!event || !event.target) {
27+
/* istanbul ignore next */
28+
return false
29+
}
30+
const el = event.target as HTMLElement
31+
// Exit early when element is disabled or a table element
32+
if (('disabled' in el && (el as any).disabled) || TABLE_TAG_NAMES.indexOf(el.tagName) !== -1) {
33+
return false
34+
}
35+
// Ignore the click when it was inside a dropdown menu
36+
if (closest('.dropdown-menu', el)) {
37+
return true
38+
}
39+
const label = el.tagName === 'LABEL' ? el : closest('label', el)
40+
// If the label's form control is not disabled then we don't propagate event
41+
// Modern browsers have `label.control` that references the associated input, but IE 11
42+
// does not have this property on the label element, so we resort to DOM lookups
43+
if (label) {
44+
const labelFor = getAttr(label, 'for')
45+
const input = labelFor ? getById(labelFor) : select('input, select, textarea', label)
46+
if (input && !input.disabled) {
47+
return true
48+
}
49+
}
50+
// Otherwise check if the event target matches one of the selectors in the
51+
// event filter (i.e. anchors, non disabled inputs, etc.)
52+
// Return `true` if we should ignore the event
53+
return matches(el, EVENT_FILTER)
54+
}

0 commit comments

Comments
 (0)