|
|
@@ -0,0 +1,206 @@
|
|
|
+<template>
|
|
|
+ <v-select
|
|
|
+ v-model="selectedItems"
|
|
|
+ :items="flattenedItems"
|
|
|
+ item-title="label"
|
|
|
+ item-value="value"
|
|
|
+ multiple
|
|
|
+ chips
|
|
|
+ closable-chips
|
|
|
+ :menu-props="{ maxHeight: 400 }"
|
|
|
+ v-bind="$attrs"
|
|
|
+ >
|
|
|
+ <template #selection="{ item, index }">
|
|
|
+ <v-chip
|
|
|
+ v-if="index < maxVisibleChips"
|
|
|
+ :key="item.raw.value"
|
|
|
+ closable
|
|
|
+ @click:close="removeItem(item.raw.value)"
|
|
|
+ >
|
|
|
+ {{ item.raw.label }}
|
|
|
+ </v-chip>
|
|
|
+ <span
|
|
|
+ v-if="index === maxVisibleChips && selectedItems.length > maxVisibleChips"
|
|
|
+ class="text-grey text-caption align-self-center"
|
|
|
+ >
|
|
|
+ (+{{ selectedItems.length - maxVisibleChips }} autres)
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template #item="{ props, item }">
|
|
|
+ <template v-if="item.raw.type === 'category'">
|
|
|
+ <v-list-item
|
|
|
+ @click.stop="toggleCategory(item.raw.id)"
|
|
|
+ :ripple="false"
|
|
|
+ :class="{ 'v-list-item--active': expandedCategories.has(item.raw.id) }"
|
|
|
+ >
|
|
|
+ <template #prepend>
|
|
|
+ <v-icon
|
|
|
+ :icon="'fas ' + (expandedCategories.has(item.raw.id) ? 'fa-angle-down' : 'fa-angle-right')"
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <v-list-item-title class="font-weight-medium">
|
|
|
+ {{ item.raw.label }}
|
|
|
+ </v-list-item-title>
|
|
|
+ </v-list-item>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else-if="item.raw.type === 'subcategory'">
|
|
|
+ <v-list-item
|
|
|
+ @click.stop="toggleSubcategory(item.raw.id)"
|
|
|
+ :ripple="false"
|
|
|
+ :class="{
|
|
|
+ 'v-list-item--active': expandedSubcategories.has(item.raw.id),
|
|
|
+ 'pl-8': true
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <template #prepend>
|
|
|
+ <v-icon
|
|
|
+ :icon="'fas ' + (expandedSubcategories.has(item.raw.id) ? 'fa-angle-down' : 'fa-angle-right')"
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <v-list-item-title>
|
|
|
+ {{ item.raw.label }}
|
|
|
+ </v-list-item-title>
|
|
|
+ </v-list-item>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <v-list-item
|
|
|
+ v-bind="props"
|
|
|
+ :class="{ 'pl-12': item.raw.level === 2, 'pl-8': item.raw.level === 1 }"
|
|
|
+ >
|
|
|
+ <template #prepend>
|
|
|
+ <v-checkbox
|
|
|
+ :model-value="selectedItems.includes(item.raw.value)"
|
|
|
+ @click.stop="toggleItem(item.raw.value)"
|
|
|
+ color="primary"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <v-list-item-title>
|
|
|
+ {{ item.raw.label }}
|
|
|
+ </v-list-item-title>
|
|
|
+ </v-list-item>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </v-select>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+interface SelectItem {
|
|
|
+ id: string
|
|
|
+ label: string
|
|
|
+ value?: string
|
|
|
+ type: 'category' | 'subcategory' | 'item'
|
|
|
+ parentId?: string
|
|
|
+ level: number
|
|
|
+}
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ modelValue: {
|
|
|
+ type: Array as PropType<string[]>,
|
|
|
+ required: true
|
|
|
+ },
|
|
|
+ items: {
|
|
|
+ type: Array as PropType<SelectItem[]>,
|
|
|
+ required: true
|
|
|
+ },
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['update:modelValue'])
|
|
|
+
|
|
|
+// État réactif
|
|
|
+const expandedCategories = ref<Set<string>>(new Set())
|
|
|
+const expandedSubcategories = ref<Set<string>>(new Set())
|
|
|
+
|
|
|
+// Modèle v-model
|
|
|
+const selectedItems = computed({
|
|
|
+ get: () => props.modelValue,
|
|
|
+ set: (value: string[]) => emit('update:modelValue', value)
|
|
|
+})
|
|
|
+
|
|
|
+// Construction de la liste aplatie pour v-select
|
|
|
+const flattenedItems = computed(() => {
|
|
|
+ const result: SelectItem[] = []
|
|
|
+
|
|
|
+ const processItems = (items: SelectItem[], parentExpanded = true) => {
|
|
|
+ for (const item of items) {
|
|
|
+ if (item.type === 'category') {
|
|
|
+ result.push(item)
|
|
|
+ if (expandedCategories.value.has(item.id)) {
|
|
|
+ const subcategories = props.items.filter(i =>
|
|
|
+ i.parentId === item.id && i.type === 'subcategory'
|
|
|
+ )
|
|
|
+ processItems(subcategories, true)
|
|
|
+ }
|
|
|
+ } else if (item.type === 'subcategory') {
|
|
|
+ if (parentExpanded) {
|
|
|
+ result.push(item)
|
|
|
+ if (expandedSubcategories.value.has(item.id)) {
|
|
|
+ const subItems = props.items.filter(i =>
|
|
|
+ i.parentId === item.id && i.type === 'item'
|
|
|
+ )
|
|
|
+ processItems(subItems, true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (item.type === 'item' && parentExpanded) {
|
|
|
+ result.push(item)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const topLevelItems = props.items.filter(item => !item.parentId)
|
|
|
+ processItems(topLevelItems)
|
|
|
+
|
|
|
+ return result
|
|
|
+})
|
|
|
+
|
|
|
+// Méthodes
|
|
|
+const toggleCategory = (categoryId: string) => {
|
|
|
+ if (expandedCategories.value.has(categoryId)) {
|
|
|
+ expandedCategories.value.delete(categoryId)
|
|
|
+ // Fermer aussi les sous-catégories
|
|
|
+ const subcategories = props.items.filter(i =>
|
|
|
+ i.parentId === categoryId && i.type === 'subcategory'
|
|
|
+ )
|
|
|
+ subcategories.forEach(sub => {
|
|
|
+ expandedSubcategories.value.delete(sub.id)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ expandedCategories.value.add(categoryId)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const toggleSubcategory = (subcategoryId: string) => {
|
|
|
+ if (expandedSubcategories.value.has(subcategoryId)) {
|
|
|
+ expandedSubcategories.value.delete(subcategoryId)
|
|
|
+ } else {
|
|
|
+ expandedSubcategories.value.add(subcategoryId)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const toggleItem = (value: string) => {
|
|
|
+ const currentSelection = [...selectedItems.value]
|
|
|
+ const index = currentSelection.indexOf(value)
|
|
|
+
|
|
|
+ if (index > -1) {
|
|
|
+ currentSelection.splice(index, 1)
|
|
|
+ } else {
|
|
|
+ currentSelection.push(value)
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedItems.value = currentSelection
|
|
|
+}
|
|
|
+
|
|
|
+const removeItem = (value: string) => {
|
|
|
+ selectedItems.value = selectedItems.value.filter(item => item !== value)
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.v-list-item--active {
|
|
|
+ background-color: rgba(var(--v-theme-primary), 0.1);
|
|
|
+}
|
|
|
+</style>
|