🌐 AI搜索 & 代理 主页
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/markdown/reference/color-variants/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ When creating custom variants, follow the Bootstrap v4 variant CSS class naming
become available to the various components that use that scheme (i.e. create a custom CSS class
`btn-purple` and `purple` becomes a valid variant to use on `<b-button>`).

Alternatively, you can create new variant theme colors by supplying custom Bootstrap SCSS theme color
maps. The default theme color map is (from `bootstrap/scss/_variables.scss`):
Alternatively, you can create new variant theme colors by supplying custom Bootstrap SCSS theme
color maps. The default theme color map is (from `bootstrap/scss/_variables.scss`):

```scss
// Base grayscale colors definitions
Expand Down
56 changes: 47 additions & 9 deletions src/components/form-tags/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ button will only appear when the user has entered a new tag value.
<template>
<div>
<label for="tags-basic">Type a new tag and press enter</label>
<b-form-tags input-id="tags-basic" v-model="value" class="mb-2"></b-form-tags>
<p>Value: {{ value }}</p>
<b-form-tags input-id="tags-basic" v-model="value"></b-form-tags>
<p class="mt-2">Value: {{ value }}</p>
</div>
</template>

Expand Down Expand Up @@ -63,9 +63,8 @@ are typed:
separator=" ,;"
placeholder="Enter new tags separated by space, comma or semicolon"
no-add-on-enter
class="mb-2"
></b-form-tags>
<p>Value: {{ value }}</p>
<p class="mt-2">Value: {{ value }}</p>
</div>
</template>

Expand Down Expand Up @@ -99,9 +98,8 @@ When the prop `remove-on-delete` is set, and the user presses <kbd>Backspace</kb
placeholder="Enter new tags separated by space"
remove-on-delete
no-add-on-enter
class="mb-2"
></b-form-tags>
<b-form-text id="tags-remove-on-delete-help">
<b-form-text id="tags-remove-on-delete-help" class="mt-2">
Press <kbd>Backspace</kbd> to remove the last tag entered
</b-form-text>
<p>Value: {{ value }}</p>
Expand Down Expand Up @@ -150,9 +148,8 @@ The focus and validation state styling of the component relies upon BootstrapVue
size="lg"
separator=" "
placeholder="Enter new tags separated by space"
class="mb-2"
></b-form-tags>
<p>Value: {{ value }}</p>
<p class="mt-2">Value: {{ value }}</p>
</div>
</template>

Expand Down Expand Up @@ -186,7 +183,7 @@ duplicate tag, and will provide integrated feedback to the user.
You can optionally provide a tag validator method via the `tag-validator` prop. The validator
function will receive one argument which is the tag being added, and should return either `true` if
the tag passes validation and can be added, or `false` if the tag fails validation (in which case it
is not added to the array of tags). integrated feedback will be provided to the user listing the
is not added to the array of tags). Integrated feedback will be provided to the user listing the
invalid tag(s) that could not be added.

Tag validation occurs only for tags added via user input. Changes to the tags via the `v-model` are
Expand Down Expand Up @@ -318,6 +315,41 @@ to either an empty string (`''`) or `null`.
<!-- b-form-tags-tags-state-event.vue -->
```

## Limiting tags

If you want to limit the amount of tags the user is able to add use the `limit` prop. When
configured, adding more tags than the `limit` allows is only possible by the `v-model`.

When the limit of tags is reached, the user is still able to type but adding more tags is disabled.
A message is shown to give the user feedback about the reached limit. This message can be configured
by the `limit-tags-text` prop. Setting it to either an empty string (`''`) or `null` will disable
the feedback.

Removing tags is unaffected by the `limit` prop.

```html
<template>
<div>
<label for="tags-limit">Enter tags</label>
<b-form-tags input-id="tags-limit" v-model="value" :limit="limit" remove-on-delete></b-form-tags>
<p class="mt-2">Value: {{ value }}</p>
</div>
</template>

<script>
export default {
data() {
return {
value: [],
limit: 5
}
}
}
</script>

<!-- b-form-tags-limit.vue -->
```

## Custom rendering with default scoped slot

If you fancy a different look and feel for the tags control, you can provide your own custom
Expand All @@ -344,17 +376,23 @@ The default slot scope properties are as follows:
| `invalidTags` | Array | Array of the invalid tag(s) the user has entered |
| `isDuplicate` | Boolean | `true` if the user input contains duplicate tag(s) |
| `duplicateTags` | Array | Array of the duplicate tag(s) the user has entered |
| `isLimitReached` | Boolean | <span class="badge badge-secondary">v2.17.0+</span> `true` if a `limit` is configured and the amount of tags has reached the limit |
| `disableAddButton` | Boolean | Will be `true` if the tag(s) in the input cannot be added (all invalid and/or duplicates) |
| `disabled` | Boolean | `true` if the component is in the disabled state. Value of the `disabled` prop |
| `state` | Boolean | The contextual state of the component. Value of the `state` prop. Possible values are `true`, `false` or `null` |
| `size` | String | The value of the `size` prop |
| `limit` | String | <span class="badge badge-secondary">v2.17.0+</span> The value of the `limit` prop |
| `separator` | String | The value of the `separator` prop |
| `placeholder` | String | The value of the `placeholder` prop |
| `tagRemoveLabel` | String | Value of the `tag-remove-label` prop. Used as the `aria-label` attribute on the remove button of tags |
| `tagVariant` | String | The value of the `tag-variant` prop |
| `tagPills` | Boolean | The value of the `tag-pills` prop |
| `tagClass` | String, Array, or Object | The value of the `tag-variant` prop. Class (or classes) to apply to the tag elements |
| `addButtonText` | String | The value of the `add-button-text` prop |
| `addButtonVariant` | String | The value of the `add-button-variant` prop |
| `invalidTagText` | String | The value of the `invalid-tag-text` prop |
| `duplicateTagText` | String | The value of the `duplicate-tag-text` prop |
| `limitTagsText` | String | <span class="badge badge-secondary">v2.17.0+</span> The value of the `limit-tags-text` prop |

#### `inputAttrs` object properties

Expand Down
87 changes: 60 additions & 27 deletions src/components/form-tags/form-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
requestAF,
select
} from '../../utils/dom'
import { isEvent, isFunction, isString } from '../../utils/inspect'
import { isEvent, isFunction, isNumber, isString } from '../../utils/inspect'
import { escapeRegExp, toString, trim, trimLeft } from '../../utils/string'
import idMixin from '../../mixins/id'
import normalizeSlotMixin from '../../mixins/normalize-slot'
Expand Down Expand Up @@ -160,6 +160,14 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
type: String,
default: () => getComponentConfig(NAME, 'invalidTagText')
},
limitTagsText: {
type: String,
default: () => getComponentConfig(NAME, 'limitTagsText')
},
limit: {
type: Number
// default: null
},
separator: {
// Character (or characters) that trigger adding tags
type: [String, Array]
Expand Down Expand Up @@ -288,6 +296,10 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
},
hasInvalidTags() {
return this.invalidTags.length > 0
},
isLimitReached() {
const { limit } = this
return isNumber(limit) && limit >= 0 && this.tags.length >= limit
}
},
watch: {
Expand Down Expand Up @@ -328,7 +340,7 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
addTag(newTag) {
newTag = isString(newTag) ? newTag : this.newTag
/* istanbul ignore next */
if (this.disabled || trim(newTag) === '') {
if (this.disabled || trim(newTag) === '' || this.isLimitReached) {
// Early exit
return
}
Expand Down Expand Up @@ -530,25 +542,27 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
// Default User Interface render
defaultRender({
tags,
addTag,
removeTag,
inputType,
inputAttrs,
inputType,
inputHandlers,
inputClass,
tagClass,
tagVariant,
tagPills,
tagRemoveLabel,
invalidTagText,
duplicateTagText,
removeTag,
addTag,
isInvalid,
isDuplicate,
isLimitReached,
disableAddButton,
disabled,
placeholder,
inputClass,
tagRemoveLabel,
tagVariant,
tagPills,
tagClass,
addButtonText,
addButtonVariant,
disableAddButton
invalidTagText,
duplicateTagText,
limitTagsText
}) {
const h = this.$createElement

Expand Down Expand Up @@ -581,12 +595,15 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
invalidTagText && isInvalid ? this.safeId('__invalid_feedback__') : null
const duplicateFeedbackId =
duplicateTagText && isDuplicate ? this.safeId('__duplicate_feedback__') : null
const limitFeedbackId =
limitTagsText && isLimitReached ? this.safeId('__limit_feedback__') : null

// Compute the `aria-describedby` attribute value
const ariaDescribedby = [
inputAttrs['aria-describedby'],
invalidFeedbackId,
duplicateFeedbackId
duplicateFeedbackId,
limitFeedbackId
]
.filter(identity)
.join(' ')
Expand Down Expand Up @@ -623,7 +640,7 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
invisible: disableAddButton
},
style: { fontSize: '90%' },
props: { variant: addButtonVariant, disabled: disableAddButton },
props: { variant: addButtonVariant, disabled: disableAddButton || isLimitReached },
on: { click: () => addTag() }
},
[this.normalizeSlot('add-button-text') || addButtonText]
Expand Down Expand Up @@ -663,7 +680,7 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({

// Assemble the feedback
let $feedback = h()
if (invalidTagText || duplicateTagText) {
if (invalidTagText || duplicateTagText || limitTagsText) {
// Add an aria live region for the invalid/duplicate tag
// messages if the user has not disabled the messages
const joiner = this.computedJoiner
Expand Down Expand Up @@ -694,13 +711,26 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
)
}

// Limit tags feedback if needed (warning, not error)
let $limit = h()
if (limitFeedbackId) {
$limit = h(
BFormText,
{
key: '_tags_limit_feedback_',
props: { id: limitFeedbackId }
},
[limitTagsText]
)
}

$feedback = h(
'div',
{
key: '_tags_feedback_',
attrs: { 'aria-live': 'polite', 'aria-atomic': 'true' }
},
[$invalid, $duplicate]
[$invalid, $duplicate, $limit]
)
}
// Return the content
Expand All @@ -712,29 +742,31 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
const scope = {
// Array of tags (shallow copy to prevent mutations)
tags: this.tags.slice(),
// Methods
removeTag: this.removeTag,
addTag: this.addTag,
// We don't include this in the attrs, as users may want to override this
inputType: this.computedInputType,
// <input> v-bind:inputAttrs
inputAttrs: this.computedInputAttrs,
// We don't include this in the attrs, as users may want to override this
inputType: this.computedInputType,
// <input> v-on:inputHandlers
inputHandlers: this.computedInputHandlers,
// Methods
removeTag: this.removeTag,
addTag: this.addTag,
// <input> :id="inputId"
inputId: this.computedInputId,
// Invalid/Duplicate state information
invalidTags: this.invalidTags.slice(),
isInvalid: this.hasInvalidTags,
duplicateTags: this.duplicateTags.slice(),
invalidTags: this.invalidTags.slice(),
isDuplicate: this.hasDuplicateTags,
duplicateTags: this.duplicateTags.slice(),
isLimitReached: this.isLimitReached,
// If the 'Add' button should be disabled
disableAddButton: this.disableAddButton,
// Pass-though values
state: this.state,
separator: this.separator,
disabled: this.disabled,
state: this.state,
size: this.size,
limit: this.limit,
separator: this.separator,
placeholder: this.placeholder,
inputClass: this.inputClass,
tagRemoveLabel: this.tagRemoveLabel,
Expand All @@ -744,7 +776,8 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
addButtonText: this.addButtonText,
addButtonVariant: this.addButtonVariant,
invalidTagText: this.invalidTagText,
duplicateTagText: this.duplicateTagText
duplicateTagText: this.duplicateTagText,
limitTagsText: this.limitTagsText
}

// Generate the user interface
Expand Down
55 changes: 55 additions & 0 deletions src/components/form-tags/form-tags.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -601,4 +601,59 @@ describe('form-tags', () => {

wrapper.destroy()
})

it('`limit` prop works', async () => {
const wrapper = mount(BFormTags, {
propsData: {
value: ['apple', 'orange'],
limit: 3
}
})

expect(wrapper.element.tagName).toBe('DIV')
expect(wrapper.vm.tags).toEqual(['apple', 'orange'])
expect(wrapper.vm.newTag).toEqual('')

const $input = wrapper.find('input')
expect($input.exists()).toBe(true)
expect($input.element.value).toBe('')

const $button = wrapper.find('button.b-form-tags-button')
expect($button.exists()).toBe(true)
expect($button.classes()).toContain('invisible')

expect(wrapper.find('small.form-text').exists()).toBe(false)

// Add new tag
$input.element.value = 'pear'
await $input.trigger('input')
expect(wrapper.vm.newTag).toEqual('pear')
expect(wrapper.vm.tags).toEqual(['apple', 'orange'])
expect($button.classes()).not.toContain('invisible')

await $button.trigger('click')
expect($button.classes()).toContain('invisible')
expect(wrapper.vm.newTag).toEqual('')
expect(wrapper.vm.tags).toEqual(['apple', 'orange', 'pear'])

const $feedback = wrapper.find('small.form-text')
expect($feedback.exists()).toBe(true)
expect($feedback.text()).toContain('Tag limit reached')

// Attempt to add new tag
$input.element.value = 'lemon'
await $input.trigger('input')
expect(wrapper.vm.newTag).toEqual('lemon')
expect(wrapper.vm.tags).toEqual(['apple', 'orange', 'pear'])
expect($button.classes()).not.toContain('invisible')

await $button.trigger('click')
expect($button.classes()).not.toContain('invisible')
expect(wrapper.vm.newTag).toEqual('lemon')
expect(wrapper.vm.tags).toEqual(['apple', 'orange', 'pear'])
expect($feedback.exists()).toBe(true)
expect($feedback.text()).toContain('Tag limit reached')

wrapper.destroy()
})
})
Loading