Autocomplete.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <!--
  2. Liste déroulante avec autocompletion
  3. @see https://vuetifyjs.com/en/components/autocompletes/#usage
  4. -->
  5. <template>
  6. <main>
  7. <v-autocomplete
  8. autocomplete="search"
  9. :value="data"
  10. :items="itemsToDisplayed"
  11. :label="$t(fieldLabel)"
  12. item-text="itemTextDisplay"
  13. :item-value="itemValue"
  14. :no-data-text="$t('autocomplete_research')"
  15. :no-filter="noFilter"
  16. auto-select-first
  17. :multiple="multiple"
  18. :loading="isLoading"
  19. :return-object="returnObject"
  20. :search-input.sync="search"
  21. :prepend-icon="prependIcon"
  22. :error="error || !!violation"
  23. :error-messages="errorMessage || violation ? $t(violation) : ''"
  24. :rules="rules"
  25. :chips="chips"
  26. @input="onChange($event)"
  27. >
  28. <template v-if="slotText" #item="data">
  29. <v-list-item-content v-text="data.item.slotTextDisplay"></v-list-item-content>
  30. </template>
  31. </v-autocomplete>
  32. </main>
  33. </template>
  34. <script setup lang="ts">
  35. import {useNuxtApp} from "#app";
  36. import {computed, ComputedRef, Ref} from "@vue/reactivity";
  37. import {useFieldViolation} from "~/composables/form/useFieldViolation";
  38. import {AnyJson} from "~/types/enum/data";
  39. import {onUnmounted, watch} from "@vue/runtime-core";
  40. import ObjectUtils from "~/services/utils/objectUtils";
  41. const props = defineProps({
  42. label: {
  43. type: String,
  44. required: false,
  45. default: null
  46. },
  47. field: {
  48. type: String,
  49. required: false,
  50. default: null
  51. },
  52. data: {
  53. type: [String, Number, Object, Array],
  54. required: false,
  55. default: null
  56. },
  57. items: {
  58. type: Array,
  59. required: false,
  60. default: () => []
  61. },
  62. readonly: {
  63. type: Boolean,
  64. required: false
  65. },
  66. itemValue: {
  67. type: String,
  68. default: 'id'
  69. },
  70. itemText: {
  71. type: Array,
  72. required: true
  73. },
  74. group:{
  75. type: String,
  76. required: false,
  77. default: null
  78. },
  79. slotText: {
  80. type: Array,
  81. required: false,
  82. default: null
  83. },
  84. returnObject: {
  85. type: Boolean,
  86. default: false
  87. },
  88. multiple: {
  89. type: Boolean,
  90. default: false
  91. },
  92. isLoading: {
  93. type: Boolean,
  94. default: false
  95. },
  96. noFilter: {
  97. type: Boolean,
  98. default: false
  99. },
  100. prependIcon: {
  101. type: String
  102. },
  103. translate: {
  104. type: Boolean,
  105. default: false
  106. },
  107. rules: {
  108. type: Array,
  109. required: false,
  110. default: () => []
  111. },
  112. chips: {
  113. type: Boolean,
  114. default: false
  115. },
  116. error: {
  117. type: Boolean,
  118. required: false
  119. },
  120. errorMessage: {
  121. type: String,
  122. required: false,
  123. default: null
  124. }
  125. })
  126. const { emit } = useNuxtApp()
  127. const { i18n } = useNuxtApp()
  128. const search: Ref<string|null> = ref(null)
  129. const fieldLabel = props.label ?? props.field
  130. const { violation, onChange } = useFieldViolation(props.field, emit)
  131. // On reconstruit les items à afficher...
  132. const itemsToDisplayed: ComputedRef<Array<AnyJson>> = computed(() => {
  133. const itemsByGroup:Array<Array<string>> = classItemsByGroup(props.items)
  134. return prepareItemsToDisplayed(itemsByGroup)
  135. })
  136. const unwatch = watch(
  137. search,
  138. useDebounce(async (newResearch, oldResearch) => {
  139. if(newResearch !== oldResearch && oldResearch !== null)
  140. emit('research', newResearch)
  141. }, 500)
  142. )
  143. onUnmounted(() => {
  144. unwatch()
  145. })
  146. /**
  147. * On construit l'Array à double entrée contenant les groups (headers) et les propositions
  148. *
  149. * @param items
  150. */
  151. const classItemsByGroup = (items:Array<any>): Array<Array<string>> => {
  152. const group = props.group as string
  153. const itemsByGroup: Array<Array<string>> = []
  154. for (const item of items) {
  155. if (item) {
  156. if (!itemsByGroup[item[group]]) {
  157. itemsByGroup[item[group]] = []
  158. }
  159. itemsByGroup[item[group]].push(item)
  160. }
  161. }
  162. return itemsByGroup
  163. }
  164. /**
  165. * Construction de l'Array JSON contenant toutes les propositions à afficher dans le select
  166. *
  167. * @param itemsByGroup
  168. */
  169. const prepareItemsToDisplayed = (itemsByGroup: Array<Array<string>>): Array<AnyJson> => {
  170. let finalItems: Array<AnyJson> = []
  171. for (const group in itemsByGroup) {
  172. // Si un groupe est présent, alors on créé le groupe options header
  173. if (group !== 'undefined') {
  174. finalItems.push({header: i18n.t(group as string)})
  175. }
  176. // On parcourt les items pour préparer les texts / slotTexts à afficher
  177. finalItems = finalItems.concat(itemsByGroup[group].map((item: any) => {
  178. const slotTextDisplay: Array<string> = []
  179. const itemTextDisplay: Array<string> = []
  180. item = ObjectUtils.cloneAndFlatten(item)
  181. // Si on souhaite avoir un texte différent dans les propositions que dans la sélection finale de select
  182. if (props.slotText) {
  183. for (const text of props.slotText) {
  184. slotTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
  185. }
  186. }
  187. for (const text of props.itemText) {
  188. itemTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
  189. }
  190. // On reconstruit l'objet
  191. return Object.assign({}, item, { itemTextDisplay: itemTextDisplay.join(' '), slotTextDisplay: slotTextDisplay.join(' ') })
  192. }))
  193. }
  194. return finalItems
  195. }
  196. </script>