1/** 2 * Library 360: https://github.com/mladenilic/threesixty.js 3 * Generate your sprite here: https://jcthepants.github.io/Retina-CSS-Sprite-Generator/ (50 per row horizontal, save the retina version make two images) -> https://www.toptal.com/developers/css/sprite-generator (then assemble the two images with this) 4 */ 5 6class SingleProductSandre { 7 constructor() { 8 // Ne pas exécuter dans le back-office WordPress 9 if (document.body.classList.contains("wp-admin")) { 10 return; 11 } 12 13 this.layout = document.querySelector(".hero-single-product"); 14 if (this.layout) { 15 document.addEventListener("DOMContentLoaded", () => this.init()); 16 } 17 } 18 19 init() { 20 // Initialize 3D viewer if enabled 21 const threeSixtyElement = document.getElementById("threesixty"); 22 if (threeSixtyElement) { 23 const sprite = this.layout.getAttribute("data-sprite"); 24 const imageSize = parseInt(this.layout.getAttribute("data-3d-image-size"), 10); 25 const numberOfImagesPerRow = parseInt( 26 this.layout.getAttribute("data-3d-nb-images-per-row"), 27 10, 28 ); 29 const numberOfImagesUsed = parseInt(this.layout.getAttribute("data-3d-nb-images"), 10); 30 31 //https://github.com/mladenilic/threesixty.js 32 //Generate your sprite here: https://jcthepants.github.io/Retina-CSS-Sprite-Generator/ -> https://www.toptal.com/developers/css/sprite-generator 33 // A la génération on utilise backgroundSize = (100 à perRow) + "% " + (100 à Math.ceil(count / perRow)) + "%" 34 // Donc pour 50 images par ligne et 100 images au total : backgroundSize = "5000% 200%" 35 const threesixty = new ThreeSixty(threeSixtyElement, { 36 image: sprite, 37 width: imageSize, 38 height: imageSize, 39 count: numberOfImagesUsed, 40 perRow: numberOfImagesPerRow, 41 swipeTarget: document.getElementById("36-wrapper"), 42 prev: document.getElementById("36-prev"), 43 next: document.getElementById("36-next"), 44 speed: 50, 45 }); 46 47 threesixty.play(false, 2); 48 49 // On hover stop animation 50 const wrapper36 = document.getElementById("36-wrapper"); 51 wrapper36.addEventListener("mouseenter", () => threesixty.stop()); 52 wrapper36.addEventListener("mouseleave", () => threesixty.play(false, 1)); 53 } 54 55 // Initialize Splide slider 56 const splideWrapper = document.querySelector(".hero-single-product .splide"); 57 if (!splideWrapper) { 58 return; 59 } 60 61 // Get attributes 62 const autoplay = splideWrapper.dataset.autoplay === "true"; 63 const interval = parseInt(splideWrapper.dataset.interval, 10) || 3000; 64 const perPage = parseInt(splideWrapper.dataset.perPage, 10) || 4; 65 66 var main = new Splide(".hero-single-product .main-hero-single-product", { 67 type: "fade", 68 rewind: true, 69 pagination: false, 70 arrows: false, 71 breakpoints: { 72 600: { 73 pagination: true, 74 }, 75 }, 76 }); 77 78 var thumbnails = new Splide(".hero-single-product .slider-hero-product", { 79 fixedWidth: 100, 80 fixedHeight: 60, 81 gap: 10, 82 rewind: true, 83 pagination: false, 84 arrows: false, 85 focus: "center", 86 isNavigation: true, 87 breakpoints: { 88 600: { 89 fixedWidth: 60, 90 fixedHeight: 44, 91 }, 92 }, 93 }); 94 main.sync(thumbnails); 95 main.mount(); 96 thumbnails.mount(); 97 98 // Lightbox sur l'image active du slider principal (les vidéos sont exclues 99 // car elles ne sont pas wrappées dans un <a class="glightbox">). 100 if (typeof GLightbox === "function") { 101 GLightbox({ 102 selector: ".hero-single-product .main-hero-single-product .glightbox", 103 touchNavigation: true, 104 loop: true, 105 }); 106 } 107 108 // Function to handle video autoplay 109 const handleVideoAutoplay = () => { 110 // Stop all videos first 111 const allVideos = document.querySelectorAll(".hero-single-product lite-youtube"); 112 allVideos.forEach((video) => { 113 // Remove iframe if exists to stop the video 114 const iframe = video.querySelector("iframe"); 115 if (iframe) { 116 iframe.remove(); 117 // Reset the lite-youtube component state 118 video.classList.remove("lyt-activated"); 119 } 120 }); 121 122 // Get the active slide 123 const activeSlide = document.querySelector( 124 ".hero-single-product .main-hero-single-product .splide__slide.is-active", 125 ); 126 127 if (activeSlide) { 128 const liteYoutube = activeSlide.querySelector("lite-youtube"); 129 if (liteYoutube) { 130 // Trigger the lite-youtube to load and play 131 setTimeout(() => { 132 const playButton = liteYoutube.querySelector(".lty-playbtn"); 133 if (playButton) { 134 playButton.click(); 135 } 136 }, 300); // Small delay to ensure slide transition is complete 137 } 138 } 139 }; 140 141 // Auto-play video on slide change 142 main.on("moved", handleVideoAutoplay); 143 144 // Handle initial slide if it's a video 145 main.on("mounted", () => { 146 setTimeout(() => { 147 handleVideoAutoplay(); 148 }, 100); 149 }); 150 151 // Initialize modals 152 this.initModals(); 153 } 154 155 initModals() { 156 // Get all modal triggers 157 const modalTriggers = document.querySelectorAll("[data-modal-target]"); 158
159 modalTriggers.forEach((trigger) => { 160 trigger.addEventListener("click", (e) => { 161 const modalId = trigger.getAttribute("data-modal-target"); 162 const tabId = trigger.getAttribute("data-tab"); 163 const host = window.location.hostname; 164 const site = 'https://' + host + '/clic-download-' + tabId; 165 _paq.push(['trackLink', site, 'link']); 166 this.openModal(modalId, tabId); 167 }); 168 169 }); 170 171 // Get all modal close buttons 172 const modalCloseButtons = document.querySelectorAll("[data-modal-close]"); 173 174 modalCloseButtons.forEach((button) => { 175 button.addEventListener("click", (e) => { 176 const modalId = button.getAttribute("data-modal-close"); 177 this.closeModal(modalId); 178 }); 179 }); 180 181 // Get all tab buttons 182 const tabButtons = document.querySelectorAll("[data-tab-button]"); 183 184 tabButtons.forEach((button) => { 185 button.addEventListener("click", (e) => { 186 const tabId = button.getAttribute("data-tab-button"); 187 this.switchTab(tabId); 188 }); 189 }); 190 191 // Close modal on backdrop click 192 const modals = document.querySelectorAll('[id^="modal-"]'); 193 194 modals.forEach((modal) => { 195 modal.addEventListener("click", (e) => { 196 if (e.target === modal) { 197 this.closeModal(modal.id); 198 } 199 }); 200 }); 201 202 // Close modal on Escape key 203 document.addEventListener("keydown", (e) => { 204 if (e.key === "Escape") { 205 const openModal = document.querySelector('[id^="modal-"]:not(.hidden)'); 206 if (openModal) { 207 this.closeModal(openModal.id); 208 } 209 } 210 }); 211 } 212 213 switchTab(tabId) { 214 const modal = document.querySelector('[id^="modal-"]:not(.hidden)'); 215 if (!modal) return; 216 217 // Hide all tab contents and footers 218 const allContents = modal.querySelectorAll(".tab-content-modal"); 219 const allFooters = modal.querySelectorAll(".tab-footer"); 220 const allButtons = modal.querySelectorAll(".tab-button"); 221 222 allContents.forEach((content) => content.classList.add("hidden")); 223 allFooters.forEach((footer) => footer.classList.add("hidden")); 224 allButtons.forEach((button) => { 225 button.classList.add("text-secondary-500"); 226 button.classList.remove("bg-primary-500", "text-white"); 227 }); 228 229 // Show selected tab content and footer 230 const selectedContent = modal.querySelector(`[data-tab-content-modal="${tabId}"]`); 231 const selectedFooter = modal.querySelector(`[data-tab-footer="${tabId}"]`); 232 const selectedButton = modal.querySelector(`[data-tab-button="${tabId}"]`); 233 234 if (selectedContent) selectedContent.classList.remove("hidden"); 235 if (selectedFooter) selectedFooter.classList.remove("hidden"); 236 if (selectedButton) { 237 selectedButton.classList.add("bg-primary-500", "text-white"); 238 selectedButton.classList.remove("text-secondary-500"); 239 } 240 } 241 242 openModal(modalId, tabId = null) { 243 const modal = document.getElementById(modalId); 244 if (modal) { 245 const modalPanel = modal.querySelector("div"); 246 247 // Show modal 248 modal.classList.remove("hidden"); 249 250 // Trigger animation on next frame 251 requestAnimationFrame(() => { 252 modal.classList.remove("bg-opacity-0"); 253 modal.classList.add("bg-opacity-50"); 254 255 if (modalPanel) { 256 modalPanel.classList.remove("translate-x-full"); 257 modalPanel.classList.add("translate-x-0"); 258 } 259 }); 260 261 // Switch to the specified tab or first tab 262 if (tabId) { 263 this.switchTab(tabId); 264 } else { 265 // Default to first tab 266 const firstTabButton = modal.querySelector("[data-tab-button]"); 267 if (firstTabButton) { 268 const firstTabId = firstTabButton.getAttribute("data-tab-button"); 269 this.switchTab(firstTabId); 270 } 271 } 272 273 document.body.style.overflow = "hidden"; 274 } 275 } 276 277 closeModal(modalId) { 278 const modal = document.getElementById(modalId); 279 if (modal) { 280 const modalPanel = modal.querySelector("div"); 281 282 // Animate out 283 modal.classList.add("bg-opacity-0"); 284 modal.classList.remove("bg-opacity-50"); 285 286 if (modalPanel) { 287 modalPanel.classList.add("translate-x-full"); 288 modalPanel.classList.remove("translate-x-0"); 289 } 290 291 // Hide after animation completes 292 setTimeout(() => { 293 modal.classList.add("hidden"); 294 }, 300); 295 296 document.body.style.overflow = ""; 297 } 298 } 299} 300 301new SingleProductSandre();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.