447 lines
15 KiB
Vue
447 lines
15 KiB
Vue
<template>
|
|
<div
|
|
ref="reorderElementsFromMorphToMany"
|
|
class="reorder-elements-from-morph-to-many-component"
|
|
>
|
|
<div class="row justify-content-start">
|
|
<!-- ITEMS LIST -->
|
|
<div class="col-12 col-xl-6 mb-3 mb-xl-0">
|
|
<!-- SEARCH INPUT -->
|
|
<label class="col-form-label">
|
|
<b>{{ titleList }} *</b>
|
|
</label>
|
|
<Transition name="fade">
|
|
<div v-if="errorMessage.length">
|
|
<span
|
|
class="d-block text-danger mb-2"
|
|
>
|
|
{{ errorMessage }}
|
|
</span>
|
|
</div>
|
|
</Transition>
|
|
<div class="position-relative">
|
|
<!-- LOADING SCREEN -->
|
|
<div
|
|
v-if="loading"
|
|
class="position-absolute top-0 start-0 w-100 h-100 bg-dark bg-opacity-25 rounded-3 z-1"
|
|
>
|
|
<div class="position-absolute top-50 start-50 translate-middle">
|
|
<div
|
|
class="spinner-border text-white"
|
|
role="status"
|
|
>
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- SELECTED ITEMS -->
|
|
<div class="selected-items-search d-flex flex-wrap">
|
|
<div
|
|
v-for="(item, itemId) in itemsParameters.selected"
|
|
:key="itemId"
|
|
class="d-flex justify-content-center align-items-center bg-light w-fit border rounded-1 p-1 mt-1 me-1"
|
|
>
|
|
<span class="text-bg-light">{{ item.label }}</span>
|
|
<button
|
|
:class="['btn btn-sm btn-danger ms-1', (itemsParameters.selected.length <= itemsParameters.selectedLimit[0]) ? 'disabled': '']"
|
|
:aria-label="__('Dissocier ce produit à la recette')"
|
|
:title="__('Dissocier ce produit à la recette')"
|
|
data-bs-toggle="tooltip"
|
|
type="button"
|
|
@click.prevent="removeElementFromSelectedList(Number(item.id))"
|
|
>
|
|
<FontAwesomeIcon icon="fa-solid fa-trash" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- INPUT SEARCH -->
|
|
<div :class="['d-flex flex-wrap border rounded-3 my-1', getErrorMessage() !== null ? 'border-danger': '']">
|
|
<div :class="['d-flex justify-content-center align-items-center w-100', (reorderElementsFromMorphToManySearch?.value.length ? 'pe-1': '')]">
|
|
<input
|
|
ref="reorderElementsFromMorphToManySearch"
|
|
type="text"
|
|
placeholder="Saisissez ici vos mots clés..."
|
|
:class="['form-control border-0 w-100', getErrorMessage() !== null ? 'is-invalid': '']"
|
|
@input.prevent="searchItem()"
|
|
:disabled="loading"
|
|
>
|
|
<button
|
|
v-if="reorderElementsFromMorphToManySearch?.value.length"
|
|
class="btn btn-sm btn-primary"
|
|
:title="__('Effacer la recherche')"
|
|
data-bs-toggle="tooltip"
|
|
:aria-label="__('Effacer la recherche')"
|
|
type="button"
|
|
@click.prevent="resetSearchValue()"
|
|
>
|
|
<FontAwesomeIcon icon="fa-solid fa-xmark" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- ITEMS LIST -->
|
|
<div class="items-list-wrapper border rounded-3">
|
|
<template v-if="itemsParameters.listFiltered.length">
|
|
<template
|
|
v-for="(item, itemId) in itemsParameters.listFiltered"
|
|
:key="itemId"
|
|
>
|
|
<div
|
|
v-if="!checkItemSelected(Number(item.id))"
|
|
:class="[
|
|
'd-flex justify-content-between align-items-center px-2 py-1',
|
|
(itemId !== itemsParameters.listFiltered.length - 1 ? 'border-bottom' : ''),
|
|
((checkItemSelected(Number(item.id)) && itemsParameters.selected.length <= itemsParameters.selectedLimit[0]) || (!checkItemSelected(Number(item.id)) && itemsParameters.selected.length >= itemsParameters.selectedLimit[1]) ? 'bg-light': ''),
|
|
]"
|
|
>
|
|
<div class="col-11">
|
|
<p class="m-0">
|
|
{{ item.label }}
|
|
</p>
|
|
</div>
|
|
<div class="col-1 text-end">
|
|
<button
|
|
:class="['btn btn-sm btn-primary', (itemsParameters.selected.length >= itemsParameters.selectedLimit[1]) ? 'disabled': '']"
|
|
:aria-label="__('Associer ce produit à la recette')"
|
|
:title="__('Associer ce produit à la recette')"
|
|
data-bs-toggle="tooltip"
|
|
type="button"
|
|
@click.prevent="addElementFromSelectedList(item)"
|
|
>
|
|
<FontAwesomeIcon icon="fa-solid fa-plus" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<div
|
|
v-else
|
|
class="d-flex flex-column justify-content-center align-items-center h-100"
|
|
>
|
|
<span class="search-result-icon">
|
|
<FontAwesomeIcon
|
|
icon="fa-solid fa-triangle-exclamation fa-2xl"
|
|
class="w-100 h-100"
|
|
/>
|
|
</span>
|
|
<p class="mt-1 mb-2">
|
|
{{ __('Aucun produit ne correspond à votre recherche') }}
|
|
</p>
|
|
<button
|
|
class="btn btn-sm btn-primary"
|
|
:title="__('Effacer la recherche')"
|
|
data-bs-toggle="tooltip"
|
|
:aria-label="__('Effacer la recherche')"
|
|
type="button"
|
|
@click.prevent="resetSearchValue()"
|
|
>
|
|
{{ __('Réinitialiser la recherche') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="w-100">
|
|
<small class="text-danger m-0">
|
|
{{ getErrorMessage() }}
|
|
</small>
|
|
</div>
|
|
<div class="w-100">
|
|
<small class="form-text text-body-secondary">
|
|
{{ helper }}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
<!-- REORDER SELECTED ITEMS -->
|
|
<div class="col-12 col-xl-6">
|
|
<label class="col-form-label">
|
|
<b>{{ __('Réorganisez les éléments en les déplaçant.') }}</b>
|
|
</label>
|
|
<ReorderElements :nodelist="itemsParameters.selected" />
|
|
</div>
|
|
<!-- INPUTS FOR SUBMISSION -->
|
|
<input
|
|
v-for="(item, itemIndex) in itemsParameters.selected"
|
|
:key="itemIndex"
|
|
type="hidden"
|
|
:name="name + '[]'"
|
|
:value="item.id"
|
|
>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
|
import { __ } from "@modules/trans";
|
|
import { nextTick, onMounted, reactive, ref, useAttrs } from "vue";
|
|
import axios from "~/axios";
|
|
import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages";
|
|
import { Tooltips } from "./../../modules/bsTooltips";
|
|
import route from "./../../modules/route";
|
|
import ReorderElements from "./ReorderElements.vue";
|
|
|
|
defineOptions({
|
|
name: "ReorderElementsFromMorphToManyComponent",
|
|
inheritAttrs: true,
|
|
});
|
|
|
|
// * ATTRIBUTES
|
|
const attrs = useAttrs();
|
|
|
|
// * REFS
|
|
const reorderElementsFromMorphToMany = ref<HTMLDivElement|null>(null);
|
|
const reorderElementsFromMorphToManySearch = ref<HTMLInputElement|null>(null);
|
|
|
|
// * DATA
|
|
const loading = ref<boolean>(false);
|
|
const name = ref<string>("");
|
|
const fieldName = ref<string>("");
|
|
const titleList = ref<string>("");
|
|
const helper = ref<string>("");
|
|
const errorMessage = ref<string>("");
|
|
const allErrors = ref<LaravelValidationErrors>({});
|
|
const tooltips = ref<Tooltips|null>(null);
|
|
|
|
/** Select parameters. */
|
|
const itemsParameters = reactive<{
|
|
selected: LaravelModelList,
|
|
selectedLimit: number[],
|
|
list: LaravelModelList,
|
|
listFiltered: LaravelModelList,
|
|
}>({
|
|
selected: [],
|
|
selectedLimit: [1,5],
|
|
list: [],
|
|
listFiltered: [],
|
|
});
|
|
|
|
/** Search parameters. */
|
|
const searchParameters = reactive<{
|
|
targetClass: string,
|
|
routeName: string,
|
|
}>({
|
|
targetClass: "",
|
|
routeName: "",
|
|
});
|
|
|
|
/** Page parameters. */
|
|
const pageParameters = reactive<{
|
|
page: number,
|
|
totalPage: number,
|
|
itemPerPage: number,
|
|
hasNextPage: boolean,
|
|
hasPrevPage: boolean,
|
|
}>({
|
|
page: 1,
|
|
totalPage: 1,
|
|
itemPerPage: 1,
|
|
hasPrevPage: false,
|
|
hasNextPage: false,
|
|
});
|
|
|
|
// * LIFECYCLE
|
|
onMounted(() => {
|
|
const json = String(attrs.json ?? "{}"),
|
|
data = JSON.parse(json);
|
|
/** Items parameters. */
|
|
itemsParameters.selected = data.itemsSelected;
|
|
itemsParameters.selectedLimit = data.itemsSelectedLimit;
|
|
itemsParameters.list = data.itemsList.data;
|
|
itemsParameters.listFiltered = itemsParameters.list;
|
|
/** Search parameters. */
|
|
searchParameters.targetClass = String(data.targetClass);
|
|
searchParameters.routeName = String(data.routeName);
|
|
/** Page parameters. */
|
|
pageParameters.page = data.itemsList.current_page;
|
|
pageParameters.totalPage = data.itemsList.last_page;
|
|
pageParameters.itemPerPage = data.itemsList.per_page;
|
|
pageParameters.hasPrevPage = false;
|
|
pageParameters.hasNextPage = false;
|
|
/** Others parameters. */
|
|
name.value = data.name;
|
|
fieldName.value = data.fieldName;
|
|
titleList.value = data.titleList;
|
|
helper.value = data.helper;
|
|
allErrors.value = data.errors;
|
|
initTooltips();
|
|
setScrollPagination();
|
|
});
|
|
|
|
/** Set the infinite pagination. */
|
|
function setScrollPagination(): void {
|
|
document.querySelector(".items-list-wrapper")?.addEventListener("scroll", (event) => {
|
|
const { scrollHeight, scrollTop, clientHeight } = event.target as HTMLDivElement;
|
|
if (Math.abs(scrollHeight - clientHeight - scrollTop) < 1 && pageParameters.page < pageParameters.totalPage) {
|
|
pageParameters.page++;
|
|
loading.value = true;
|
|
window.axios
|
|
.get(getJsonRouteUrl())
|
|
.then((response) => {
|
|
itemsParameters.listFiltered = itemsParameters.listFiltered.concat(response.data.data);
|
|
pageParameters.totalPage = response.data.last_page;
|
|
pageParameters.page = response.data.current_page;
|
|
})
|
|
.then(() => { loading.value = false; })
|
|
.catch(ajaxErrorHandler);
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Get items from a specific page. */
|
|
function searchItem(): void {
|
|
loading.value = true;
|
|
pageParameters.page = 1;
|
|
document.querySelector(".items-list-wrapper")!.scrollTop = 0;
|
|
window.axios
|
|
.get(getJsonRouteUrl())
|
|
.then((response) => {
|
|
itemsParameters.listFiltered = response.data.data;
|
|
pageParameters.totalPage = response.data.last_page;
|
|
pageParameters.page = response.data.current_page;
|
|
})
|
|
.then(() => {
|
|
loading.value = false;
|
|
nextTick(() => {
|
|
reorderElementsFromMorphToManySearch.value!.focus();
|
|
});
|
|
})
|
|
.catch(ajaxErrorHandler);
|
|
}
|
|
|
|
/**
|
|
* Return error message after the ajax call.
|
|
* @param e any
|
|
* @return void
|
|
*/
|
|
function ajaxErrorHandler(e: unknown): void {
|
|
let message = __("Une erreur est survenue");
|
|
|
|
if (axios.isAxiosError<LaravelValidationErrorResponse>(e)) {
|
|
if (e.response) {
|
|
if (e.response.status === 422) {
|
|
message = parseLaravelErrorMessages.methods.parseValidationErrors(
|
|
e.response?.data?.errors ?? {}
|
|
) || message;
|
|
if (window.vueDebug) {
|
|
console.warn(e.response.data.errors);
|
|
}
|
|
} else if (e.response.status === 419) {
|
|
console.log(__("Votre session a expiré, la page va être rechargée"));
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
} else if (e.response.status === 403) {
|
|
console.log(__("Vous n'êtes pas autorisé a effectuer cette action"));
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
} else {
|
|
if (window.vueDebug) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
} else if (e.request) {
|
|
console.error("Aucune réponse reçue :", e.request);
|
|
} else {
|
|
console.error("Erreur de configuration de la requête :", e.message);
|
|
}
|
|
}
|
|
console.log(message);
|
|
}
|
|
|
|
/** Return json route to get paginate models. */
|
|
function getJsonRouteUrl(): string {
|
|
const routeUrl = route(searchParameters.routeName);
|
|
if (!routeUrl) {
|
|
throw new Error("Undefined route " + searchParameters.routeName);
|
|
}
|
|
return routeUrl + "?targetModel=" + searchParameters.targetClass +
|
|
"&page=" + pageParameters.page +
|
|
"&paginate=" + pageParameters.itemPerPage +
|
|
"&fieldName=" + fieldName.value +
|
|
((reorderElementsFromMorphToManySearch.value?.value.length) ? "&search=" + reorderElementsFromMorphToManySearch.value?.value : "");
|
|
}
|
|
|
|
/** Reset the textual search value. */
|
|
function resetSearchValue(): void {
|
|
reorderElementsFromMorphToManySearch.value!.value = "";
|
|
tooltips.value?.closeBootstrapTooltip();
|
|
searchItem();
|
|
}
|
|
|
|
/** Add specific element from the selected list. */
|
|
function addElementFromSelectedList(item: LaravelModel): void {
|
|
if (itemsParameters.selected.length >= itemsParameters.selectedLimit[1]) {
|
|
errorMessage.value = "Limite maximale atteinte, vous ne pouvez pas ajouter d'éléments supplémentaires";
|
|
setTimeout(() => errorMessage.value = "", 5000);
|
|
throw new Error("Maximum limit reached, you cannot add an additional element.");
|
|
}
|
|
itemsParameters.selected = itemsParameters.selected.concat(item);
|
|
tooltips.value?.closeBootstrapTooltip();
|
|
initTooltips();
|
|
}
|
|
|
|
/** Remove specific element from the selected list. */
|
|
function removeElementFromSelectedList(id: number): void {
|
|
if (itemsParameters.selected.length <= itemsParameters.selectedLimit[0]) {
|
|
errorMessage.value = "Limite minimale atteinte, vous ne pouvez pas supprimer d'éléments supplémentaires";
|
|
setTimeout(() => errorMessage.value = "", 5000);
|
|
throw new Error("Minimum limit reached, you cannot delete an additional element");
|
|
}
|
|
itemsParameters.selected = itemsParameters.selected.filter((item) => {
|
|
return item.id !== id;
|
|
});
|
|
tooltips.value?.closeBootstrapTooltip();
|
|
initTooltips();
|
|
}
|
|
|
|
/** Check if an id exist in selected items list. */
|
|
function checkItemSelected(id: number): boolean {
|
|
return Boolean(itemsParameters.selected.find((item) => {
|
|
return item.id === id;
|
|
}));
|
|
}
|
|
|
|
/** Return error message or null. */
|
|
function getErrorMessage(): string|null {
|
|
return parseLaravelErrorMessages.methods.parseValidationInput(
|
|
`${name.value}`,
|
|
allErrors.value
|
|
);
|
|
}
|
|
|
|
/** Initialise all tooltips in the component. */
|
|
function initTooltips(): void {
|
|
setTimeout(() => {
|
|
tooltips.value = Tooltips.make({
|
|
type: "dom",
|
|
elements: reorderElementsFromMorphToMany.value!.querySelectorAll("[data-bs-toggle=\"tooltip\"]")
|
|
});
|
|
}, 500);
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.reorder-elements-from-morph-to-many-component {
|
|
.spinner-border {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
}
|
|
.items-list-wrapper {
|
|
height: 20rem;
|
|
overflow: auto;
|
|
}
|
|
.search-result-icon {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
}
|
|
.fade-enter-active,
|
|
.fade-leave-active {
|
|
transition: opacity 0.5s ease;
|
|
}
|
|
.fade-enter-from,
|
|
.fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
.reorder-elements {
|
|
.handle {
|
|
cursor: grab;
|
|
}
|
|
}
|
|
}
|
|
</style>
|