From 1dc5ceb2416e8a7d7fd5e51cbc72df26ebde4ee2 Mon Sep 17 00:00:00 2001 From: Isabelle Date: Fri, 29 May 2026 12:04:54 +0200 Subject: [PATCH] fix(extra-features): Fixed the sync product image/gallery --- .../product-gallery/_product-thumbs.scss | 43 ++-- .../js/product/components/ProductGallery.js | 223 +++++++++++------- .../components/product-miniature-form.js | 175 ++++++++++++++ falcon/_dev/js/theme/index.js | 17 +- 4 files changed, 336 insertions(+), 122 deletions(-) create mode 100644 falcon/_dev/js/theme/components/product-miniature-form.js diff --git a/falcon/_dev/css/product/components/product-gallery/_product-thumbs.scss b/falcon/_dev/css/product/components/product-gallery/_product-thumbs.scss index 216cde1..7553cb4 100644 --- a/falcon/_dev/css/product/components/product-gallery/_product-thumbs.scss +++ b/falcon/_dev/css/product/components/product-gallery/_product-thumbs.scss @@ -1,27 +1,34 @@ @use "sass:map"; .product-thumbs { - margin-right: -#{map.get($spacers, 1)}; - margin-left: -#{map.get($spacers, 1)}; + margin-right: -#{map.get($spacers, 1)}; + margin-left: -#{map.get($spacers, 1)}; - &__elem { - padding: 0 map.get($spacers, 1); - cursor: pointer; - opacity: .3; - transition: .3s ease opacity; - @include make-col(4); - - &:focus, - &:hover { - opacity: 0.6; + &.swiper-vertical { + max-height: 600px; + } + &.swiper-horizontal { + max-height: 200px; } - &.swiper-slide-thumb-active { - opacity: 1; - } + &__elem { + padding: 0 map.get($spacers, 1); + cursor: pointer; + opacity: 0.3; + transition: 0.3s ease opacity; + @include make-col(4); - @include media-breakpoint-up(sm) { - @include make-col(3); + &:focus, + &:hover { + opacity: 0.6; + } + + &.swiper-slide-thumb-active { + opacity: 1; + } + + @include media-breakpoint-up(sm) { + @include make-col(3); + } } - } } diff --git a/falcon/_dev/js/product/components/ProductGallery.js b/falcon/_dev/js/product/components/ProductGallery.js index 61f50b0..cade7de 100644 --- a/falcon/_dev/js/product/components/ProductGallery.js +++ b/falcon/_dev/js/product/components/ProductGallery.js @@ -1,101 +1,148 @@ class ProductGallery { - constructor({ - thumbsSliderSelector = '.js-product-thumbs', - mainSliderSelector = '.js-product-main-images', - modalSliderSelector = '.js-modal-gallery', - galleryModalSelector = '.js-product-images-modal', - } = {}) { - this.thumbsSliderSelector = thumbsSliderSelector; - this.mainSliderSelector = mainSliderSelector; - this.modalSliderSelector = modalSliderSelector; - this.galleryModalSelector = galleryModalSelector; - this.mainSliderSwiperInstance = null; - this.modalSliderSwiperInstance = null; - } - - init() { - this.mainSliderSwiperInstance = null; - this.modalSliderSwiperInstance = null; - this.initProductImageSlider(); - this.initModalGallerySlider(); - } - - async initProductImageSlider() { - const thumbsElem = document.querySelector(this.thumbsSliderSelector); - const galleryTopElem = document.querySelector(this.mainSliderSelector); - - if (!thumbsElem && !galleryTopElem) { - return; + constructor({ + thumbsSliderSelector = ".js-product-thumbs", + mainSliderSelector = ".js-product-main-images", + modalSliderSelector = ".js-modal-gallery", + galleryModalSelector = ".js-product-images-modal", + } = {}) { + this.thumbsSliderSelector = thumbsSliderSelector; + this.mainSliderSelector = mainSliderSelector; + this.modalSliderSelector = modalSliderSelector; + this.galleryModalSelector = galleryModalSelector; + this.mainSliderSwiperInstance = null; + this.modalSliderSwiperInstance = null; } - const galleryThumbs = new prestashop.SwiperSlider(thumbsElem, { - breakpoints: { - 320: { - slidesPerView: 3, - }, - 576: { - slidesPerView: 4, - }, - }, - watchSlidesVisibility: true, - watchSlidesProgress: true, - }); - - const galleryThumbsInstance = await galleryThumbs.initSlider(); - - const mainSlider = new prestashop.SwiperSlider(galleryTopElem, { - spaceBetween: 10, - navigation: { - nextEl: galleryTopElem.querySelector('.swiper-button-next'), - prevEl: galleryTopElem.querySelector('.swiper-button-prev'), - }, - thumbs: { - swiper: galleryThumbsInstance, - }, - }); - - const mainSliderInstance = await mainSlider.initSlider(); - - this.mainSliderSwiperInstance = mainSliderInstance; - } - - initModalGallerySlider() { - const gallerySliderElem = document.querySelector(this.modalSliderSelector); - - if (!gallerySliderElem) { - return; + init() { + this.mainSliderSwiperInstance = null; + this.modalSliderSwiperInstance = null; + this.initProductImageSlider(); + this.initModalGallerySlider(); } - const handleModalOpen = async () => { - if (this.modalSliderSwiperInstance) { - gallerySliderElem.style.opacity = 0; + syncThumbsHeight() { + const mainImages = document.querySelector(".product-main-images"); + const thumbs = document.querySelector(this.thumbsSliderSelector); + if (!mainImages || !thumbs) return; - // DIRTY HACK - setTimeout(() => { - this.modalSliderSwiperInstance.update(); - this.modalSliderSwiperInstance.slideTo(this.mainSliderSwiperInstance ? this.mainSliderSwiperInstance.activeIndex : 0, 0); - gallerySliderElem.style.opacity = 1; - }, 200); - } else { - const modalSlider = new prestashop.SwiperSlider(gallerySliderElem, { - slidesPerView: 1, - spaceBetween: 10, - initialSlide: this.mainSliderSwiperInstance ? this.mainSliderSwiperInstance.activeIndex : 0, - navigation: { - nextEl: gallerySliderElem.querySelector('.swiper-button-next'), - prevEl: gallerySliderElem.querySelector('.swiper-button-prev'), - }, + if (thumbs.classList.contains("swiper-vertical")) { + const h = mainImages.offsetHeight; + if (h > 0) { + thumbs.style.height = h + "px"; + } + } else { + thumbs.style.height = ""; + } + } + + async initProductImageSlider() { + const thumbsElem = document.querySelector(this.thumbsSliderSelector); + const galleryTopElem = document.querySelector(this.mainSliderSelector); + + if (!thumbsElem && !galleryTopElem) { + return; + } + + const galleryThumbs = new prestashop.SwiperSlider(thumbsElem, { + direction: "horizontal", // default + breakpoints: { + 0: { + direction: "horizontal", + slidesPerView: 3, + }, + 768: { + direction: "vertical", + slidesPerView: 5, + }, + 992: { + direction: "horizontal", + slidesPerView: 3, + }, + 1200: { + direction: "vertical", + slidesPerView: 5, + }, + }, + watchSlidesVisibility: true, + watchSlidesProgress: true, + spaceBetween: 10, }); - const modalSliderInstance = await modalSlider.initSlider(); + const galleryThumbsInstance = await galleryThumbs.initSlider(); - this.modalSliderSwiperInstance = modalSliderInstance; - } - }; + galleryThumbsInstance.on("resize", () => this.syncThumbsHeight()); + galleryThumbsInstance.on("breakpoint", () => this.syncThumbsHeight()); - // TO REFACTO LATER WITH BS5 REMOVE JQUERY! - $(this.galleryModalSelector).on('show.bs.modal', handleModalOpen); - } + const mainSlider = new prestashop.SwiperSlider(galleryTopElem, { + spaceBetween: 10, + navigation: { + nextEl: galleryTopElem.querySelector(".swiper-button-next"), + prevEl: galleryTopElem.querySelector(".swiper-button-prev"), + }, + thumbs: { + swiper: galleryThumbsInstance, + }, + }); + + const mainSliderInstance = await mainSlider.initSlider(); + + this.mainSliderSwiperInstance = mainSliderInstance; + this.syncThumbsHeight(); + } + + initModalGallerySlider() { + const gallerySliderElem = document.querySelector( + this.modalSliderSelector, + ); + + if (!gallerySliderElem) { + return; + } + + const handleModalOpen = async () => { + if (this.modalSliderSwiperInstance) { + gallerySliderElem.style.opacity = 0; + + // DIRTY HACK + setTimeout(() => { + this.modalSliderSwiperInstance.update(); + this.modalSliderSwiperInstance.slideTo( + this.mainSliderSwiperInstance + ? this.mainSliderSwiperInstance.activeIndex + : 0, + 0, + ); + gallerySliderElem.style.opacity = 1; + }, 200); + } else { + const modalSlider = new prestashop.SwiperSlider( + gallerySliderElem, + { + slidesPerView: 1, + spaceBetween: 10, + initialSlide: this.mainSliderSwiperInstance + ? this.mainSliderSwiperInstance.activeIndex + : 0, + navigation: { + nextEl: gallerySliderElem.querySelector( + ".swiper-button-next", + ), + prevEl: gallerySliderElem.querySelector( + ".swiper-button-prev", + ), + }, + }, + ); + + const modalSliderInstance = await modalSlider.initSlider(); + + this.modalSliderSwiperInstance = modalSliderInstance; + } + }; + + // TO REFACTO LATER WITH BS5 REMOVE JQUERY! + $(this.galleryModalSelector).on("show.bs.modal", handleModalOpen); + } } export default ProductGallery; diff --git a/falcon/_dev/js/theme/components/product-miniature-form.js b/falcon/_dev/js/theme/components/product-miniature-form.js new file mode 100644 index 0000000..86659d8 --- /dev/null +++ b/falcon/_dev/js/theme/components/product-miniature-form.js @@ -0,0 +1,175 @@ +//Add functionality to the custom form in product miniatures + +import useAlertToast from "@js/theme/components/useAlertToast"; + +document.addEventListener("DOMContentLoaded", function () { + const toast = useAlertToast(); + + const parsePositiveInteger = (value, fallback = 1) => { + const parsedValue = parseInt(value, 10); + return Number.isFinite(parsedValue) && parsedValue > 0 + ? parsedValue + : fallback; + }; + + const isInvalidStepMultiple = (rawValue, minQuantity, stepQuantity) => { + const parsedValue = parseInt(rawValue, 10); + + if (!Number.isFinite(parsedValue) || parsedValue <= 0) { + return false; + } + + return (parsedValue - minQuantity) % stepQuantity !== 0; + }; + + const normalizeQuantityByStep = (rawValue, minQuantity, stepQuantity) => { + const parsedValue = parseInt(rawValue, 10); + const safeValue = Number.isFinite(parsedValue) + ? Math.max(parsedValue, minQuantity) + : minQuantity; + + return ( + minQuantity + + Math.ceil((safeValue - minQuantity) / stepQuantity) * stepQuantity + ); + }; + + const getPackWarningMessage = (input, stepQuantity) => { + const messageTemplate = input.getAttribute( + "data-pack-message-template", + ); + + if (!messageTemplate) { + return ""; + } + + return messageTemplate.replace("__PACK__", stepQuantity); + }; + + let $forms = document.querySelectorAll(".product-miniature__form"); + + $forms.forEach(($form) => { + let $quantityInput = $form.querySelector("input[name='qty-field']"); + let $quantityHidden = $form.querySelector("input[name='qty']"); + let $incrementBtn = $form.querySelector( + "button[name='qty-field-plus']", + ); + let $decrementBtn = $form.querySelector( + "button[name='qty-field-minus']", + ); + + if ( + !$quantityInput || + !$quantityHidden || + !$incrementBtn || + !$decrementBtn + ) { + return; + } + + const minQuantity = parsePositiveInteger( + $quantityInput.getAttribute("min"), + parsePositiveInteger($quantityHidden.value, 1), + ); + const stepQuantity = parsePositiveInteger( + $quantityInput.getAttribute("step"), + minQuantity, + ); + + $quantityInput.setAttribute("min", minQuantity); + $quantityInput.setAttribute("step", stepQuantity); + + function updateHiddenQuantity() { + const normalizedQuantity = normalizeQuantityByStep( + $quantityInput.value, + minQuantity, + stepQuantity, + ); + + $quantityHidden.value = normalizedQuantity; + } + + function normalizeTypedQuantity(showToast = false) { + const enteredQuantity = $quantityInput.value; + const normalizedQuantity = normalizeQuantityByStep( + enteredQuantity, + minQuantity, + stepQuantity, + ); + + if ( + showToast && + isInvalidStepMultiple( + enteredQuantity, + minQuantity, + stepQuantity, + ) + ) { + const warningMessage = getPackWarningMessage( + $quantityInput, + stepQuantity, + ); + + if (warningMessage) { + toast.warning(warningMessage); + } + } + + if (parseInt($quantityInput.value, 10) !== normalizedQuantity) { + $quantityInput.value = normalizedQuantity; + } + + $quantityHidden.value = normalizedQuantity; + } + + $quantityInput.addEventListener("input", function () { + if ($quantityInput.value === "") { + $quantityHidden.value = minQuantity; + return; + } + + updateHiddenQuantity(); + }); + + $incrementBtn.addEventListener("click", function (event) { + event.preventDefault(); + + const currentValue = normalizeQuantityByStep( + $quantityInput.value, + minQuantity, + stepQuantity, + ); + + $quantityInput.value = currentValue + stepQuantity; + updateHiddenQuantity(); + }); + + $decrementBtn.addEventListener("click", function (event) { + event.preventDefault(); + + const currentValue = normalizeQuantityByStep( + $quantityInput.value, + minQuantity, + stepQuantity, + ); + + $quantityInput.value = Math.max( + minQuantity, + currentValue - stepQuantity, + ); + updateHiddenQuantity(); + }); + + $quantityInput.addEventListener("focusout", function () { + normalizeTypedQuantity(true); + }); + + $quantityInput.addEventListener("keyup", function (event) { + if (event.keyCode === 13) { + normalizeTypedQuantity(true); + } + }); + + normalizeTypedQuantity(false); + }); +}); diff --git a/falcon/_dev/js/theme/index.js b/falcon/_dev/js/theme/index.js index 5cd4d84..a1319c9 100644 --- a/falcon/_dev/js/theme/index.js +++ b/falcon/_dev/js/theme/index.js @@ -13,6 +13,7 @@ import "@js/theme/components/quickview"; import "@js/theme/components/product"; import "@js/theme/components/cart/cart"; import "@js/theme/components/cart/block-cart"; +import "@js/theme/components/product-miniature-form.js"; import usePasswordPolicy from "@js/theme/components/usePasswordPolicy"; import prestashop from "prestashop"; @@ -66,22 +67,6 @@ function initStickyHeader() { } } -// Sync product thumbs height with main images height -function syncThumbsHeight() { - const mainImages = document.querySelector(".product-main-images"); - const thumbs = document.querySelector(".product-thumbs.swiper"); - // Only apply if Swiper is vertical - if (mainImages && thumbs && thumbs.classList.contains("swiper-vertical")) { - thumbs.style.maxHeight = mainImages.offsetHeight + "px"; - thumbs.style.height = mainImages.offsetHeight + "px"; - } else { - return; - } -} - -window.addEventListener("load", syncThumbsHeight); -window.addEventListener("resize", syncThumbsHeight); - $(() => { initStickyHeader(); accLinksTriggerActive();