1(function ($) { 2 $.fn.galleryPAC = function () { 3 const $container = $(this); 4 5 // Conserve l'ordre d'apparition 6 const $els = $container.find('[data-src]'); 7 const items = new Array($els.length); 8 let currentIndex = 0; 9 let $modal, $main, $thumbs, $arrowPrev, $arrowNext, $closeBtn, $360Folder; 10 let isOrbitvuActive = false; 11 12 // HEAD pour récupérer le MIME. Fallback silencieux si CORS/erreur. 13 const headMime = async (url) => { 14 try { 15 const res = await fetch(url, { method: 'HEAD' }); 16 return res.headers.get('Content-Type'); 17 } catch (_) { 18 return null; 19 } 20 }; 21 22 const looksLikeVideo = (src) => 23 /\.(mp4|webm|ogg|mov|m4v)$/i.test(src) || 24 src.includes('youtube.com') || 25 src.includes('youtu.be') || 26 src.includes('vimeo.com') || 27 src.includes('dailymotion.com') || 28 src.includes('dai.ly'); 29 30 const detectType = async (src, $el) => { 31 if (looksLikeVideo(src)) return 'video'; 32 33 const mime = await headMime(src); 34 if (mime && mime.toLowerCase().includes('application/pdf')) return 'pdf'; 35 36 const declared = ($el.attr('data-type') || '').toLowerCase(); 37 if (declared === 'pdf') return 'pdf'; 38 if (/\.pdf(\?|#|$)/i.test(src)) return 'pdf'; 39 40 if (declared === '360') return '360'; 41 42 return 'image'; 43 }; 44 45 const init = async () => { 46 const tasks = []; 47 48 $els.each(function (i) { 49 const $el = $(this); 50 const src = $el.data('src'); 51 const thumb = 52 $el.find('img').attr('src') || 53 $el.find('source').attr('src') || 54 null; 55 56 const p = detectType(src, $el).then((type) => { 57 items[i] = { type, src, thumb }; 58 }); 59 tasks.push(p); 60 }); 61 62 await Promise.all(tasks); 63 bindTriggersInOrder(); 64 }; 65 66 const buildModal = () => { 67 $modal = $(` 68 <div class="gallerypac-modal"> 69 <div class="gallerypac-content"> 70 <span class="gallerypac-close">×</span> 71 <div class="gallerypac-arrow prev">❮</div> 72 <div class="gallerypac-main"></div> 73 <div class="gallerypac-arrow next">❯</div> 74 <div class="gallerypac-thumbs"></div> 75 </div> 76 </div> 77 `); 78 $('body').append($modal); 79 $main = $modal.find('.gallerypac-main'); 80 $thumbs = $modal.find('.gallerypac-thumbs'); 81 $arrowPrev = $modal.find('.gallerypac-arrow.prev'); 82 $arrowNext = $modal.find('.gallerypac-arrow.next'); 83 $closeBtn = $modal.find('.gallerypac-close'); 84 85 $modal.on('click', function (e) { 86 const $t = $(e.target); 87 const isInsideAllowed = 88 $t.closest('.gallerypac-arrow').length > 0 || 89 $t.closest('.gallerypac-thumbs .thumb').length > 0 || 90 $t.closest('.gallerypac-360-container').length > 0 || 91 $t.closest('.gallerypac-main > *').length > 0 || 92 $t.closest('.gallerypac-close').length > 0; 93 if (!isInsideAllowed) closeModal(); 94 }); 95 96 $closeBtn.on('click', () => closeModal()); 97 $arrowPrev.on('click', () => 98 showItem((currentIndex - 1 + items.length) % items.length) 99 ); 100 $arrowNext.on('click', () => 101 showItem((currentIndex + 1) % items.length) 102 ); 103 104 let startX = 0; 105 $main.on('touchstart', (e) => { 106 if (isOrbitvuActive) return; 107 if ($(e.target).closest('#picture360, .ovviosautorotate, .orbitvu').length) return; 108 startX = e.originalEvent.touches[0].clientX; 109 }); 110 $main.on('touchend', (e) => { 111 if (isOrbitvuActive) return; 112 113 if ($(e.target).closest('#picture360, .ovviosautorotate, .orbitvu').length) return; 114 115 const endX = e.originalEvent.changedTouches[0].clientX; 116 if (endX - startX > 50) 117 showItem((currentIndex - 1 + items.length) % items.length); 118 else if (startX - endX > 50) 119 showItem((currentIndex + 1) % items.length); 120 }); 121 122 $(document).on('touchmove', '.gallerypac-360-container', function (e) { 123 e.stopPropagation(); 124 }); 125 }; 126 127 const renderItem = (item) => { 128 if (item.type === 'video') { 129 if (item.src.includes('youtube.com') || item.src.includes('youtu.be')) { 130 let videoId = ''; 131 const regexList = [ 132 /\/embed\/([a-zA-Z0-9_-]+)/, 133 /watch\?v=([a-zA-Z0-9_-]+)/, 134 /shorts\/([a-zA-Z0-9_-]+)/, 135 /youtu\.be\/([a-zA-Z0-9_-]+)/ 136 ]; 137 for (const r of regexList) { 138 const m = item.src.match(r); 139 if (m && m[1]) { videoId = m[1]; break; } 140 } 141 if (videoId) { 142 const autoplayParam = 143 (typeof window.mobileCheck === 'function' && window.mobileCheck()) ? '' : 'auto
143play=1&'; 144 return ` 145 <div class="gallerypac-video-container"> 146 <div class="gallerypac-video-subcontainer"> 147 <iframe 148 src="https://www.youtube-nocookie.com/embed/${videoId}?${autoplayParam}rel=0&controls=1&enablejsapi=1&fs=1&playsinline=1" 149 frameborder="0" 150 allow="autoplay; encrypted-media; fullscreen; picture-in-picture" 151 allowfullscreen 152 loading="eager" 153 ></iframe> 154 </div></div>`; 155 } 156 } 157 if (item.src.includes('vimeo.com')) { 158 const vimeoId = item.src.split('/').pop(); 159 return ` 160 <div class="gallerypac-video-container"> 161 <div class="gallerypac-video-subcontainer"> 162 <iframe 163 src="https://player.vimeo.com/video/${vimeoId}?autoplay=1" 164 frameborder="0" 165 allow="autoplay; fullscreen" 166 allowfullscreen 167 ></iframe> 168 </div></div>`; 169 } 170 return `<video controls autoplay src="${item.src}" class="gallerypac-fullmedia"></video>`; 171 } 172 173 if (item.type === 'pdf') { 174 return ` 175 <div class="gallerypac-pdf-container"> 176 <iframe 177 src="${item.src}#toolbar=0&navpanes=0&scrollbar=0" 178 frameborder="0" 179 class="gallerypac-pdf-frame" 180 allowfullscreen 181 ></iframe> 182 </div>`; 183 } 184 185 if (item.type === '360') { 186 return ` 187 <div class="gallerypac-360-container"> 188 <div id="picture360"></div> 189 </div>`; 190 } 191 192 return `<img src="${item.src}" class="gallerypac-fullmedia" />`; 193 }; 194 195 const showItem = (index) => { 196 currentIndex = index; 197 isOrbitvuActive = false; 198 $main.empty(); 199 setTimeout(() => { 200 $main.html(renderItem(items[index])); 201 202 // INIT ORBITVU 203 if (items[index].type === '360') { 204 isOrbitvuActive = true; 205 inject_orbitvu( 206 'picture360', 207 '/templates/template2017/js/plugins/orbitvu/orbitvu.js', 208 '', 209 { 210 ovus_folder: items[index].src, 211 width: "100%", 212 content2: "yes", 213 height: "100%", 214 partial_load: "yes", 215 auto_rotate_dir: "right", 216 rotate_dir: "right", 217 teaser: "onerotationslow", 218 disable_nosize: "yes", 219 dynamic_avif_webp_ext: "yes" 220 } 221 ); 222 } 223 224 $thumbs.find('.thumb').removeClass('active'); 225 $thumbs.find(`[data-index="${index}"]`).addClass('active'); 226 227 // Hook générique : permet à un consommateur d'injecter un overlay 228 // (ex: bouton "Acheter le matériel") en lisant le DOM source via index. 229 $container.trigger('gallerypac:show', [index, items[index], $modal]); 230 }, 10); 231 }; 232 233 const closeModal = () => { 234 $modal.fadeOut(200, () => { 235 $modal.remove(); 236 $('body').css('overflow', ''); 237 }); 238 }; 239 240 const openModal = (index) => { 241 if (!$('.gallerypac-modal').length) buildModal(); 242 else $modal.fadeIn(); 243 244 $('body').css('overflow', 'hidden'); 245 $main.empty(); 246 $thumbs.empty(); 247 248 const hasThumbs = items.some(it => !!it.thumb); 249 const $content = $modal.find('.gallerypac-content'); 250 251 // Cacher flèches si pas de thumbs ou qu'un seul élément 252 if (!hasThumbs || items.length <= 1) { 253 $arrowPrev.hide(); 254 $arrowNext.hide(); 255 } else { 256 $arrowPrev.show(); 257 $arrowNext.show(); 258 } 259 260 if (!hasThumbs) { 261 $thumbs.hide(); 262 $content.removeClass('gallerypac-has-thumbs-right'); 263 } else { 264 if (window.innerWidth > 768) $content.addClass('gallerypac-has-thumbs-right'); 265 else $content.removeClass('gallerypac-has-thumbs-right'); 266 267 items.forEach((item, i) => { 268 if (item.thumb) { 269 let $t; 270 271 if (item.type === '360') { 272 // $t = $(` 273 // <div class="thumb thumb-360" data-index="${i}"> 274 // <img class="thumb" src="${item.thumb}" /> 275 // </div> 276 // `); 277 $t = $(` 278 <img class="thumb" src="/images/360.png" data-index="${i}" /> 279 `); 280 } else { 281 $t = $(` 282 <img class="thumb" src="${item.thumb}" data-index="${i}" /> 283 `); 284 } 285 286 if (i === index) $t.addClass('active'); 287 $t.on('click', () => showItem(i)); 288 $thumbs.append($t); 289 } 290 }); 291 $thumbs.show(); 292 } 293 294 showItem(index); 295 $modal.fadeIn(); 296 }; 297 298 const bindTriggersInOrder = () => { 299 // Si le container porte data-manual-bind, on n'attache PAS le clic 300 // automatique. Le consommateur (ex: products.js) déclenche la modale 301 // via $element.trigger('manualBindGallery') sur l'item de son choix. 302 const manualBind = !!$container.data('manualBind'); 303 304 $els.each(function (i) { 305 const $el = $(this); 306 307 // Déclencheur custom toujours présent (utilisable même en mode auto) 308 $el.off('manualBindGallery.gallerypac') 309 .on('manualBindGallery.gallerypac', () => openModal(i)); 310 311 if (!manualBind) 312 $el.css('cursor', 'pointer') 313 .off('click.gallerypac') 314 .on('click.gallerypac', () => openModal(i)); 315 }); 316 }; 317 318 init(); 319 }; 320 321 $('.galleryPAC').each(function () { 322 $(this).galleryPAC(); 323 }); 324})(jQuery);
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.