250 lines
6 KiB
Vue
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>
|
|
|