mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(web-core): add VtsTagsInput component
This commit is contained in:
13
@xen-orchestra/lite/src/route-map.d.ts
vendored
13
@xen-orchestra/lite/src/route-map.d.ts
vendored
@@ -527,6 +527,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/tags-input/vts-tags-input': RouteRecordInfo<
|
||||
'/story/web-core/tags-input/vts-tags-input',
|
||||
'/story/web-core/tags-input/vts-tags-input',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/story/web-core/tree/vts-tree-item': RouteRecordInfo<
|
||||
'/story/web-core/tree/vts-tree-item',
|
||||
'/story/web-core/tree/vts-tree-item',
|
||||
@@ -1572,6 +1579,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/tags-input/vts-tags-input.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/tags-input/vts-tags-input'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/stories/web-core/tree/vts-tree-item.story.vue': {
|
||||
routes:
|
||||
| '/story/web-core/tree/vts-tree-item'
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Usage
|
||||
|
||||
Enter or comma adds the typed value: it is trimmed, and an empty value or a value already in the list is ignored. A
|
||||
pasted comma-separated draft adds each of its values. Enter while an IME composition is in progress adds nothing.
|
||||
|
||||
Removing a chip updates the model with the new array.
|
||||
|
||||
The label comes from the surrounding `VtsInputWrapper`.
|
||||
|
||||
```vue-template
|
||||
<VtsInputWrapper label="Values">
|
||||
<VtsTagsInput v-model="values" accent="brand" placeholder="Add a value" />
|
||||
</VtsInputWrapper>
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
model().type('string[]').preset(['alpha', 'beta']).help('Enter or comma adds the typed value'),
|
||||
prop('accent').enum('brand', 'warning', 'danger').required().preset('brand').widget(),
|
||||
prop('placeholder').str().preset('Add a value').widget(),
|
||||
]"
|
||||
>
|
||||
<VtsInputWrapper label="Values">
|
||||
<VtsTagsInput v-bind="properties" />
|
||||
</VtsInputWrapper>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { model, prop } from '@/libs/story/story-param.ts'
|
||||
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
|
||||
import VtsTagsInput from '@core/components/tags-input/VtsTagsInput.vue'
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="vts-tags-input">
|
||||
<UiInput v-model="draft" :accent :placeholder @keydown="(event: KeyboardEvent) => handleKeydown(event)" />
|
||||
<div v-if="tags.length > 0" class="chips">
|
||||
<UiChip v-for="(tag, index) of tags" :key="index" accent="info" @remove="removeTag(index)">
|
||||
{{ tag }}
|
||||
</UiChip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiChip from '@core/components/ui/chip/UiChip.vue'
|
||||
import UiInput, { type InputAccent } from '@core/components/ui/input/UiInput.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { accent, placeholder } = defineProps<{
|
||||
accent: InputAccent
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const tags = defineModel<string[]>({ required: true })
|
||||
|
||||
const draft = ref('')
|
||||
|
||||
const TAG_SEPARATOR_KEYS = ['Enter', ',']
|
||||
|
||||
// A pasted draft may hold several comma-separated tags
|
||||
function addDraftTags() {
|
||||
const draftTags = new Set(draft.value.split(',').map(tag => tag.trim()))
|
||||
const newTags = [...draftTags].filter(tag => tag !== '' && !tags.value.includes(tag))
|
||||
|
||||
draft.value = ''
|
||||
|
||||
if (newTags.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
tags.value = [...tags.value, ...newTags]
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Enter also validates an IME composition, which must not add a half-typed tag
|
||||
const isSeparator = !event.isComposing && TAG_SEPARATOR_KEYS.includes(event.key)
|
||||
|
||||
if (!isSeparator) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prevents a form submit on Enter and keeps the comma out of the draft
|
||||
event.preventDefault()
|
||||
addDraftTags()
|
||||
}
|
||||
|
||||
function removeTag(index: number) {
|
||||
tags.value = tags.value.filter((_tag, tagIndex) => tagIndex !== index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.vts-tags-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user