ldc-legaulois/resources/ts/fo/components/SearchModelComponent.vue

961 lines
34 KiB
Vue

<template>
<div
ref="searchModel"
class="search-model-component row w-100 mx-auto gx-5"
>
<!-- TEXTS MOBILE -->
<div class="d-md-none col-12 col-md-7 col-xl-8 text-center">
<h2 ref="modelSubtitleMobile">
{{ textsParameters.subtitle }}
</h2>
<p
ref="modelDescriptionMobile"
v-html="textsParameters.description"
/>
</div>
<!-- FILTERS -->
<div class="col-12 col-md-5 col-xl-4">
<div
class="row filters-list"
ref="filtersList"
>
<!-- TEXT FILTER -->
<div class="col-12 mb-3 z-1">
<div class="search-custom bottom-left-shadow">
<SearchBelongsToDropdownComponent
name="words"
:multiple="true"
:fieldname="searchParameters.fieldName"
:modeltarget="searchParameters.modelTarget"
:modellistpaginate="searchParameters.modelListPaginate"
:modelselected="searchParameters.selectedList"
:routename="searchParameters.routeList"
@selected-value-changed="getModelByText"
:placeholder="searchParameters.placeholder"
:disabled="loading || paginationParameters.loading"
/>
</div>
</div>
<!-- FILTERS LIST -->
<div class="col-12">
<button
class="d-md-none btn btn-secondary w-100 mb-3"
type="button"
:title="__('Ouvrir le panneau des filtres')"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvasRecipesFilters"
aria-controls="offcanvasRecipesFilters"
>
<span class="pe-2">
{{ __('Filtres') }}
</span>
<img
src="@/images/recipes/filter.svg"
:alt="__('Icône de filtres')"
>
</button>
<div
class="offcanvas-md offcanvas-start"
tabindex="-1"
id="offcanvasRecipesFilters"
aria-labelledby="offcanvasRecipesFiltersLabel"
>
<div class="offcanvas-header">
<button
type="button"
class="btn btn-link text-decoration-none f-catamaran fw-semibold"
data-bs-dismiss="offcanvas"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvasRecipesFilters"
:aria-label="__('Fermer le menu')"
>
<FontAwesomeIcon
icon="fa-solid fa-xmark"
class="me-2"
/>
{{ __('Fermer') }}
</button>
</div>
<div class="offcanvas-body py-0">
<div class="row w-100 mx-auto">
<div
class="col-12 bottom-left-shadow p-0"
v-for="(filter, filterIndex) in filters"
:key="filterIndex"
>
<div class="line-border-small mb-3">
<button
class="btn btn-transparent d-flex justify-content-between align-items-center fs-4 text-start border-0 shadow-none w-100"
type="button"
data-bs-toggle="collapse"
:data-bs-target="'#' + slugify(filter.title, slugifyOptions)"
aria-expanded="true"
:aria-controls="slugify(filter.title, slugifyOptions)"
>
{{ filter.title }}
<span class="f-bourton-base">></span>
</button>
<div
class="collapse show"
:id="slugify(filter.title, slugifyOptions)"
>
<fieldset
:inert="loading || paginationParameters.loading"
class="d-flex justify-content-start align-items-center flex-wrap px-4 py-2"
>
<div
class="w-fit"
v-for="(model, modelIndex) in filter.models"
:key="modelIndex"
>
<input
class="d-none"
type="checkbox"
:name="slugify(filter.title, slugifyOptions)"
:id="`${slugify(filter.title, slugifyOptions)}-${ modelIndex }`"
@click="getModelByFilters(filter, model, $event)"
:checked="filtersDetailsParameters.selectedFilter === slugify(String(model.label ?? model.title), slugifyOptions)"
:disabled="loading || paginationParameters.loading"
>
<label
:class="[
'form-check-label btn position-relative border-0 mb-3 me-3 px-2 py-1',
filtersDetailsParameters.selectedFilter === slugify(String(model.label ?? model.title), slugifyOptions) ? 'btn-secondary' : 'btn-tertiary',
]"
:style="[filtersDetailsParameters.selectedFilter === slugify(String(model.label ?? model.title), slugifyOptions) && model.color !== undefined ? `background-color:${model.color}!important;` : '']"
:for="`${slugify(filter.title, slugifyOptions)}-${ modelIndex }`"
data-bs-dismiss="offcanvas"
data-bs-target="#offcanvasRecipesFilters"
>
{{ model.label ?? model.title }}
<span
v-if="filtersDetailsParameters.selectedFilter === slugify(String(model.label ?? model.title), slugifyOptions)"
:class="[
'filters-btn d-flex justify-content-center align-items-center position-absolute',
{'bg-secondary':filtersDetailsParameters.selectedFilter === slugify(String(model.label ?? model.title), slugifyOptions)}
]"
:style="`background-color:${model.color}!important;`"
>
<FontAwesomeIcon icon="fa-solid fa-xmark" />
</span>
</label>
</div>
</fieldset>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- MODELS LIST -->
<div class="col-12 col-md-7 col-xl-8">
<div class="row gx-5">
<!-- TEXTS DESKTOP -->
<div class="d-none d-md-block col-12 text-start">
<h2 ref="modelSubtitle">
{{ textsParameters.subtitle }}
</h2>
<p
ref="modelDescription"
v-html="textsParameters.description"
/>
</div>
<!-- MODELS LIST -->
<template v-if="!loading">
<div
v-if="filtersDetailsParameters.selectedFilter.length && filters[getFilterById(filtersDetailsParameters.selectedFilterId)].children?.models.length"
ref="childrenList"
class="col-12"
>
<div class="children-list position-relative px-5 mb-3">
<div class="swiper swiper-children overflow-hidden position-relative p-2 z-1">
<div class="swiper-wrapper w-fit">
<div
class="swiper-slide w-fit"
v-for="(model, modelIndex) in filters[getFilterById(filtersDetailsParameters.selectedFilterId)].children?.models"
:key="modelIndex"
>
<input
class="d-none"
type="checkbox"
name="child"
@click="getModelByChildren(filters[getFilterById(filtersDetailsParameters.selectedFilterId)].children!, model, $event)"
:id="`child-${modelIndex}`"
:checked="filtersDetailsParameters.selectedChildFilter === slugify(String(model.label ?? model.title), slugifyOptions)"
>
<label
:class="[
'form-check-label btn position-relative border-0 px-2 py-1',
filtersDetailsParameters.selectedChildFilter === slugify(String(model.label ?? model.title), slugifyOptions) ? 'btn-secondary' : 'btn-white'
]"
:for="`child-${modelIndex}`"
>
{{ model.label ?? model.title }}
<span
v-if="filtersDetailsParameters.selectedChildFilter === slugify(String(model.label ?? model.title), slugifyOptions)"
class="filters-btn d-flex justify-content-center align-items-center position-absolute bg-secondary"
>
<FontAwesomeIcon icon="fa-solid fa-xmark" />
</span>
</label>
</div>
</div>
</div>
<button class="swiper-btns swiper-btn-prev border-0 d-flex justify-content-center align-items-center position-absolute top-50 z-1 bg-white rounded-pill">
<img
src="@/images/common/arrow-brown.svg"
:title="__('Navigation du slider (gauche)')"
:alt="__('flèche vers la gauche')"
role="button"
>
</button>
<button class="swiper-btns swiper-btn-next border-0 d-flex justify-content-center align-items-center position-absolute top-50 translate-middle-y z-1 bg-white rounded-pill">
<img
src="@/images/common/arrow-brown.svg"
:title="__('Navigation du slider (droite)')"
:alt="__('flèche vers la droite')"
role="button"
>
</button>
</div>
</div>
<!-- MODELS LIST -->
<template v-if="filtersDetailsParameters.viewsPaginate.length">
<div
v-for="(view, viewIndex) in filtersDetailsParameters.viewsPaginate"
:key="viewIndex"
class="col-12 col-lg-6 py-4"
v-html="view"
/>
<!-- INFORMATION -->
<p
v-if="paginationParameters.page === paginationParameters.lastPage"
class="text-center mt-5"
>
{{ __('Tous les éléments ont été chargés') }}
</p>
</template>
<!-- NO RESULT -->
<div
v-else
class="col-12 d-flex flex-column justify-content-center align-items-center"
>
<span class="no-result-icon">
<FontAwesomeIcon
icon="fa-solid fa-triangle-exclamation"
class="w-100 h-100"
/>
</span>
<p class="text-center m-0 pt-2">
{{ __("Aucun résultat ne correspond à votre recherche,") }}
</p>
<p class="text-center m-0 pb-3">
{{ __("essayez un autre paramètre de recherche ou réinitialisez-la.") }}
</p>
<button
@click="resetList(true)"
class="btn btn-primary"
type="button"
>
{{ __("Ré-initialiser la recherche") }}
</button>
</div>
</template>
<!-- PLACEHOLDERS -->
<div
v-if="loading || paginationParameters.loading"
class="col-12 col-md-6 recipe-card placeholder-glow bottom-left-shadow"
>
<div class="position-relative d-flex justify-content-center">
<span class="placeholder ratio ratio-16x9 line-border-medium w-100" />
</div>
<div class="line-border-medium position-relative d-flex flex-column align-items-center recipe-card-bottom-part bg-white px-3 pt-5">
<p class="placeholder placeholder-title col-7" />
<div class="d-flex flex-wrap justify-content-around align-items-center w-100">
<p class="placeholder placeholder-time col-3" />
<p class="placeholder placeholder-time col-3" />
<p class="placeholder placeholder-time col-3" />
</div>
<div class="d-flex flex-wrap justify-content-around align-items-center w-100">
<p class="placeholder placeholder-tag col-3" />
<p class="placeholder placeholder-tag col-3" />
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { __ } from "@modules/trans.js";
import slugify from "slugify";
import Swiper from "swiper";
import { Navigation } from "swiper/modules";
import type { App } from "vue";
import { createApp, nextTick, onMounted, reactive, ref, useAttrs } from "vue";
import axios from "~/axios/index.js";
import SearchBelongsToDropdownComponent from "./../../bo/components/SearchBelongsToDropdownComponent.vue";
import parseLaravelErrorMessages from "./../../modules/parseLaravelErrorMessages";
import route from "./../../modules/route";
import BtnClubFavoriteComponent from "./BtnClubFavoriteComponent.vue";
defineOptions({
name: "SearchModelComponent"
});
/** Slugify options */
const slugifyOptions = { lower: true, strict:true, trim: true };
// * ATTRIBUTES
const attrs = useAttrs();
// * REFS
const searchModel = ref<HTMLDivElement|null>(null);
const filtersList = ref<HTMLDivElement|null>(null);
const childrenList = ref<HTMLDivElement|null>(null);
const modelSubtitle = ref<HTMLTitleElement|null>(null);
const modelDescription = ref<HTMLParagraphElement|null>(null);
const modelSubtitleMobile = ref<HTMLTitleElement|null>(null);
const modelDescriptionMobile = ref<HTMLParagraphElement|null>(null);
// * DATA
/** Texts parameters. */
const textsParameters = reactive<{
title: string,
subtitle: string,
description: string,
}>({
title: "",
subtitle: "",
description: "",
});
/** Filters parameters */
const filters = ref<Array<{
id: string,
title: string,
models: LaravelModelList,
route: string,
children: {
models: LaravelModelList,
route: string,
}|null
}>>([]);
/** Filters details parameters. */
const filtersDetailsParameters = reactive<{
viewsPaginate: Array<string>,
defaultPath: string,
selectedModel: LaravelModel|null,
selectedChildModel: LaravelModel|null,
selectedFilter: string,
selectedChildFilter: string,
selectedFilterId: string,
}>({
viewsPaginate: [],
defaultPath: "",
selectedModel: null,
selectedChildModel: null,
selectedFilter: "",
selectedChildFilter: "",
selectedFilterId: "",
});
/** Search parameters */
const searchParameters = reactive<{
modelTarget: string,
modelListPaginate: LaravelPaginator|undefined,
routeWords: string,
routeList: string,
fieldName: string,
placeholder: string,
selectedList: VselectOption[]|null,
}>({
modelTarget: "",
modelListPaginate: undefined,
routeWords: "",
routeList: "",
fieldName: "",
placeholder: "",
selectedList: null,
});
/** Pagination parameters. */
const paginationParameters = reactive<{
page: number,
itemPerPage: number,
lastPage: number,
loading: boolean,
}>({
page: 1,
itemPerPage: 1,
lastPage: 1,
loading: false,
});
/** This is the 'mise en avant' field for recipes */
const highlightModel = ref<LaravelModel|null>();
const swiper = ref<Swiper|null>(null);
const loading = ref<boolean>(false);
// * LIFECYCLE
onMounted(() => {
const json = String(attrs.json ?? "{}"),
data = JSON.parse(json);
highlightModel.value = data.highlightModel ?? null;
/** Texts parameters. */
textsParameters.title = data.texts.title;
textsParameters.subtitle = data.texts.subtitle;
textsParameters.description = data.texts.description;
/** Filters parameters */
filters.value = data.filters;
/** Filters details parameters. */
filtersDetailsParameters.viewsPaginate = data.filtersDetails.viewsPaginate.data;
filtersDetailsParameters.defaultPath = data.filtersDetails.defaultPath;
filtersDetailsParameters.selectedFilter = data.filtersDetails.selectedFilter;
filtersDetailsParameters.selectedChildFilter = data.filtersDetails.selectedChildFilter;
/** Pagination parameters. */
paginationParameters.page = data.filtersDetails.viewsPaginate.current_page;
paginationParameters.itemPerPage = data.filtersDetails.viewsPaginate.per_page;
paginationParameters.lastPage = data.filtersDetails.viewsPaginate.last_page;
/** Search parameters. */
searchParameters.modelTarget = data.search.modelTarget;
searchParameters.modelListPaginate = data.search.modelListPaginate;
searchParameters.routeWords = data.search.routeWords;
searchParameters.routeList = data.search.routeList;
searchParameters.fieldName = data.search.fieldName;
searchParameters.placeholder = data.search.placeholder;
checkScroll();
nextTick(() => {
filtersDetailsParameters.selectedFilterId = String(getFilterId());
initChildrenSwiper();
updateTexts();
mountCardComponents();
});
});
/**
* Update texts (title/subtitle/description) with the model selected
* or default texts if there aren't any.
* @param string|null newTitle
*/
function updateTexts(newTitle: string|null = null): void {
getCurrentModels();
const currentModelSelected = (filtersDetailsParameters.selectedChildModel !== null)
? filtersDetailsParameters.selectedChildModel
: filtersDetailsParameters.selectedModel;
modelSubtitle.value!.textContent = String(((currentModelSelected !== null) ? currentModelSelected : textsParameters).subtitle);
modelDescription.value!.innerHTML = String(((currentModelSelected !== null) ? currentModelSelected : textsParameters).description);
modelSubtitleMobile.value!.textContent = String(((currentModelSelected !== null) ? currentModelSelected : textsParameters).subtitle);
modelDescriptionMobile.value!.innerHTML = String(((currentModelSelected !== null) ? currentModelSelected : textsParameters).description);
const parentTitle = (filtersDetailsParameters.selectedChildModel !== null)
? filtersDetailsParameters.selectedModel?.label + ", "
: "";
const currentFilter = parentTitle + (
newTitle !== null
? newTitle
: (currentModelSelected !== null
? (currentModelSelected?.title) ? currentModelSelected?.title : currentModelSelected?.label
: "")
);
document.querySelector(".current-filter")!.textContent = filtersDetailsParameters.selectedModel !== null ? " : " + currentFilter : "";
updateImage(currentModelSelected);
}
/**
* Update header image with current filter model selected,
* or with an image from a random filter model.
* @param LaravelModel|null currentModelSelected
*/
function updateImage(currentModelSelected: LaravelModel|null): void {
if (currentModelSelected === null) {
if(highlightModel.value) {
currentModelSelected = highlightModel.value;
} else {
let models = [] as LaravelModelList;
filters.value.map(function(filter) {
models = models.concat(filter.models);
if (filter.children)
models = models.concat(filter.children.models);
});
currentModelSelected = models[Math.floor(Math.random() * models.length)];
}
}
const image = document.querySelector("#header-image") as HTMLImageElement|null;
image!.src = "/" + String(currentModelSelected.picture);
image?.setAttribute("alt", String(currentModelSelected.picture_alt));
image?.setAttribute("title", String(currentModelSelected.picture_title));
}
/**
* Get models selected if there are any.
* @return void
*/
function getCurrentModels(): void {
getCurrentModelSelected();
getCurrentChildModelSelected();
}
/**
* Get parent model selected if there is one.
* @return void
*/
function getCurrentModelSelected(): void {
filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)].models!.find(model => {
if (model.slug === filtersDetailsParameters.selectedFilter)
filtersDetailsParameters.selectedModel = model;
});
}
/**
* Get child model selected if there is one.
* @return void
*/
function getCurrentChildModelSelected(): void {
filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)].children?.models!.find(model => {
if (model.slug === filtersDetailsParameters.selectedChildFilter)
filtersDetailsParameters.selectedChildModel = model;
});
}
/**
* Init children slider.
* @return void
*/
function initChildrenSwiper(): void {
nextTick(() => {
swiper.value = new Swiper(".swiper-children", {
modules: [Navigation],
grabCursor: true,
slidesPerView: "auto",
spaceBetween: 20,
navigation: {
nextEl: ".children-list .swiper-btn-next",
prevEl: ".children-list .swiper-btn-prev",
}
});
});
}
/**
* Choose between "sub" or root route.
* e.g fo.search.subrange or fo.search.range
* @return string
*/
const routeOrSubRouteSearch = () => {
if(filtersDetailsParameters.selectedChildModel) {
return filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)].children?.route ?? filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)].route;
}
return filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)].route;
};
/**
* Check if all models are loaded,
* if not, get next models.
* @return void
*/
function checkScroll(): void {
window.addEventListener("scroll", () => {
if (
(window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 800)
&& !loading.value
&& !paginationParameters.loading
&& paginationParameters.page < paginationParameters.lastPage
) {
paginationParameters.page++;
paginationParameters.loading = true;
const routeCustom = (searchParameters.selectedList?.length)
? (getRouteFilter(searchParameters.routeWords) + `?page=${paginationParameters.page}&words=${searchParameters.selectedList.join(",")}`)
: getRouteFilter(
routeOrSubRouteSearch(),
(filtersDetailsParameters.selectedChildFilter.length)
? { model_slug: filtersDetailsParameters.selectedFilter, child_slug: filtersDetailsParameters.selectedChildFilter, page: paginationParameters.page }
: (filtersDetailsParameters.selectedFilter.length)
? { model_slug: filtersDetailsParameters.selectedFilter, page: paginationParameters.page }
: { page: paginationParameters.page }
);
const url = (filtersDetailsParameters.selectedChildFilter.length)
? filtersDetailsParameters.selectedChildFilter
: ((filtersDetailsParameters.selectedFilter.length)
? filtersDetailsParameters.selectedFilter
: "");
const children = filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)].children ? true : false;
ajaxCall(routeCustom, url, children, true);
}
});
}
/**
* Get models by search words.
* @return void
*/
function getModelByText(elements: VselectOption|VselectOption[]|null) {
if (elements !== null){
filtersDetailsParameters.selectedModel = filtersDetailsParameters.selectedChildModel = null;
searchParameters.selectedList = (!Array.isArray(elements)) ? [elements] : elements;
resetList();
ajaxCall(
getRouteFilter(searchParameters.routeWords) + `?words=${searchParameters.selectedList.join(",")}`,
`${location.origin}/${filtersDetailsParameters.defaultPath}`,
);
updateTexts(searchParameters.selectedList.join(", "));
}
}
/**
* Get models by filter checked.
* @param {
title: string,
models: LaravelModelList,
route: string,
} filter
* @param LaravelModel|null model
* @param Event|null event
* @return void
*/
function getModelByFilters(filter: {
title: string,
models: LaravelModelList,
route: string,
}, model: LaravelModel|null = null, event: Event|null = null): void {
const target = getTarget(event);
searchParameters.selectedList = filtersDetailsParameters.selectedModel = filtersDetailsParameters.selectedChildModel = null;
filtersDetailsParameters.selectedFilter = filtersDetailsParameters.selectedChildFilter = "";
resetFilters(filtersList.value, target?.checked ? target?.id : null);
filtersDetailsParameters.selectedFilterId = String(getFilterId());
filtersDetailsParameters.selectedFilter = target?.checked ? slugify(String(model?.label ?? model?.title), slugifyOptions) : "";
paginationParameters.page = 1;
ajaxCall(
getRouteFilter(filter.route, target?.checked ? { model_slug: String(model?.slug), page: paginationParameters.page } : { page: paginationParameters.page }),
target?.checked ? getRouteFilter(filter.route).split("MODELSLUG")[0] + String(model?.slug) : `${location.origin}/${filtersDetailsParameters.defaultPath}`,
filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)].children ? true : false,
);
updateTexts();
}
/**
* Get models by child checked.
* @param {
models: LaravelModelList,
route: string,
} children
* @param LaravelModel|null model
* @param Event|null event
* @return void
*/
function getModelByChildren(children: {
models: LaravelModelList,
route: string,
}, model: LaravelModel|null = null, event: Event|null = null): void {
const target = getTarget(event);
searchParameters.selectedList = filtersDetailsParameters.selectedChildModel = null;
filtersDetailsParameters.selectedChildFilter = "";
resetFilters(childrenList.value, target?.checked ? target?.id : null);
filtersDetailsParameters.selectedChildFilter = target?.checked ? slugify(String(model?.label ?? model?.title), slugifyOptions) : "";
paginationParameters.page = 1;
ajaxCall(
getRouteFilter(target?.checked ? children.route : filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)].route, target?.checked ? { model_slug: filtersDetailsParameters.selectedFilter, child_slug: String(model?.slug), page: paginationParameters.page } : { model_slug: filtersDetailsParameters.selectedFilter, page: paginationParameters.page }),
target?.checked ? `${getRouteFilter(children.route).split("MODELSLUG")[0]}${filtersDetailsParameters.selectedFilter}/${String(model?.slug)}` : getRouteFilter(children.route).split("MODELSLUG")[0] + filtersDetailsParameters.selectedFilter,
);
updateTexts();
}
/**
* Ajax call template to get models views list.
* @param string customRoute
* @param string url
* @param boolean updateChildren
* @param boolean concatResult
* @return void
*/
function ajaxCall(
customRoute: string,
url: string,
updateChildren: boolean = false,
concatResult: boolean = false
): void {
let newViewsPaginate = [] as string[];
if (concatResult)
paginationParameters.loading = true;
else
loading.value = true;
window.axios
.get(customRoute)
.then((reponse) => {
newViewsPaginate = reponse.data.viewsPaginate.data;
paginationParameters.page = reponse.data.viewsPaginate.current_page;
paginationParameters.itemPerPage = reponse.data.viewsPaginate.per_page;
paginationParameters.lastPage = reponse.data.viewsPaginate.last_page;
filtersDetailsParameters.viewsPaginate = (concatResult)
? filtersDetailsParameters.viewsPaginate.concat(newViewsPaginate)
: newViewsPaginate;
if (updateChildren) {
filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)].children!.models = reponse.data.children;
}
updateURL(url);
if (!concatResult)
searchModel.value?.scrollIntoView();
})
.then(() => {
if (concatResult)
paginationParameters.loading = false;
else loading.value = false;
setTimeout(() => {
initChildrenSwiper();
mountCardComponents();
}, 10);
})
.catch(ajaxErrorHandler);
}
/**
* Get route filter.
* @param string routeName
* @param Record<string, string|number|(string|number)[]> parameters
* @return string
*/
function getRouteFilter(routeName: string, parameters: Record<string, string|number|(string|number)[]> = {}): string {
const routeFilter = route(routeName, parameters);
if (!routeFilter) {
throw new Error("Undefined route " + routeName);
}
return routeFilter;
}
/**
* Update url of the page.
* @param string newURL
* @return void
*/
function updateURL(newURL: string): void {
history.replaceState("Object", "", newURL);
}
/**
* Get checked filter id.
* @return string|void
*/
function getFilterId(): string|void {
const inputs = searchModel.value?.querySelectorAll("input[type='checkbox']") as NodeListOf<HTMLInputElement>;
for (let index = 0; index < inputs.length; index++) {
if (inputs[index].checked) {
return inputs[index].name;
}
}
}
/**
* Get filter index by id.
* @param string id
* @return number
*/
function getFilterById(id: string): number {
for (let index = 0; index < filters.value.length; index++) {
if (filters.value[index].id === id) {
return index;
}
}
return 0;
}
/**
* Get the target of a click event.
* @param event Event|null
* @return HTMLInputElement|undefined
*/
function getTarget(event: Event|null): HTMLInputElement|undefined {
let target = undefined as HTMLInputElement|undefined;
if (event && event.target) {
target = event.target as HTMLInputElement;
}
return target;
}
/**
* Reset the models list and all filters.
* @return void
*/
function resetList(getModels: boolean = false): void {
filtersDetailsParameters.selectedFilter = "";
filtersDetailsParameters.selectedChildFilter = "";
resetFilters(searchModel.value);
if (getModels)
getModelByFilters(filters.value[getFilterById(filtersDetailsParameters.selectedFilterId)]);
}
/**
* Reset all filters.
* @param HTMLDivElement nodeElement
* @param string|null inputId
* @return void
*/
function resetFilters(nodeElement: HTMLDivElement|null, inputId: string|null = null): void {
const inputs = nodeElement?.querySelectorAll("input[type='checkbox']") as NodeListOf<HTMLInputElement>;
inputs.forEach(input => {
input.checked = (inputId === input.id) ? true : false;
});
}
/**
* Mount components in render cards.
* @param string[] data
* @return void
*/
function mountCardComponents(): void {
const setupApp = (app: App<Element>) => {
app.config.globalProperties.window = window;
app.config.globalProperties.document = document;
app.component("FontAwesomeIcon", FontAwesomeIcon);
};
const buttons = document.querySelectorAll(".btn-club-favorite");
const newButtons = [].slice.call(buttons, paginationParameters.itemPerPage * (paginationParameters.page - 1)) as Array<HTMLDivElement>;
newButtons.forEach((card, cardIndex) => {
const app = createApp(BtnClubFavoriteComponent, JSON.parse(String(newButtons[cardIndex].getAttribute("data-json"))));
setupApp(app);
app.mount(newButtons[cardIndex] ?? "");
});
}
/**
* 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);
}
</script>
<style lang="scss" scopped>
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins";
@import "swiper/css";
@import "swiper/css/navigation";
.search-model-component {
scroll-margin-top: 7rem;
@include media-breakpoint-up(md) {
.offcanvas-md {
.line-border-small {
background-color: rgb(var(--bs-white-rgb)) !important;
}
}
}
.filters-list {
position: sticky;
top: 7rem;
z-index: 2;
}
.filters-list:has(.offcanvas-md.show, .offcanvas-md.showing) {
z-index: 1050;
}
.placeholder-title {
height: 1.76rem;
}
.placeholder-time {
height: 1.3rem;
}
.placeholder-tag {
height: 1.1rem;
}
.no-result-icon {
width: 4rem;
height: 4rem;
}
.form-check-label .filters-btn {
top: -.5rem;
right: -.7rem;
width: 1.2rem;
height: 1.2rem;
border-radius: 100%;
padding: .7rem;
}
button[data-bs-toggle="collapse"] span {
rotate: -90deg;
transition: .3s;
}
button[data-bs-toggle="collapse"].collapsed span {
rotate: 90deg !important;
}
.swiper-btns {
width: 2rem;
height: 2rem;
}
.swiper-btn-prev {
left: 0;
transform: translateY(-50%) rotate(180deg);
}
.swiper-btn-next {
right: 0;
}
.search-custom {
.vs__dropdown-toggle {
border: 0;
border-radius: 0;
padding: 0.75rem 1.12rem !important;
background-color: var(--bs-white) !important;
}
.vs__dropdown-menu {
background-color: var(--bs-white);
}
.vs--multiple .vs__selected {
margin: .375rem .5rem .375rem 0 !important;
}
.vs__search {
margin: 0 !important;
}
}
}
</style>