1/** 2 * LorShop Theme JavaScript 3 * Mobile menu, search, account dropdown, categories mega menu, hero carousel, 4 * product tabs, cart sidebar, quantity controls (single + card grid) 5 * 6 * @package LorShop 7 */ 8 9(function() { 10 'use strict'; 11 12 function init() { 13 14 // ================================================ 15 // Mobile Menu 16 // ================================================ 17 var mobileMenuToggle = document.getElementById('mobile-menu-toggle'); 18 var mobileMenu = document.getElementById('mobile-menu'); 19 20 if (mobileMenuToggle && mobileMenu) { 21 mobileMenuToggle.addEventListener('click', function() { 22 mobileMenu.classList.toggle('hidden'); 23 }); 24 } 25 26 // ================================================ 27 // Mobile Search 28 // ================================================ 29 var mobileSearchToggle = document.getElementById('mobile-search-toggle'); 30 var mobileSearch = document.getElementById('mobile-search'); 31 32 if (mobileSearchToggle && mobileSearch) { 33 mobileSearchToggle.addEventListener('click', function() { 34 mobileSearch.classList.toggle('hidden'); 35 if (!mobileSearch.classList.contains('hidden')) { 36 var input = mobileSearch.querySelector('input'); 37 if (input) input.focus(); 38 } 39 }); 40 } 41 42 // ================================================ 43 // Account Dropdown 44 // ================================================ 45 var accountToggle = document.getElementById('account-toggle'); 46 var accountMenu = document.getElementById('account-menu'); 47 48 if (accountToggle && accountMenu) { 49 accountToggle.addEventListener('click', function(e) { 50 e.stopPropagation(); 51 accountMenu.classList.toggle('hidden'); 52 }); 53 54 document.addEventListener('click', function(e) { 55 if (!accountMenu.contains(e.target) && !accountToggle.contains(e.target)) { 56 accountMenu.classList.add('hidden'); 57 } 58 }); 59 } 60 61 // ================================================ 62 // Categories Mega Menu 63 // ================================================ 64 var categoriesToggle = document.getElementById('categories-toggle'); 65 var categoriesDropdown = document.getElementById('categories-dropdown'); 66 67 if (categoriesToggle && categoriesDropdown) { 68 categoriesToggle.addEventListener('click', function(e) { 69 e.stopPropagation(); 70 categoriesDropdown.classList.toggle('hidden'); 71 }); 72 73 document.addEventListener('click', function(e) { 74 if (!categoriesDropdown.contains(e.target) && !categoriesToggle.contains(e.target)) { 75 categoriesDropdown.classList.add('hidden'); 76 } 77 }); 78 } 79 80 // ================================================ 81 // Global WooCommerce Notices (floating + auto-hide) 82 // ================================================ 83 var globalNotices = document.getElementById('lorshop-global-notices'); 84 if (globalNotices) { 85 var noticeItems = globalNotices.querySelectorAll('.woocommerce-message, .woocommerce-info, .woocommerce-error'); 86 87 function dismissNotice(item) { 88 if (!item || item.classList.contains('lorshop-notice-hide')) return; 89 item.classList.add('lorshop-notice-hide'); 90 setTimeout(function() { 91 item.remove(); 92 if (!globalNotices.querySelector('.woocommerce-message, .woocommerce-info, .woocommerce-error')) { 93 globalNotices.remove(); 94 } 95 }, 260); 96 } 97 98 noticeItems.forEach(function(item) { 99 var closeBtn = document.createElement('button'); 100 closeBtn.type = 'button'; 101 closeBtn.className = 'lorshop-notice-close'; 102 closeBtn.setAttribute('aria-label', 'Hinweis schlieÃen'); 103 closeBtn.textContent = 'Ã'; 104 closeBtn.addEventListener('click', function() { 105 dismissNotice(item); 106 }); 107 item.appendChild(closeBtn); 108 109 var timeout = setTimeout(function() {
110 dismissNotice(item); 111 }, 6500); 112 113 item.addEventListener('mouseenter', function() { 114 clearTimeout(timeout); 115 }); 116 117 item.addEventListener('mouseleave', function() { 118 timeout = setTimeout(function() { 119 dismissNotice(item); 120 }, 2500); 121 }); 122 }); 123 } 124 125 // ================================================ 126 // Hero Carousel (image-only, progress-bar dots, scale transitions) 127 // ================================================ 128 var carousel = document.getElementById('hero-carousel'); 129 if (carousel) { 130 var slides = carousel.querySelectorAll('.carousel-slide'); 131 var dots = carousel.querySelectorAll('.carousel-dot'); 132 var prevBtn = carousel.querySelector('.carousel-prev'); 133 var nextBtn = carousel.querySelector('.carousel-next'); 134 var currentSlide = 0; 135 var autoplayTimer; 136 var interval = parseInt(carousel.getAttribute('data-interval')) || 6000; 137 138 function goToSlide(index) { 139 // Update slides (scale + opacity transition) 140 slides.forEach(function(slide, i) { 141 if (i === index) { 142 slide.classList.remove('opacity-0', 'scale-[1.08]'); 143 slide.classList.add('opacity-100', 'scale-100'); 144 } else { 145 slide.classList.remove('opacity-100', 'scale-100'); 146 slide.classList.add('opacity-0', 'scale-[1.08]'); 147 } 148 }); 149 150 // Update progress-bar dots 151 dots.forEach(function(dot, i) { 152 var progressBar = dot.querySelector('.carousel-progress'); 153 154 if (i === index) { 155 dot.classList.add('active'); 156 dot.style.width = '40px'; 157 158 // Create or restart progress bar 159 if (!progressBar) { 160 progressBar = document.createElement('div'); 161 progressBar.className = 'carousel-progress absolute inset-0 rounded-full'; 162 progressBar.style.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--lorshop-primary').trim() || '#e30513'; 163 dot.appendChild(progressBar); 164 } 165 // Restart animation
166 progressBar.style.animation = 'none'; 167 // Force reflow 168 void progressBar.offsetWidth; 169 progressBar.style.animation = 'progress-bar ' + interval + 'ms linear'; 170 } else { 171 dot.classList.remove('active'); 172 dot.style.width = '12px'; 173 if (progressBar) { 174 progressBar.style.animation = 'none'; 175 } 176 } 177 }); 178 179 currentSlide = index; 180 } 181 182 function nextSlide() { 183 goToSlide((currentSlide + 1) % slides.length); 184 } 185 186 function prevSlide() { 187 goToSlide((currentSlide - 1 + slides.length) % slides.length); 188 } 189 190 function startAutoplay() { 191 stopAutoplay(); 192 autoplayTimer = setInterval(nextSlide, interval); 193 } 194 195 function stopAutoplay() { 196 if (autoplayTimer) { 197 clearInterval(autoplayTimer); 198 autoplayTimer = null; 199 } 200 } 201 202 if (prevBtn) prevBtn.addEventListener('click', function() { stopAutoplay(); prevSlide(); startAutoplay(); }); 203 if (nextBtn) nextBtn.addEventListener('click', function() { stopAutoplay(); nextSlide(); startAutoplay(); }); 204 205 dots.forEach(function(dot) { 206 dot.addEventListener('click', function() { 207 stopAutoplay(); 208 goToSlide(parseInt(this.getAttribute('data-goto'))); 209 startAutoplay(); 210 }); 211 }); 212 213 // Only autoplay if more than 1 slide 214 if (slides.length > 1) { 215 startAutoplay(); 216 } 217 218 // Pause on hover 219 carousel.addEventListener('mouseenter', stopAutoplay); 220 carousel.addEventListener('mouseleave', function() { 221 if (slides.length > 1) startAutoplay(); 222 }); 223 } 224 225 // ================================================ 226 // Product Image Switcher 227 // ================================================ 228 window.switchImage = function(thumb) { 229 var mainImage = document.getElementById('product-main-image'); 230 if (!mainImage || !thumb) return; 231 232 var newSrc = thumb.getAttribute('data-image'); 233 mainImage.src = newSrc; 234 235 // Update active thumbnail border 236 document.querySelectorAll('.product-thumb').forEach(function(t) { 237 t.classList.remove('border-blue-500', 'active'); 238 t.classList.add('border-gray-200'); 239 }); 240 thumb.classList.remove('border-gray-200'); 241 thumb.classList.add('border-blue-500', 'active'); 242 }; 243 244 // ================================================ 245 // Product Tabs 246 // ================================================ 247 var tabBtns = document.querySelectorAll('.product-tab-btn'); 248 var tabContents = document.querySelectorAll('.product-tab-content'); 249 250 tabBtns.forEach(function(btn) { 251 btn.addEventListener('click', function() { 252 var tab = this.getAttribute('data-tab'); 253 var primary = getComputedStyle(document.documentElement).getPropertyValue('--lorshop-primary').trim(); 254 255 // Update buttons 256 tabBtns.forEach(function(b) { 257 b.classList.remove('active'); 258 b.style.borderColor = 'transparent'; 259 b.style.color = '#6b7280'; 260 }); 261 this.classList.add('active'); 262 this.style.borderColor = primary; 263 this.style.color = primary; 264 265 // Update content 266 tabContents.forEach(function(content) { 267 if (content.getAttribute('data-tab') === tab) { 268 content.classList.remove('hidden'); 269 } else { 270 content.classList.add('hidden'); 271 } 272 }); 273 }); 274 }); 275 276 // ================================================ 277 // Quantity Controls â Single Product Page 278 // ================================================ 279 var qtyMinus = document.querySelector('.qty-minus'); 280 var qtyPlus = document.querySelector('.qty-plus'); 281 var qtyInput = document.getElementById('product-qty'); 282 283 if (qtyMinus && qtyPlus && qtyInput) { 284 qtyMinus.addEventListener('click', function() { 285 var val = parseInt(qtyInput.value) || 1; 286 if (val > 1) qtyInput.value = val - 1; 287 }); 288 qtyPlus.addEventListener('click', function() { 289 var val = parseInt(qtyInput.value) || 1; 290 var max = parseInt(qtyInput.getAttribute('max')); 291 if (!max || val < max) qtyInput.value = val + 1; 292 }); 293 } 294 295 // ================================================ 296 // Quantity Controls â Product Card Grid 297 // ================================================ 298 document.querySelectorAll('.qty-minus[data-product]').forEach(function(btn
298) { 299 btn.addEventListener('click', function(e) { 300 e.preventDefault(); 301 e.stopPropagation(); 302 var productId = this.getAttribute('data-product'); 303 var display = document.querySelector('.qty-display[data-product="' + productId + '"]'); 304 if (!display) return; 305 var val = parseInt(display.textContent) || 1; 306 if (val > 1) display.textContent = val - 1; 307 }); 308 }); 309 310 document.querySelectorAll('.qty-plus[data-product]').forEach(function(btn) { 311 btn.addEventListener('click', function(e) { 312 e.preventDefault(); 313 e.stopPropagation(); 314 var productId = this.getAttribute('data-product'); 315 var display = document.querySelector('.qty-display[data-product="' + productId + '"]'); 316 if (!display) return; 317 var val = parseInt(display.textContent) || 1; 318 display.textContent = val + 1; 319 }); 320 }); 321 322 // ================================================ 323 // Floating toast (shared with WooCommerce notice styling) 324 // ================================================ 325 function lorshopShowToast(message, type) { 326 var container = document.getElementById('lorshop-global-notices'); 327 if (!container) { 328 container = document.createElement('div'); 329 container.id = 'lorshop-global-notices'; 330 container.className = 'lorshop-global-notices'; 331 container.setAttribute('aria-live', 'polite'); 332 container.setAttribute('aria-atomic', 'true'); 333 container.appendChild(Object.assign(document.createElement('div'), { className: 'woocommerce-notices-wrapper' })); 334 document.body.appendChild(container); 335 } 336 var wrapper = container.querySelector('.woocommerce-notices-wrapper') || container; 337 338 var notice = document.createElement('div'); 339 notice.className = 'woocommerce-' + (type || 'message'); 340 notice.setAttribute('role', 'alert'); 341 notice.textContent = message; 342 wrapper.appendChild(notice); 343 344 var dismiss = function() { 345 if (notice.classList.contains('lorshop-notice-hide')) return; 346 notice.classList.add('lorshop-notice-hide'); 347 setTimeout(function() { 348 notice.remove(); 349 if (!container.querySelector('.woocommerce-message, .woocommerce-info, .woocommerce-error')) container.remove(); 350 }, 260); 351 }; 352 var closeBtn = document.createElement('button'); 353 closeBtn.type = 'button'; 354 closeBtn.className = 'lorshop-notice-close'; 355 closeBtn.setAttribute('aria-label', 'Hinweis schlieÃen'); 356 closeBtn.textContent = 'Ã'; 357 closeBtn.addEventListener('click', dismiss); 358 notice.appendChild(closeBtn); 359 360 var timeout = setTimeout(dismiss, 6500); 361 notice.addEventListener('mouseenter', function() { clearTimeout(timeout); }); 362 notice.addEventListener('mouseleave', function() { timeout = setTimeout(dismiss, 2500); }); 363 } 364 window.lorshopShowToast = lorshopShowToast; 365 366 // ================================================ 367 // Add-to-cart on card grid (AJAX + loading state) 368 // ================================================ 369 var spinnerSvg = '<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>'; 370 var checkSvg = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>'; 371 372 document.querySelectorAll('.add-to-cart-btn[data-product-id]').forEach(function(btn) { 373 btn.addEventListener('click', function(e) { 374 e.preventDefault(); 375 if (btn.getAttribute('data-loading') === '1') return; 376 377 var productId = btn.getAttribute('data-product-id'); 378 var productName = btn.getAttribute('data-product-name') || ''; 379 var display = document.querySelector('.qty-display[data-product="' + productId + '"]'); 380 var qty = display ? parseInt(display.textContent) || 1 : 1; 381 var originalHtml = btn.innerHTML; 382 383 // Loading state inside the button. 384 btn.setAttribute('data-loading', '1'); 385 btn.style.pointerEvents = 'none'; 386 btn.style.opacity = '0.85'; 387 btn.innerHTML = spinnerSvg + '<span>Hinzufügenâ¦</span>'; 388 389 var body = new URLSearchParams(); 390 body.append('product_id', productId); 391 body.append('quantity', qty); 392 393 fetch('/?wc-ajax=add_to_cart', { 394 method: 'POST', 395 credentials: 'same-origin', 396 headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' }, 397 body: body.toString() 398 }) 399 .then(function(r) { return r.json(); }) 400 .then(function(data) { 401 // Variation required / other server-side redirect. 402 if (data && data.error && data.product_url) { 403 window.location.href = data.product_url; 404 return; 405 } 406 407 // Apply WooCommerce fragments (mini-cart sidebar, header count, â¦). 408 if (data && data.fragments) {
409 Object.keys(data.fragments).forEach(function(sel) { 410 document.querySelectorAll(sel).forEach(function(el) { 411 var tmp = document.createElement('div'); 412 tmp.innerHTML = data.fragments[sel]; 413 if (tmp.firstElementChild) el.replaceWith(tmp.firstElementChild); 414 }); 415 }); 416 document.body.dispatchEvent(new CustomEvent('wc_fragments_refreshed')); 417 } 418 419 // Success feedback: button check + floating toast. 420 btn.innerHTML = checkSvg + '<span>Hinzugefügt</span>'; 421 lorshopShowToast((productName ? productName + ' ' : '') + 'wurde in den Warenkorb gelegt.', 'message'); 422 423 setTimeout(function() { 424 btn.innerHTML = originalHtml; 425 btn.removeAttribute('data-loading'); 426 btn.style.pointerEvents = ''; 427 btn.style.opacity = ''; 428 }, 1600); 429 }) 430 .catch(function() { 431 // Network/parse failure â fall back to the standard add-to-cart URL. 432 window.location.href = btn.href; 433 }); 434 }); 435 }); 436 437 // ================================================ 438 // Cart Sidebar 439 // ================================================ 440 window.toggleCartSidebar = function() { 441 var sidebar = document.getElementById('cart-sidebar'); 442 if (!sidebar) return; 443 444 if (sidebar.classList.contains('open')) { 445 sidebar.classList.remove('open'); 446 document.body.style.overflow = ''; 447 setTimeout(function() { sidebar.classList.add('hidden'); }, 300); 448 } else { 449 sidebar.classList.remove('hidden'); 450 // Force reflow 451 sidebar.offsetHeight; 452 sidebar.classList.add('open'); 453 document.body.style.overflow = 'hidden'; 454 } 455 }; 456 457 var cartBtn = document.getElementById('header-cart-btn'); 458 if (cartBtn) { 459 cartBtn.addEventListener('click', function(e) { 460 e.preventDefault(); 461 window.toggleCartSidebar(); 462 }); 463 } 464 465 // ================================================ 466 // Close dropdowns/menus on Escape 467 // ================================================ 468 document.addEventListener('keydown', function(e) { 469 if (e.key === 'Escape') { 470 // Close account dropdown 471 if (accountMenu && !accountMenu.classList.contains('hidden')) { 472 accountMenu.classList.add('hidden'); 473 } 474 // Close categories dropdown 475 if (categoriesDropdown && !categoriesDropdown.classList.contains('hidden')) { 476 categoriesDropdown.classList.add('hidden'); 477 } 478 // Close mobile menu 479 if (mobileMenu && !mobileMenu.classList.contains('hidden')) { 480 mobileMenu.classList.add('hidden'); 481 } 482 // Close cart sidebar 483 var sidebar = document.getElementById('cart-sidebar'); 484 if (sidebar && sidebar.classList.contains('open')) { 485 window.toggleCartSidebar(); 486 } 487 } 488 }); 489 490 // ================================================ 491 // Scroll Reveal (IntersectionObserver) 492 // ================================================ 493 if ('IntersectionObserver' in window && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 494 var revealObserver = new IntersectionObserver(function(entries) { 495 entries.forEach(function(entry) { 496 if (entry.isIntersecting) { 497 entry.target.classList.add('is-visible'); 498 revealObserver.unobserve(entry.target); 499 } 500 }); 501 }, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }); 502 503 document.querySelectorAll('.ls-reveal').forEach(function(el) { 504 revealObserver.observe(el); 505 }); 506 } else { 507 document.querySelectorAll('.ls-reveal').forEach(function(el) { 508 el.classList.add('is-visible'); 509 }); 510 } 511 512 // ================================================ 513 // Header scroll effect (subtle shadow) 514 // ================================================ 515 var header = document.getElementById('site-header'); 516 if (header) { 517 window.addEventListener('scroll', function() { 518 var scrollY = window.pageYOffset || document.documentElement.scrollTop; 519 if (scrollY > 10) { 520 header.style.boxShadow = '0 4px 20px rgba(0,0,0,0.15)'; 521 } else { 522 header.style.boxShadow = ''; 523 } 524 }, { passive: true }); 525 } 526 527 } 528 529 // Run when DOM is ready 530 if (document.readyState === 'loading') { 531 document.addEventListener('DOMContentLoaded', init); 532 } else { 533 init(); 534 } 535 536})();
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.