Autocomplete.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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(label_field)"
  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"
  23. :rules="rules"
  24. @input="onChange($event)"
  25. >
  26. <template v-if="slotText" #item="data">
  27. <v-list-item-content v-text="data.item.slotTextDisplay"></v-list-item-content>
  28. </template>
  29. </v-autocomplete>
  30. </main>
  31. </template>
  32. <script lang="ts">
  33. import {computed, defineComponent, ComputedRef, Ref, ref, watch, onUnmounted, useContext} from '@nuxtjs/composition-api'
  34. import { AnyJson } from '~/types/interfaces'
  35. import * as _ from 'lodash'
  36. import {$objectProperties} from "~/services/utils/objectProperties";
  37. import {$useError} from "~/composables/form/useError";
  38. export default defineComponent({
  39. props: {
  40. label: {
  41. type: String,
  42. required: false,
  43. default: null
  44. },
  45. field: {
  46. type: String,
  47. required: false,
  48. default: null
  49. },
  50. data: {
  51. type: [String, Number, Object, Array],
  52. required: false,
  53. default: null
  54. },
  55. items: {
  56. type: Array,
  57. required: false,
  58. default: () => []
  59. },
  60. readonly: {
  61. type: Boolean,
  62. required: false
  63. },
  64. itemValue: {
  65. type: String,
  66. default: 'id'
  67. },
  68. itemText: {
  69. type: Array,
  70. required: true
  71. },
  72. group:{
  73. type: String,
  74. required: false,
  75. default: null
  76. },
  77. slotText: {
  78. type: Array,
  79. required: false,
  80. default: null
  81. },
  82. returnObject: {
  83. type: Boolean,
  84. default: false
  85. },
  86. multiple: {
  87. type: Boolean,
  88. default: false
  89. },
  90. isLoading: {
  91. type: Boolean,
  92. default: false
  93. },
  94. noFilter: {
  95. type: Boolean,
  96. default: false
  97. },
  98. prependIcon: {
  99. type: String
  100. },
  101. translate: {
  102. type: Boolean,
  103. default: false
  104. },
  105. rules: {
  106. type: Array,
  107. required: false,
  108. default: () => []
  109. },
  110. },
  111. setup (props, { emit }) {
  112. const {app:{i18n}} = useContext()
  113. const search:Ref<string|null> = ref(null)
  114. const {error, onChange} = $useError(props.field, emit)
  115. // On reconstruit les items à afficher...
  116. const itemsToDisplayed: ComputedRef<Array<AnyJson>> = computed(() => {
  117. const itemsByGroup:Array<Array<string>> = classItemsByGroup(props.items)
  118. return prepareItemsToDisplayed(itemsByGroup)
  119. })
  120. const unwatch = watch(search, _.debounce(async (newResearch, oldResearch) => {
  121. if(newResearch !== oldResearch && oldResearch !== null)
  122. emit('research', newResearch)
  123. }, 500))
  124. onUnmounted(() => {
  125. unwatch()
  126. })
  127. /**
  128. * On construit l'Array à double entrée contenant les groups (headers) et les propositions
  129. * @param items
  130. */
  131. const classItemsByGroup = (items:Array<any>): Array<Array<string>> => {
  132. const itemsByGroup:Array<Array<string>> = []
  133. for (const item of items){
  134. if(item){
  135. if(!itemsByGroup[item[props.group as string]])
  136. itemsByGroup[item[props.group as string]] = []
  137. itemsByGroup[item[props.group as string]].push(item)
  138. }
  139. }
  140. return itemsByGroup
  141. }
  142. /**
  143. * Construction de l'Array JSON contenant toutes les propositions à afficher dans le select
  144. * @param itemsByGroup
  145. */
  146. const prepareItemsToDisplayed = (itemsByGroup:Array<Array<string>>):Array<AnyJson> => {
  147. let finalItems:Array<AnyJson> = []
  148. for(const group in itemsByGroup){
  149. //Si un group est présent, alors on créer le group options header
  150. if(group !== 'undefined'){
  151. finalItems.push({header: i18n.t(group as string)})
  152. }
  153. //On parcours les items pour préparer les texts/slotTexts à afficher
  154. finalItems = finalItems.concat(itemsByGroup[group].map((item: any) => {
  155. const slotTextDisplay: Array<string> = []
  156. const itemTextDisplay: Array<string> = []
  157. item = $objectProperties.cloneAndFlatten(item)
  158. //Si on souhaite avoir un texte différent dans les propositions que dans la sélection finale de select
  159. if(props.slotText){
  160. for (const text of props.slotText) {
  161. slotTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
  162. }
  163. }
  164. for (const text of props.itemText) {
  165. itemTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
  166. }
  167. //On reconstruit l'objet
  168. return Object.assign({}, item, { itemTextDisplay: itemTextDisplay.join(' '), slotTextDisplay: slotTextDisplay.join(' ') })
  169. }))
  170. }
  171. return finalItems
  172. }
  173. return {
  174. label_field: props.label ?? props.field,
  175. itemsToDisplayed,
  176. search,
  177. error,
  178. onChange
  179. }
  180. }
  181. })
  182. </script>