1
1
// Tagged input form control
2
2
// Based loosely on https://adamwathan.me/renderless-components-in-vuejs/
3
3
import Vue from '../../utils/vue'
4
-
import identity from '../../utils/identity'
5
4
import KeyCodes from '../../utils/key-codes'
5
+
import identity from '../../utils/identity'
6
6
import looseEqual from '../../utils/loose-equal'
7
7
import { arrayIncludes, concat } from '../../utils/array'
8
8
import { getComponentConfig } from '../../utils/config'
@@ -11,10 +11,10 @@ import { isEvent, isFunction, isString } from '../../utils/inspect'
11
11
import { escapeRegExp, toString, trim, trimLeft } from '../../utils/string'
12
12
import idMixin from '../../mixins/id'
13
13
import normalizeSlotMixin from '../../mixins/normalize-slot'
14
-
import { BFormTag } from './form-tag'
14
+
import { BButton } from '../button/button'
15
15
import { BFormInvalidFeedback } from '../form/form-invalid-feedback'
16
16
import { BFormText } from '../form/form-text'
17
-
import { BButton } from '../button/button'
17
+
import { BFormTag } from './form-tag'
18
18
19
19
// --- Constants ---
20
20
@@ -26,6 +26,9 @@ const TYPES = ['text', 'email', 'tel', 'url', 'number']
26
26
// Pre-compiled regular expressions for performance reasons
27
27
const RX_SPACES = /[\s\uFEFF\xA0]+/g
28
28
29
+
// KeyCode constants
30
+
const { ENTER, BACKSPACE, DELETE } = KeyCodes
31
+
29
32
// --- Utility methods ---
30
33
31
34
// Escape special chars in string and replace
@@ -132,6 +135,10 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
132
135
type: String,
133
136
default: () => getComponentConfig(NAME, 'tagRemoveLabel')
134
137
},
138
+
tagRemovedLabel: {
139
+
type: String,
140
+
default: () => getComponentConfig(NAME, 'tagRemovedLabel')
141
+
},
135
142
tagValidator: {
136
143
type: Function,
137
144
default: null
@@ -182,6 +189,8 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
182
189
hasFocus: false,
183
190
newTag: '',
184
191
tags: [],
192
+
// Tags that were removed
193
+
removedTags: [],
185
194
// Populated when tags are parsed
186
195
tagsState: cleanTagsState()
187
196
}
@@ -263,11 +272,16 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
263
272
value(newVal) {
264
273
this.tags = cleanTags(newVal)
265
274
},
266
-
tags(newVal) {
275
+
tags(newVal, oldVal) {
267
276
// Update the `v-model` (if it differs from the value prop)
268
277
if (!looseEqual(newVal, this.value)) {
269
278
this.$emit('input', newVal)
270
279
}
280
+
if (!looseEqual(newVal, oldVal)) {
281
+
newVal = concat(newVal).filter(identity)
282
+
oldVal = concat(oldVal).filter(identity)
283
+
this.removedTags = oldVal.filter(old => !arrayIncludes(newVal, old))
284
+
}
271
285
},
272
286
tagsState(newVal, oldVal) {
273
287
// Emit a tag-state event when the `tagsState` object changes
@@ -336,7 +350,9 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
336
350
// Or emit cancelable `BvEvent`
337
351
this.tags = this.tags.filter(t => t !== tag)
338
352
// Return focus to the input (if possible)
339
-
this.focus()
353
+
this.$nextTick(() => {
354
+
this.focus()
355
+
})
340
356
},
341
357
// --- Input element event handlers ---
342
358
onInputInput(evt) {
@@ -383,20 +399,26 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
383
399
const keyCode = evt.keyCode
384
400
const value = evt.target.value || ''
385
401
/* istanbul ignore else: testing to be added later */
386
-
if (!this.noAddOnEnter && keyCode === KeyCodes.ENTER) {
402
+
if (!this.noAddOnEnter && keyCode === ENTER) {
387
403
// Attempt to add the tag when user presses enter
388
404
evt.preventDefault()
389
405
this.addTag()
390
-
} else if (this.removeOnDelete && keyCode === KeyCodes.BACKSPACE && value === '') {
391
-
// Remove the last tag if the user pressed backspace and the input is empty
406
+
} else if (
407
+
this.removeOnDelete &&
408
+
(keyCode === BACKSPACE || keyCode === DELETE) &&
409
+
value === ''
410
+
) {
411
+
// Remove the last tag if the user pressed backspace/delete and the input is empty
392
412
evt.preventDefault()
393
-
this.tags.pop()
413
+
this.tags = this.tags.slice(0, -1)
394
414
}
395
415
},
396
416
// --- Wrapper event handlers ---
397
417
onClick(evt) {
398
418
if (!this.disabled && isEvent(evt) && evt.target === evt.currentTarget) {
399
-
this.$nextTick(this.focus)
419
+
this.$nextTick(() => {
420
+
this.focus()
421
+
})
400
422
}
401
423
},
402
424
onFocusin() {
@@ -512,7 +534,7 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
512
534
staticClass: 'mt-1 mr-1',
513
535
class: tagClass,
514
536
props: {
515
-
// 'BFormTag' will auto generate an ID
537
+
// `BFormTag` will auto generate an ID
516
538
// so we do not need to set the ID prop
517
539
tag: 'li',
518
540
title: tag,
@@ -591,10 +613,14 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
591
613
'li',
592
614
{
593
615
key: '__li-input__',
594
-
staticClass: 'd-inline-flex flex-grow-1 mt-1',
595
-
attrs: { role: 'group', 'aria-live': 'off', 'aria-controls': tagListId }
616
+
staticClass: 'flex-grow-1 mt-1',
617
+
attrs: {
618
+
role: 'none',
619
+
'aria-live': 'off',
620
+
'aria-controls': tagListId
621
+
}
596
622
},
597
-
[$input, $button]
623
+
[h('div', { staticClass: 'd-flex', attrs: { role: 'group' } }, [$input, $button])]
598
624
)
599
625
600
626
// Wrap in an unordered list element (we use a list for accessibility)
@@ -603,16 +629,7 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
603
629
{
604
630
key: '_tags_list_',
605
631
staticClass: 'list-unstyled mt-n1 mb-0 d-flex flex-wrap align-items-center',
606
-
attrs: {
607
-
id: tagListId,
608
-
// Don't interrupt the user abruptly
609
-
// Although maybe this should be 'assertive'
610
-
// to provide immediate feedback of the tag added/removed
611
-
'aria-live': 'polite',
612
-
// Only read elements that have been added or removed
613
-
'aria-atomic': 'false',
614
-
'aria-relevant': 'additions removals'
615
-
}
632
+
attrs: { id: tagListId }
616
633
},
617
634
// `concat()` is faster than array spread when args are known to be arrays
618
635
concat($tags, $field)
@@ -707,6 +724,38 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
707
724
// Generate the user interface
708
725
const $content = this.normalizeSlot('default', scope) || this.defaultRender(scope)
709
726
727
+
// Generate the `aria-live` region for the current value(s)
728
+
const $output = h(
729
+
'output',
730
+
{
731
+
staticClass: 'sr-only',
732
+
attrs: {
733
+
id: this.safeId('_selected-tags_'),
734
+
role: 'status',
735
+
for: this.computedInputId,
736
+
'aria-live': this.hasFocus ? 'polite' : 'off',
737
+
'aria-atomic': 'true',
738
+
'aria-relevant': 'additions text'
739
+
}
740
+
},
741
+
this.tags.join(', ')
742
+
)
743
+
744
+
// Removed tag live region
745
+
const $removed = h(
746
+
'div',
747
+
{
748
+
staticClass: 'sr-only',
749
+
attrs: {
750
+
id: this.safeId('_removed-tags_'),
751
+
role: 'status',
752
+
'aria-live': this.hasFocus ? 'assertive' : 'off',
753
+
'aria-atomic': 'true'
754
+
}
755
+
},
756
+
this.removedTags.length > 0 ? `(${this.tagRemovedLabel}) ${this.removedTags.join(', ')}` : ''
757
+
)
758
+
710
759
// Add hidden inputs for form submission
711
760
let $hidden = h()
712
761
if (this.name && !this.disabled) {
@@ -740,15 +789,16 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
740
789
attrs: {
741
790
id: this.safeId(),
742
791
role: 'group',
743
-
tabindex: this.disabled || this.noOuterFocus ? null : '-1'
792
+
tabindex: this.disabled || this.noOuterFocus ? null : '-1',
793
+
'aria-describedby': this.safeId('_selected_')
744
794
},
745
795
on: {
746
796
focusin: this.onFocusin,
747
797
focusout: this.onFocusout,
748
798
click: this.onClick
749
799
}
750
800
},
751
-
concat($content, $hidden)
801
+
concat($output, $removed, $content, $hidden)
752
802
)
753
803
}
754
804
})
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