Autocomplete.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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 {onUnmounted, watch} from "@vue/runtime-core";
  39. import ObjectUtils from "~/services/utils/objectUtils";
  40. const props = defineProps({
  41. label: {
  42. type: String,
  43. required: false,
  44. default: null
  45. },
  46. field: {
  47. type: String,
  48. required: false,
  49. default: null
  50. },
  51. data: {
  52. type: [String, Number, Object, Array],
  53. required: false,
  54. default: null
  55. },
  56. items: {
  57. type: Array,
  58. required: false,
  59. default: () => []
  60. },
  61. readonly: {
  62. type: Boolean,
  63. required: false
  64. },
  65. itemValue: {
  66. type: String,
  67. default: 'id'
  68. },
  69. itemText: {
  70. type: Array,
  71. required: true
  72. },
  73. group:{
  74. type: String,
  75. required: false,
  76. default: null
  77. },
  78. slotText: {
  79. type: Array,
  80. required: false,
  81. default: null
  82. },
  83. returnObject: {
  84. type: Boolean,
  85. default: false
  86. },
  87. multiple: {
  88. type: Boolean,
  89. default: false
  90. },
  91. isLoading: {
  92. type: Boolean,
  93. default: false
  94. },
  95. noFilter: {
  96. type: Boolean,
  97. default: false
  98. },
  99. prependIcon: {
  100. type: String
  101. },
  102. translate: {
  103. type: Boolean,
  104. default: false
  105. },
  106. rules: {
  107. type: Array,
  108. required: false,
  109. default: () => []
  110. },
  111. chips: {
  112. type: Boolean,
  113. default: false
  114. },
  115. error: {
  116. type: Boolean,
  117. required: false
  118. },
  119. errorMessage: {
  120. type: String,
  121. required: false,
  122. default: null
  123. }
  124. })
  125. const { emit } = useNuxtApp()
  126. const { i18n } = useNuxtApp()
  127. const search: Ref<string|null> = ref(null)
  128. const fieldLabel = props.label ?? props.field
  129. const { violation, onChange } = useFieldViolation(props.field, emit)
  130. // On reconstruit les items à afficher...
  131. const itemsToDisplayed: ComputedRef<Array<AnyJson>> = computed(() => {
  132. const itemsByGroup:Array<Array<string>> = classItemsByGroup(props.items)
  133. return prepareItemsToDisplayed(itemsByGroup)
  134. })
  135. const unwatch = watch(
  136. search,
  137. useDebounce(async (newResearch, oldResearch) => {
  138. if(newResearch !== oldResearch && oldResearch !== null)
  139. emit('research', newResearch)
  140. }, 500)
  141. )
  142. onUnmounted(() => {
  143. unwatch()
  144. })
  145. /**
  146. * On construit l'Array à double entrée contenant les groups (headers) et les propositions
  147. *
  148. * @param items
  149. */
  150. const classItemsByGroup = (items:Array<any>): Array<Array<string>> => {
  151. const group = props.group as string
  152. const itemsByGroup: Array<Array<string>> = []
  153. for (const item of items) {
  154. if (item) {
  155. if (!itemsByGroup[item[group]]) {
  156. itemsByGroup[item[group]] = []
  157. }
  158. itemsByGroup[item[group]].push(item)
  159. }
  160. }
  161. return itemsByGroup
  162. }
  163. /**
  164. * Construction de l'Array JSON contenant toutes les propositions à afficher dans le select
  165. *
  166. * @param itemsByGroup
  167. */
  168. const prepareItemsToDisplayed = (itemsByGroup: Array<Array<string>>): Array<AnyJson> => {
  169. let finalItems: Array<AnyJson> = []
  170. for (const group in itemsByGroup) {
  171. // Si un groupe est présent, alors on créé le groupe options header
  172. if (group !== 'undefined') {
  173. finalItems.push({header: i18n.t(group as string)})
  174. }
  175. // On parcourt les items pour préparer les texts / slotTexts à afficher
  176. finalItems = finalItems.concat(itemsByGroup[group].map((item: any) => {
  177. const slotTextDisplay: Array<string> = []
  178. const itemTextDisplay: Array<string> = []
  179. item = ObjectUtils.cloneAndFlatten(item)
  180. // Si on souhaite avoir un texte différent dans les propositions que dans la sélection finale de select
  181. if (props.slotText) {
  182. for (const text of props.slotText) {
  183. slotTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
  184. }
  185. }
  186. for (const text of props.itemText) {
  187. itemTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
  188. }
  189. // On reconstruit l'objet
  190. return Object.assign({}, item, { itemTextDisplay: itemTextDisplay.join(' '), slotTextDisplay: slotTextDisplay.join(' ') })
  191. }))
  192. }
  193. return finalItems
  194. }
  195. </script>