ldc-com-app-bo/resources/ts/bo/components/BelongsToManyDropdownComponent.vue
2025-05-20 10:28:50 +02:00

250 lines
6 KiB
Vue

<template>
<div class="belongs-to-many-dropdown">
<Transition name="fade">
<p
v-if="message"
class="text-danger mb-2"
>
{{ message }}
</p>
</Transition>
<div class="d-inline-block tagInput w-100">
<VueTagsInputCls
v-model="item"
:tags="selectedItems"
:autocomplete-items="filteredItems"
:autocomplete-min-length="0"
@before-adding-tag="beforeTagSave"
@tags-changed="changedTags"
:max-tags="max ?? 9999"
:placeholder="placeholder"
/>
</div>
<input
v-if="!selectedItems.length"
:name="name"
type="hidden"
value=""
>
<template
v-for="(sItem, index) in selectedItems"
:key="index"
>
<p class="text-danger m-0">
{{ parseLaravelErrorMessages.methods.parseValidationInput(`${name}[${String(index)}][name]`, allErrors) }}
</p>
<input
:name="`${name}[${String(index)}][id]`"
type="hidden"
:value="sItem.id"
>
<input
:name="`${name}[${String(index)}][value]`"
type="hidden"
:value="sItem.id"
>
<input
:name="`${name}[${String(index)}][name]`"
type="hidden"
:value="sItem.text"
>
<input
:name="`${name}[${String(index)}][label]`"
type="hidden"
:value="sItem.text"
>
<input
:name="`${name}[${String(index)}][title]`"
type="hidden"
:value="sItem.text"
>
</template>
</div>
</template>
<script lang="ts" setup>
import type VueTagsInput from "@sipec/vue3-tags-input";
import { ref, computed, onBeforeMount, onMounted, useAttrs } from "vue";
import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input";
import slugify from "slugify";
import parseLaravelErrorMessages from "./../../modules/parseLaravelErrorMessages";
import trans from "./../../modules/trans";
defineOptions({
name: "BelongsToManyDropdownComponent"
});
// * ATTRIBUTES
const attrs = useAttrs();
// * DATA
const item = ref<string>("");
const name = ref<string>("");
const selectedItems = ref<CustomTag[]>([]);
const allItems = ref<CustomTag[]>([]);
const placeholder = ref<string>("");
const min = ref<number>(0);
const max = ref<number | null>(null);
const allErrors = ref<Record<string, string[]>>({});
const message = ref<string | null>(null);
// Check attributes before mount
onBeforeMount(() => {
if (!Object.hasOwnProperty.call(attrs, "json")) {
throw new Error("this component requires json");
}
});
onMounted(() => {
const json = String((attrs as any).json ?? "{}");
const data = JSON.parse(json);
name.value = data.name;
selectedItems.value = parseTags(data.value);
allItems.value = parseTags(data.itemsList);
placeholder.value = data.placeholder;
min.value = Math.max(Number.parseInt((data?.itemLimit ?? [])[0] ?? 0), 0);
const parsedMax = Number.parseInt((data?.itemLimit ?? [])[1] ?? 0);
max.value = parsedMax > min.value ? parsedMax : null;
allErrors.value = data.errors;
});
function parseTags(selectedItems: CustomTag[]): CustomTag[] {
return selectedItems.map((item) => {
item.text = String(item.label ?? item.name ?? item.title);
item.slug = String(item.slug ?? slugify(item.text));
return item;
});
}
/**
* Add item
* @param obj
*/
function beforeTagSave(obj: VueTagsInput.IAddArgs): void {
if (!max.value || selectedItems.value.length < max.value) {
let customTag = tagExists(allItems.value, obj.tag);
if (customTag) {
obj.tag = customTag;
selectedItems.value.push(customTag);
item.value = "";
return;
}
} else {
message.value = trans.methods.__("Nombre d'élements maximal atteint.");
setTimeout(() => (message.value = null), 1500);
}
}
/**
* Delete item.
* @param newItems
*/
function changedTags(newItems: CustomTag[]): void {
// * Si on enleve un élement
if (selectedItems.value.length > newItems.length) {
// * On respecte la validation minimale
if (newItems.length >= min.value) {
selectedItems.value = newItems as CustomTag[];
} else {
// * Non respect de la validation minimale
message.value = trans.methods.__("Nombre d'élements minimal atteint.");
setTimeout(() => (message.value = null), 1500);
// * Réassignation pour forcer la valeur minimale.
selectedItems.value = Object.assign([], selectedItems);
}
}
}
function tagExists(
items: CustomTag[],
tagToCheck: VueTagsInput.ITag
): CustomTag | null {
let customTag: CustomTag | null = null;
items.filter((item: CustomTag) => {
if (
item.id === (tagToCheck as CustomTag).id ||
item.slug === slugify(tagToCheck.text)
) {
customTag = item;
}
});
return customTag;
}
const filteredItems = computed(() =>
allItems.value.filter((i) => {
return i.text.toLowerCase().indexOf(item.value.toLowerCase()) !== -1;
})
);
</script>
<style lang="scss">
.belongs-to-many-dropdown {
max-width: 100% !important;
.vue-tags-input {
max-width: 100% !important;
}
.ti- {
&autocomplete {
max-height: 10rem !important;
border-radius: 0.375rem;
box-shadow: 0 0 4px var(--bs-gray-400);
overflow-y: auto !important;
ul {
padding: 0.25rem 0 !important;
}
}
&input {
padding: 0.375rem !important;
border-radius: 0.375rem;
}
&tags {
line-height: inherit !important;
}
&tag,
&new-tag-input-wrapper {
padding: 0.1rem !important;
margin: 0.1rem !important;
}
&new-tag-input-wrapper {
font-size: 1em !important;
input {
min-width: 165px !important;
}
}
&tag {
padding: 0.1rem 0.3rem !important;
background-color: var(--bs-blue) !important;
border-radius: 0.375rem !important;
}
&selected-item {
background-color: var(--bs-gray-300) !important;
color: var(--bs-gray-900) !important;
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
}
</style>