Преглед изворни кода

Adds search functionality to TreeSelect component

Implements a search functionality within the TreeSelect component
to allow users to quickly filter and find specific items.

The search filters the displayed items based on the input text,
expanding relevant categories to reveal matching items.

Includes reset search when the menu closes.
Olivier Massot пре 4 месеци
родитељ
комит
ee9ac69ffa
1 измењених фајлова са 154 додато и 6 уклоњено
  1. 154 6
      components/Ui/Input/TreeSelect.vue

+ 154 - 6
components/Ui/Input/TreeSelect.vue

@@ -8,14 +8,31 @@
     chips
     closable-chips
     :menu-props="{ maxHeight: 400 }"
+    @update:menu="onMenuUpdate"
     v-bind="$attrs"
   >
+    <template #prepend-item>
+      <v-text-field
+        v-model="searchText"
+        density="compact"
+        hide-details
+        placeholder="Rechercher..."
+        prepend-inner-icon="fas fa-magnifying-glass"
+        variant="outlined"
+        clearable
+        class="mx-2 my-2"
+        @click.stop
+        @input="onSearchInput"
+      />
+      <v-divider class="mt-2"/>
+    </template>
+
     <template #selection="{ item, index }">
       <v-chip
         v-if="maxVisibleChips && index < maxVisibleChips"
         :key="item.raw.value"
         closable
-        @click:close="removeItem(item.raw.value)"
+        @click:close="removeItem(item.raw.value!)"
       >
         {{ item.raw.label }}
       </v-chip>
@@ -27,7 +44,7 @@
       </span>
     </template>
 
-    <template #item="{ props, item }">
+    <template #item="{ item }">
       <template v-if="item.raw.type === 'category'">
         <v-list-item
           @click.stop="toggleCategory(item.raw.id)"
@@ -69,14 +86,14 @@
 
       <template v-else>
         <v-list-item
-          @click="toggleItem(item.raw.value)"
-          :active="selectedItems.includes(item.raw.value)"
+          @click="toggleItem(item.raw.value!)"
+          :active="selectedItems.includes(item.raw.value!)"
           :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)"
+              :model-value="selectedItems.includes(item.raw.value!)"
+              @click.stop="toggleItem(item.raw.value!)"
               color="primary"
               :hide-details="true"
             />
@@ -91,6 +108,8 @@
 </template>
 
 <script setup lang="ts">
+import StringUtils from '~/services/utils/stringUtils'
+
 interface SelectItem {
   id: string
   label: string
@@ -121,6 +140,47 @@ const emit = defineEmits(['update:modelValue'])
 // État réactif
 const expandedCategories = ref<Set<string>>(new Set())
 const expandedSubcategories = ref<Set<string>>(new Set())
+const searchText = ref('')
+
+// Fonction pour gérer l'entrée de recherche
+const onSearchInput = () => {
+  // Réinitialiser les états d'expansion dans tous les cas
+  expandedCategories.value.clear()
+  expandedSubcategories.value.clear()
+
+  if (searchText.value.trim()) {
+    // Trouver tous les éléments qui correspondent à la recherche
+    const matchingItems = props.items.filter(item =>
+      item.type === 'item' &&
+      item.level === 2 &&
+      itemMatchesSearch(item)
+    )
+
+    // Pour chaque élément correspondant, ajouter ses parents aux ensembles d'expansion
+    for (const item of matchingItems) {
+      // Trouver et ajouter la sous-catégorie parente
+      const subcategory = props.items.find(i => i.id === item.parentId)
+      if (subcategory) {
+        expandedSubcategories.value.add(subcategory.id)
+
+        // Trouver et ajouter la catégorie parente
+        const category = props.items.find(i => i.id === subcategory.parentId)
+        if (category) {
+          expandedCategories.value.add(category.id)
+        }
+      }
+    }
+  }
+  // Si la recherche est vide, toutes les catégories restent repliées (déjà réinitialisées)
+}
+
+// Fonction pour gérer l'ouverture/fermeture du menu
+const onMenuUpdate = (isOpen: boolean) => {
+  // Réinitialiser la recherche quand le menu se ferme
+  if (!isOpen && searchText.value) {
+    searchText.value = ''
+  }
+}
 
 // Modèle v-model
 const selectedItems = computed({
@@ -128,10 +188,86 @@ const selectedItems = computed({
   set: (value: string[]) => emit('update:modelValue', value)
 })
 
+// Fonction pour vérifier si un élément correspond à la recherche
+const itemMatchesSearch = (item: SelectItem): boolean => {
+  if (!searchText.value) return true
+
+  const normalizedSearch = StringUtils.normalize(searchText.value)
+
+  // Si c'est un élément de niveau 2, vérifier son label et les labels de ses parents
+  if (item.type === 'item' && item.level === 2) {
+    // Vérifier le label de l'élément
+    if (StringUtils.normalize(item.label).includes(normalizedSearch)) return true
+
+    // Trouver et vérifier le label de la sous-catégorie parente
+    const subcategory = props.items.find(i => i.id === item.parentId)
+    if (subcategory && StringUtils.normalize(subcategory.label).includes(normalizedSearch)) return true
+
+    // Trouver et vérifier le label de la catégorie parente
+    if (subcategory && subcategory.parentId) {
+      const category = props.items.find(i => i.id === subcategory.parentId)
+      if (category && StringUtils.normalize(category.label).includes(normalizedSearch)) return true
+    }
+
+    return false
+  }
+
+  // Pour les autres types d'éléments, vérifier simplement leur label
+  return StringUtils.normalize(item.label).includes(normalizedSearch)
+}
+
 // Construction de la liste aplatie pour v-select
 const flattenedItems = computed(() => {
   const result: SelectItem[] = []
+  const hasSearch = !!searchText.value.trim()
+
+  // Si une recherche est active, afficher uniquement les éléments de niveau 2 qui correspondent
+  if (hasSearch) {
+    // Trouver tous les éléments de niveau 2 qui correspondent à la recherche
+    const matchingItems = props.items.filter(item =>
+      item.type === 'item' &&
+      item.level === 2 &&
+      itemMatchesSearch(item)
+    )
+
+    // Ensemble pour suivre les catégories et sous-catégories déjà ajoutées
+    const addedCategoryIds = new Set<string>()
+    const addedSubcategoryIds = new Set<string>()
+
+    // Pour chaque élément correspondant, ajouter sa hiérarchie complète
+    for (const item of matchingItems) {
+      // Trouver la sous-catégorie parente
+      const subcategory = props.items.find(i => i.id === item.parentId)
+      if (!subcategory) continue
+
+      // Trouver la catégorie parente
+      const category = props.items.find(i => i.id === subcategory.parentId)
+      if (!category) continue
+
+      // Ajouter la catégorie si elle n'est pas déjà présente
+      if (!addedCategoryIds.has(category.id)) {
+        result.push(category)
+        addedCategoryIds.add(category.id)
+        // S'assurer que la catégorie est considérée comme "expanded" pendant la recherche
+        expandedCategories.value.add(category.id)
+      }
+
+      // Ajouter la sous-catégorie si elle n'est pas déjà présente
+      if (!addedSubcategoryIds.has(subcategory.id)) {
+        result.push(subcategory)
+        addedSubcategoryIds.add(subcategory.id)
+        // S'assurer que la sous-catégorie est considérée comme "expanded" pendant la recherche
+        expandedSubcategories.value.add(subcategory.id)
+      }
+
+      // Ajouter l'élément
+      result.push(item)
+    }
 
+    return result
+  }
+
+  // Comportement normal sans recherche
   const processItems = (items: SelectItem[], parentExpanded = true) => {
     for (const item of items) {
       if (item.type === 'category') {
@@ -210,4 +346,16 @@ const removeItem = (value: string) => {
 .v-list-item--active {
   background-color: rgba(var(--v-theme-primary), 0.1);
 }
+
+.search-icon {
+  color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
+}
+
+:deep(.v-field__prepend-inner) {
+  padding-top: 0;
+}
+
+:deep(.v-list) {
+  padding-top: 0;
+}
 </style>