1// modal module 2 3 4export function modal(images, isGallery = false, initialIndex = 0, type) { 5 const container = document.getElementById('modal'); 6 const main_header = document.getElementById('main_header'); 7 8 if (!container) return; 9 10 container.classList.add('active'); 11 main_header.classList.add('none'); 12 13 14 document.body.classList.add('modal_on'); 15 locked(); 16 17 18 gsap.to(container, { opacity: 1, duration: 0.35, ease: 'power2.out' }); 19 20 if (type === 'image') { 21 renderImageModal(images, isGallery, initialIndex); 22 } else { 23 renderVideoModal(images, initialIndex); 24 } 25 26 } 27 28 29 30 31 32 33 34// =============================== 35// IMAGE 36// =============================== 37 38 39 40 41 42 43 44 45 // function renderImageModal(images, isGallery = false, initialIndex = 0) { 46 // const container = document.getElementById('modal'); 47 // const modalImg = container.querySelector('.main-image img'); 48 // const thumbnails = container.querySelector('.thumbnails'); 49 // const altBox = container.querySelector('.main-image .alt-text'); 50 // const countBox = thumbnails.querySelector('.thumb-count'); 51 52 // const expandedView = container.querySelector('.expanded-view'); 53 // const expandedImg = container.querySelector('.expanded-view img'); 54 55 // const closeExpanded = container.querySelector('.close-expanded'); 56 // const expandBtn = container.querySelector('.expand-image'); 57 // const closeBtn = container.querySelector('.close-modal'); 58 // const downBtn = container.querySelector('.download-image'); 59 60 61 // if (countBox) { 62 // thumbnails.innerHTML = ''; 63 // thumbnails.appendChild(countBox); 64 // } 65 66 // if (thumbnails) { 67 // thumbnails.innerHTML = ''; 68 // if (countBox) thumbnails.appendChild(countBox); 69 // } 70 71 // container.classList.add('image-modal'); 72 // container.classList.toggle('gallery', isGallery); 73 74 // let currentIndex = initialIndex; 75 76 // updateImage({ 77 // images, 78 // index: currentIndex, 79 // modalImg, 80 // altBox, 81 // expandedImg, 82 // thumbnails, 83 // countBox 84 // }); 85 86 // if (isGallery) { 87 // renderThumbnails(images, currentIndex, thumbnails, newIndex => { 88 // currentIndex = newIndex; 89 // updateImage({ 90 // images, 91 // index: currentIndex, 92 // modalImg, 93 // altBox, 94 // expandedImg, 95 // thumbnails, 96 // countBox 97 // }); 98 // }); 99 100 // setupNavigation(container, images, () => { 101 // currentIndex = (currentIndex + 1) % images.length; 102 // updateImage({ 103 // images, 104 // index: currentIndex, 105 // modalImg, 106 // altBox, 107 // expandedImg, 108 // thumbnails, 109 // countBox 110 // }); 111 // }, () => { 112 // currentIndex = (currentIndex - 1 + images.length) % images.length; 113 // updateImage({ 114 // images, 115 // index: currentIndex, 116 // modalImg, 117 // altBox, 118 // expandedImg, 119 // thumbnails, 120 // countBox 121 // }); 122 // }); 123 // } 124 125 // expandBtn.onclick = () => { 126 // expandedImg.src = modalImg.src; 127 // expandedImg.alt = modalImg.alt; 128 129 // gsap.set(expandedView, { opacity: 0 }); 130 // expandedView.classList.add('active'); 131 132 // gsap.to(expandedView, { 133 // opacity: 1, 134 // duration: 0.35, 135 // ease: 'power2.out' 136 // }); 137 // }; 138 139 // closeExpanded.onclick = () => { 140 // gsap.to(expandedView, { 141 // opacity: 0, 142 // duration: 0.3, 143 // ease: 'power2.inOut', 144 // onComplete: () => { 145 // expandedView.classList.remove('active'); 146 // } 147 // }); 148 // }; 149 150 // closeBtn.onclick = () => closeModal('image'); 151 152 // gsap.to('.main-image', { opacity: 1, duration: 0.3, ease: 'power1.out' }); 153 // if (isGallery) { 154 // gsap.to('.thumbnails', { opacity: 1, duration: 0.3, ease: 'power1.out' }); 155 // } 156 // } 157 158 159 160 161 162 // function renderImageModal(images, isGallery = false, initialIndex = 0) { 163 // const container = document.getElementById('modal'); 164 // const modalImg = container.querySelector('.main-image img'); 165 // const thumbnails = container.querySelector('.thumbnails'); 166 // const altBox = container.querySelector('.main-image .alt-text'); 167 // const countBox = thumbnails.querySelector('.thumb-count'); 168 169 // const expandedView = container.querySelector('.expanded-view'); 170 // const expandedImg = container.querySelector('.expanded-view img'); 171 172 // const closeExpanded = container.querySelector('.close-expanded'); 173 // const expandBtn = container.querySelector('.expand-image'); 174 // const closeBtn = container.querySelector('.close-modal'); 175 // const downBtn = container.querySelector('.download-image'); 176 177 178 // if (countBox) { 179 // thumbnails.innerHTML = ''; 180 // thumbnails.appendChild(countBox); 181 // } 182 183 // if (thumbnails) { 184 // thumbnails.innerHTML = ''; 185 // if (countBox) thumbnails.appendChild(countBox); 186 // } 187 188 // container.classList.add('image-modal'); 189 // container.classList.toggle('gallery', isGallery); 190 191 // let currentIndex = initialIndex; 192 193 // updateImage({ 194 // images, 195 // index: currentIndex, 196 // modalImg, 197 // altBox, 198 // expandedImg, 199 // thumbnails, 200 // countBox 201 // }); 202 203 // if (isGallery) { 204 // renderThumbnails(images, currentIndex, thumbnails, newIndex => { 205 // currentIndex = newIndex; 206 // updateImage({ 207 // images, 208 // index: currentIndex, 209 // modalImg, 210 // altBox, 211 // expandedImg, 212 // thumbnails, 213 // countBox 214 // }); 215 // }); 216 217 // setupNavigation(container, images, () => { 218 // currentIndex = (currentIndex + 1) % images.length; 219 // updateImage({ 220 // images, 221 // index: currentIndex, 222 // modalImg, 223 // altBox, 224 // expandedImg, 225 // thumbnails, 226 // countBox 227 // }); 228 // }, () => { 229 // currentIndex = (currentIndex - 1 + images.length) % images.length; 230 // updateImage({ 231 // images, 232 // index: currentIndex, 233 // modalImg, 234 // altBox, 235 // expandedImg, 236 // thumbnails, 237 // countBox 238 // }); 239 // }); 240 // } 241 242 // expandBtn.onclick = () => { 243 // expandedImg.src = modalImg.src; 244 // expandedImg.alt = modalImg.alt; 245 246 // gsap.set(expandedView, { opacity: 0 }); 247 // expandedView.classList.add('active'); 248 249 // gsap.to(expandedView, { 250 // opacity: 1, 251 // duration: 0.35, 252 // ease: 'power2.out' 253 // }); 254 // }; 255 256 // closeExpanded.onclick = () => { 257 // gsap.to(expandedView, { 258 // opacity: 0, 259 // duration: 0.3, 260 // ease: 'power2.inOut', 261 // onComplete: () => { 262 // expandedView.classList.remove('active'); 263 // } 264 // }); 265 // }; 266 267 // closeBtn.onclick = () => closeModal('image'); 268 269 // gsap.to('.main-image', { opacity: 1, duration: 0.3, ease: 'power1.out' }); 270 // if (isGallery) { 271 // gsap.to('.thumbnails', { opacity: 1, duration: 0.3, ease: 'power1.out' }); 272 // } 273 // } 274 275 276 277 function renderImageModal(images, isGallery = false, initialIndex = 0) { 278 // 1) Deduplicate images by URL 279 const seen = new Set(); 280 const uniqueImages = images.filter(img => { 281 if (seen.has(img.display)) return false;
282 seen.add(img.display); 283 return true; 284 }); 285 286 // 2) Remap initialIndex into the deduped array 287 const requestedURL = images[initialIndex]?.display; 288 let currentIndex = uniqueImages.findIndex(img => img.display === requestedURL); 289 if (currentIndex < 0) currentIndex = 0; 290 291 // 3) Grab DOM references 292 const container = document.getElementById('modal'); 293 const modalImg = container.querySelector('.main-image img'); 294 const thumbnails = container.querySelector('.thumbnails'); 295 const altBox = container.querySelector('.main-image .alt-text'); 296 const titleBox = container.querySelector('.main-image .title-text'); 297 const countBox = thumbnails.querySelector('.thumb-count'); 298 const expandedView= container.querySelector('.expanded-view'); 299 const expandedImg = expandedView.querySelector('img'); 300 const closeExpanded = container.querySelector('.close-expanded'); 301 const expandBtn = container.querySelector('.expand-image'); 302 const closeBtn = container.querySelector('.close-modal'); 303 304 // 4) Clear out any old thumbnails 305 if (thumbnails) { 306 thumbnails.innerHTML = ''; 307 if (countBox) thumbnails.appendChild(countBox); 308 } 309 310 container.classList.add('image-modal'); 311 container.classList.toggle('gallery', isGallery); 312 313 // 5) Initial render 314 updateImage({ 315 images: uniqueImages, 316 index: currentIndex, 317 modalImg, 318 altBox, 319 titleBox, // â pass it in 320 expandedImg, 321 thumbnails, 322 countBox 323 }); 324 325 // 6) If gallery, build thumbs & nav 326 if (isGallery) { 327 renderThumbnails(uniqueImages, currentIndex, thumbnails, newIndex => { 328 currentIndex = newIndex; 329 updateImage({ 330 images: uniqueImages, 331 index: currentIndex, 332 modalImg, 333 altBox, 334 titleBox, 335 expandedImg, 336 thumbnails, 337 countBox 338 }); 339 }); 340 341 setupNavigation(container, uniqueImages, 342 // next 343 () => { 344 currentIndex = (currentIndex + 1) % uniqueImages.length; 345 updateImage({ 346 images: uniqueImages, 347 index: currentIndex, 348 modalImg, 349 altBox, 350 expandedImg, 351 thumbnails, 352 countBox 353 }); 354 }, 355 // prev 356 () => { 357 currentIndex = (currentIndex - 1 + uniqueImages.length) % uniqueImages.length; 358 updateImage({ 359 images: uniqueImages, 360 index: currentIndex, 361 modalImg, 362 altBox, 363 expandedImg, 364 thumbnails, 365 countBox 366 }); 367 } 368 ); 369 } 370 371 // 7) Expandedâview handlers 372 expandBtn.onclick = () => { 373 expandedImg.src = modalImg.src; 374 expandedImg.alt = modalImg.alt; 375 gsap.set(expandedView, { opacity: 0 }); 376 expandedView.classList.add('active'); 377 gsap.to(expandedView, { opacity: 1, duration: 0.35, ease: 'power2.out' }); 378 }; 379 closeExpanded.onclick = () => { 380 gsap.to(expandedView, { 381 opacity: 0, 382 duration: 0.3, 383 ease: 'power2.inOut', 384 onComplete: () => { 385 expandedView.classList.remove('active'); 386 } 387 }); 388 }; 389 390 // 8) Close modal 391 closeBtn.onclick = () => closeModal('image'); 392 393 // 9) Fade in 394 gsap.to('.main-image', { opacity: 1, duration: 0.3, ease: 'power1.out' }); 395 if (isGallery) { 396 gsap.to('.thumbnails', { opacity: 1, duration: 0.3, ease: 'power1.out' }); 397 } 398 } 399 400 401 402 403 404 // function updateImage({ images, index, modalImg, altBox, titleBox, expandedImg, thumbnails, countBox }) { 405 // const imgObj = images[index]; 406 407 // // âââ NEW: title support âââ 408 // if (titleBox) { 409 // titleBox.textContent = imgObj.title || ''; 410 // titleBox.classList.toggle('on', !!imgObj.title); 411 // } 412 413 // // fade out/in main image 414 // gsap.to(modalImg, { 415 // opacity: 0, 416 // duration: 0.2, 417 // ease: 'power1.out', 418 // onComplete: () => { 419 // modalImg.src = imgObj.display; 420 // modalImg.alt = imgObj.alt || ''; 421 // gsap.to(modalImg, { opacity: 1, duration: 0.3, ease: 'power2.out' }); 422 // } 423 // }); 424 425 // // alt text as before 426 // altBox.textContent = imgObj.alt || ''; 427 // altBox.classList.toggle('on', !!imgObj.alt); 428 429 // // expanded view 430 // expandedImg.src = imgObj.display; 431 // expandedImg.alt = imgObj.alt || ''; 432 433 // // thumbnails highlighting (unchanged) 434 // if (thumbnails) { 435 // const thumbs = thumbnails.querySelectorAll('img');
436 // thumbs.forEach(thumb => { 437 // const thumbIndex = +thumb.dataset.index; 438 // const isActive = thumbIndex === index; 439 // gsap.to(thumb, { 440 // borderColor: isActive ? '#fff' : 'transparent', 441 // opacity: isActive ? 1 : 0.6, 442 // duration: 0.3, 443 // ease: isActive ? 'power2.out' : 'power2.inOut' 444 // }); 445 // thumb.classList.toggle('active', isActive); 446 // }); 447 // } 448 449 // // count 450 // if (countBox) { 451 // countBox.textContent = `${index + 1} / ${images.length}`; 452 // } 453 // } 454 455 456 457 function updateImage({ images, index, modalImg, altBox, expandedImg, thumbnails, countBox }) { 458 const imgObj = images[index]; 459 460 // 1) Build the altâtext HTML 461 let html = ''; 462 if (imgObj.title) { 463 html += `<h3>${imgObj.title}</h3>`; 464 } 465 html += `<p>${imgObj.alt || ''}</p>`; 466 467 // 2) Write it and toggle the .on class 468 altBox.innerHTML = html; 469 altBox.classList.toggle('on', !!(imgObj.title || imgObj.alt)); 470 471 // 3) Main image fadeâout/in 472 gsap.to(modalImg, { 473 opacity: 0, 474 duration: 0.2, 475 ease: 'power1.out', 476 onComplete: () => { 477 modalImg.src = imgObj.display; 478 modalImg.alt = imgObj.alt; 479 gsap.to(modalImg, { opacity: 1, duration: 0.3, ease: 'power2.out' }); 480 } 481 }); 482 483 // 4) Expanded view 484 expandedImg.src = imgObj.display; 485 expandedImg.alt = imgObj.alt; 486 487 // 5) Highlight thumbnails 488 if (thumbnails) { 489 thumbnails.querySelectorAll('img').forEach(thumb => { 490 const isActive = +thumb.dataset.index === index; 491 gsap.to(thumb, { 492 borderColor: isActive ? '#fff' : 'transparent', 493 opacity: isActive ? 1 : 0.6, 494 duration: 0.3, 495 ease: isActive ? 'power2.out' : 'power2.inOut' 496 }); 497 thumb.classList.toggle('active', isActive); 498 }); 499 } 500 501 // 6) Update count 502 if (countBox) { 503 countBox.textContent = `${index + 1} / ${images.length}`; 504 } 505 } 506 507 508// =============================== 509// Carousel-Based Thumbnail Module (Balanced Wrapping) 510// =============================== 511 512function renderThumbnails(images, currentIndex, thumbnails, onThumbClick) { 513 // remove old carousel 514 const old = thumbnails.querySelector('.thumb-carousel'); 515 if (old) thumbnails.removeChild(old); 516 517 // create new 518 const inner = document.createElement('div'); 519 inner.className = 'thumb-carousel'; 520 thumbnails.appendChild(inner); 521 522 // calculate the âaroundâ indexes 523 const total = images.length; 524 const half = Math.floor(total / 2); 525 const order = []; 526 for (let i = -half; i <= half; i++) { 527 order.push((currentIndex + i + total) % total); 528 } 529 530 // build them, skipping any URL weâve already emitted 531 const seenUrls = new Set(); 532 order.forEach(idx => { 533 const obj = images[idx]; 534 const url = obj.display; 535 if (seenUrls.has(url)) { 536 console.log(`Skipping duplicate URL at index ${idx}:`, url); 537 return; 538 } 539 seenUrls.add(url); 540 541 const thumb = document.createElement('img'); 542 thumb.src = url; 543 thumb.alt = obj.alt || ''; 544 thumb.dataset.index = idx; 545 if (idx === currentIndex) thumb.classList.add('active'); 546 thumb.onclick = () => onThumbClick(idx); 547 inner.appendChild(thumb); 548 }); 549 550 // (you can still keep your small-by-index dedupe if you really need both guards) 551} 552 553 554 555 556 function shiftThumbnails(wrapper, direction = 'next') { 557 const thumbs = wrapper.querySelectorAll('img'); 558 if (thumbs.length === 0) return; 559 560 if (direction === 'next') { 561 const first = thumbs[0]; 562 wrapper.removeChild(first); 563 wrapper.appendChild(first); 564 } else { 565 const last = thumbs[thumbs.length - 1]; 566 wrapper.removeChild(last); 567 wrapper.insertBefore(last, thumbs[0]); 568 } 569 570 const active = wrapper.querySelector('img.active'); 571 if (!active) return; 572 const newIndex = Array.from(wrapper.children).indexOf(active); 573 //centerActiveThumbnail(wrapper, newIndex); 574 } 575 576 function setupNavigation(container, images, onNext, onPrev) { 577 const nextBtn = container.querySelector('.nav.next'); 578 const prevBtn = container.querySelector('.nav.prev'); 579 const wrapper = container.querySelector('.thumb-carousel'); 580 581 if (nextBtn) nextBtn.onclick = () => { 582 shiftThumbnails(wrapper, 'next'); 583 onNext(); 584 }; 585 586 if (prevBtn) prevBtn.onclick = () => { 587 shiftThumbnails(wrapper, 'prev'); 588 onPrev(); 589 }; 590 } 591 592
593 594 595 596// =============================== 597// VIDEO 598// =============================== 599 600 601 602 603 604 605export function renderVideoModal(videos = [], initialIndex = 0) { 606 const container = document.getElementById('modal'); 607 const videoWrap = container.querySelector('.type_video'); 608 const nativeVideo = videoWrap.querySelector('video'); 609 const iframe = videoWrap.querySelector('iframe'); 610 const closeBtn = container.querySelector('.close-modal'); 611 612 // Reset styles and hide both video types 613 nativeVideo.style.display = 'none'; 614 iframe.style.display = 'none'; 615 616 const data = videos[initialIndex]; 617 console.log('[VIDEO MODAL]', data); 618 const isYouTube = !!data.youtube; 619 620 if (isYouTube) { 621 const baseSrc = data.youtube.split('?')[0]; 622 const yid = baseSrc.split('/').pop(); 623 iframe.src = `${baseSrc}?autoplay=1&loop=1&rel=0&modestbranding=1&disablekb=1&playsinline=1&playlist=${yid}`; 624 iframe.style.display = 'block'; 625 } else { 626 nativeVideo.poster = data.poster || ''; 627 nativeVideo.preload = data.preload || 'auto'; 628 629 const sources = nativeVideo.querySelectorAll('source'); 630 sources[0].src = data.source1 || ''; 631 sources[0].type = data.mime1 || 'video/webm'; 632 633 if (sources[1]) { 634 sources[1].src = data.source2 || ''; 635 sources[1].type = data.mime2 || 'video/mp4'; 636 } 637 638 nativeVideo.load(); 639 nativeVideo.style.display = 'block'; 640 } 641 642 // Set visibility for the video section 643 container.classList.add('video-modal'); 644 container.classList.remove('image-modal'); 645 646 647 // Bind close button 648 closeBtn.onclick = () => closeModal('video'); 649 650 // Fade in the video wrapper (optional) 651 gsap.to(videoWrap, { opacity: 1, duration: 0.3, ease: 'power1.out' }); 652 } 653 654 655 656 657 658 659 660 661 662// =============================== 663// CLOSE 664// =============================== 665 666 667//pause_product_carousel() 668 669function closeModal(type) { 670 const container = document.getElementById('modal'); 671 const modalInner = container.querySelector('.modal-inner'); 672 const main_header = document.getElementById('main_header'); 673 const closeBtn = container.querySelector('.close-modal'); 674 675 676 console.log('$stop_carousel value:', $stop_carousel); 677 if ($stop_carousel) { 678 console.log('â Carousel is stopped'); 679 pause_product_carousel(); 680 } else { 681 console.log('â Carousel is running'); 682 } 683 684 685 if (!container || !modalInner) return; 686 687 688 if (type === 'image') { 689 690 691 // Start opacity fade on .modal-inner 692 gsap.to(modalInner, { 693 opacity: 0, 694 duration: 0.2, 695 ease: 'power2.inOut', 696 onComplete: () => { 697 698 const expandedView = container.querySelector('.expanded-view'); 699 // Remove event handlers 700 701 const nextBtn = container.querySelector('.nav.next'); 702 const prevBtn = container.querySelector('.nav.prev'); 703 704 if (closeBtn) closeBtn.onclick = null; 705 if (nextBtn) nextBtn.onclick = null; 706 if (prevBtn) prevBtn.onclick = null; 707 708 709 // Clear thumbnails and preserve countBox 710 const thumbs = container.querySelector('.thumbnails'); 711 if (thumbs) { 712 const countBox = thumbs.querySelector('.thumb-count'); 713 thumbs.innerHTML = ''; 714 if (countBox) thumbs.appendChild(countBox); 715 } 716 717 // Clear modal image 718 const modalImg = container.querySelector('.main-image img'); 719 if (modalImg) { 720 modalImg.src = ''; 721 modalImg.alt = ''; 722 } 723 724 // Clear alt text 725 const altBox = container.querySelector('.main-image .alt-text'); 726 if (altBox) { 727 altBox.textContent = ''; 728 altBox.classList.remove('on'); 729 } 730 731 main_header.classList.remove('none'); 732 733 // Then fade the container 734 gsap.to(container, { 735 opacity: 0, 736 duration: 0.3, 737 ease: 'power2.inOut', 738 onComplete: () => { 739 container.classList.remove('active', 'gallery', 'image-modal'); 740 741 // Remove all inline styles inside the modal 742 const allDescendants = container.querySelectorAll('*');
743 allDescendants.forEach(el => el.removeAttribute('style')); 744 container.removeAttribute('style'); 745 746 // Cleanup expanded view 747 if (expandedView) { 748 expandedView.classList.remove('active'); 749 expandedView.removeAttribute('style'); 750 } 751 752 753 document.body.classList.remove('modal_on'); 754 unlock(); 755 } 756 }); 757 } 758 }); 759 760 761 } else { 762 763 764 const videoWrap = container.querySelector('.type_video'); 765 const nativeVideo = videoWrap.querySelector('video'); 766 const iframe = videoWrap.querySelector('iframe'); 767 768 // Fade out the video wrapper first 769 gsap.to(videoWrap, { 770 opacity: 0, 771 duration: 0.2, 772 ease: 'power2.inOut', 773 onComplete: () => { 774 775 // Stop and reset native video 776 if (nativeVideo) { 777 nativeVideo.pause(); 778 nativeVideo.removeAttribute('poster'); 779 nativeVideo.removeAttribute('preload'); 780 const sources = nativeVideo.querySelectorAll('source'); 781 sources.forEach(source => { 782 source.removeAttribute('src'); 783 source.removeAttribute('type'); 784 }); 785 nativeVideo.load(); 786 } 787 788 // Clear iframe src 789 if (iframe) { 790 iframe.removeAttribute('src'); 791 } 792 793 // Remove inline styles and reset 794 const allDescendants = container.querySelectorAll('*');
795 allDescendants.forEach(el => el.removeAttribute('style')); 796 container.removeAttribute('style'); 797 798 // Restore main header 799 main_header.classList.remove('none'); 800 801 // Fade out and fully clean container 802 gsap.to(container, { 803 opacity: 0, 804 duration: 0.3, 805 ease: 'power2.inOut', 806 onComplete: () => { 807 808 document.body.classList.remove('modal_on'); 809 unlock(); 810 811 container.classList.remove('active', 'video-modal'); 812 if (closeBtn) closeBtn.onclick = null; 813 } 814 }); 815 } 816 }); 817 818 819 } 820 } 821 822
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.