| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- <!--
- Liste déroulante avec autocompletion, à placer dans un composant `UiForm`
- @see https://vuetifyjs.com/en/components/autocompletes/#usage
- -->
- <template>
- <main>
- <!--suppress TypeScriptValidateTypes -->
- <v-autocomplete
- :model-value="modelValue"
- autocomplete="search"
- :items="items"
- :label="$t(fieldLabel)"
- item-text="itemTextDisplay"
- :item-value="itemValue"
- :no-data-text="$t('autocomplete_research')"
- :no-filter="noFilter"
- auto-select-first
- :multiple="multiple"
- :loading="isLoading"
- :return-object="returnObject"
- :search-input.sync="search"
- :prepend-icon="prependIcon"
- :error="error || !!fieldViolations"
- :error-messages="errorMessage || fieldViolations ? $t(fieldViolations) : ''"
- :rules="rules"
- :chips="chips"
- @update:model-value="onUpdate"
- >
- <template v-if="slotText" #item="data">
- <v-list-item-content v-text="data.item.slotTextDisplay"></v-list-item-content>
- </template>
- </v-autocomplete>
- </main>
- </template>
- <script setup lang="ts">
- import {computed, ComputedRef, Ref} from "@vue/reactivity";
- import {useFieldViolation} from "~/composables/form/useFieldViolation";
- import ObjectUtils from "~/services/utils/objectUtils";
- import {AnyJson} from "~/types/data";
- const props = defineProps({
- /**
- * v-model
- */
- modelValue: {
- type: [String, Number, Object, Array],
- required: false,
- default: null
- },
- /**
- * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
- * - Utilisé par la validation
- * - Laisser null si le champ ne s'applique pas à une entité
- */
- field: {
- type: String,
- required: false,
- default: null
- },
- /**
- * Label du champ
- * Si non défini, c'est le nom de propriété qui est utilisé
- */
- label: {
- type: String,
- required: false,
- default: null
- },
- /**
- * Liste des éléments de la liste
- * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-items
- */
- items: {
- type: Array<Object>,
- required: false,
- default: () => []
- },
- /**
- * Définit si le champ est en lecture seule
- * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-readonly
- */
- readonly: {
- type: Boolean,
- required: false
- },
- /**
- * Le model est l'objet lui-même, et non pas son id (ou la propriété définie avec itemValue)
- * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-return-object
- */
- returnObject: {
- type: Boolean,
- default: false
- },
- /**
- * Autorise la sélection multiple
- * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-multiple
- */
- multiple: {
- type: Boolean,
- default: false
- },
- /**
- * Propriété de l'objet à utiliser comme label
- * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-item-title
- */
- itemTitle: {
- type: String,
- required: true
- },
- /**
- * Propriété de l'objet à utiliser comme clé (et correspondant au v-model)
- * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-item-value
- */
- itemValue: {
- type: String,
- default: 'id'
- },
- /**
- * Icône de gauche
- * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-prepend-icon
- */
- prependIcon: {
- type: String
- },
- /**
- * Rends les résultats sous forme de puces
- * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-chips
- */
- chips: {
- type: Boolean,
- default: false
- },
- /**
- * Le contenu de la liste est en cours de chargement
- */
- isLoading: {
- type: Boolean,
- required: false,
- default: false
- },
- /**
- * Propriété de l'objet utilisé pour grouper les items ; laisser null pour ne pas grouper
- */
- group: {
- type: String,
- required: false,
- default: null
- },
- // TODO: c'est quoi?
- slotText: {
- type: Array,
- required: false,
- default: null
- },
- // TODO: c'est quoi?
- noFilter: {
- type: Boolean,
- default: false
- },
- // TODO: c'est quoi?
- translate: {
- type: Boolean,
- default: false
- },
- /**
- * Règles de validation
- * @see https://vuetify.cn/en/components/forms/#validation-with-submit-clear
- */
- rules: {
- type: Array,
- required: false,
- default: () => []
- },
- /**
- * Le champ est-il actuellement en état d'erreur
- */
- error: {
- type: Boolean,
- required: false
- },
- /**
- * Si le champ est en état d'erreur, quel est le message d'erreur?
- */
- errorMessage: {
- type: String,
- required: false,
- default: null
- }
- })
- const i18n = useI18n()
- const search: Ref<string|null> = ref(null)
- const fieldLabel: string = props.label ?? props.field
- const {fieldViolations, updateViolationState} = useFieldViolation(props.field)
- const emit = defineEmits(['update:model-value'])
- const onUpdate = (event: string) => {
- updateViolationState(event)
- emit('update:model-value', props.modelValue)
- }
- /**
- * Items à afficher
- * TODO: à revoir
- */
- const items: ComputedRef<Array<AnyJson>> = computed(() => {
- let _items: Array<any> = props.items
- return _items
- // if (props.group !== null) {
- // _items = groupItems(props.items)
- // }
- //
- // return prepareGroups(_items)
- })
- /**
- * On construit l'Array à double entrée contenant les groups (headers) et les items
- * TODO: à revoir
- *
- * @param items
- */
- const groupItems = (items: Array<any>): Array<Array<string>> => {
- const group = props.group as string | null
- if (group === null) {
- return items
- }
- const itemsByGroup: Array<Array<string>> = []
- let groupValue = null
- for (const item of items) {
- if (item) {
- groupValue = item[group]
- if (!itemsByGroup[groupValue]) {
- itemsByGroup[groupValue] = []
- }
- itemsByGroup[groupValue].push(item)
- }
- }
- return itemsByGroup
- }
- /**
- * Construction de l'Array JSON contenant toutes les propositions à afficher dans le select
- * TODO: à revoir
- *
- * @param groupedItems
- */
- const prepareGroups = (groupedItems: Array<Array<string>>): Array<AnyJson> => {
- let finalItems: Array<AnyJson> = []
- for (const group in groupedItems) {
- // Si un groupe est présent, alors on créé le groupe options header
- if (group !== 'undefined') {
- finalItems.push({header: i18n.t(group as string)})
- }
- // On parcourt les items pour préparer les texts / slotTexts à afficher
- finalItems = finalItems.concat(groupedItems[group].map((item: any) => {
- return prepareItem(item)
- }))
- }
- return finalItems
- }
- /**
- * Construction d'un item
- * TODO: à revoir
- *
- * @param item
- */
- const prepareItem = (item: Object): AnyJson => {
- const slotTextDisplay: Array<string> = []
- const itemTextDisplay: Array<string> = []
- item = ObjectUtils.cloneAndFlatten(item)
- // Si on souhaite avoir un texte différent dans les propositions que dans la sélection finale de select
- if (props.slotText) {
- for (const text of props.slotText) {
- slotTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
- }
- }
- for (const text of props.itemTitle) {
- itemTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
- }
- // On reconstruit l'objet
- return Object.assign({}, item, { itemTextDisplay: itemTextDisplay.join(' '), slotTextDisplay: slotTextDisplay.join(' ') })
- }
- </script>
|