1/** 2 * PolyCMS Corporate Theme JavaScript 3 * 4 * Professional business theme functionality 5 */ 6 7(function() { 8 'use strict'; 9 10 // Mobile Menu Toggle 11 const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); 12 const mobileMenu = document.getElementById('mobile-menu'); 13 14 if (mobileMenuToggle && mobileMenu) { 15 // Update ARIA attributes 16 mobileMenuToggle.setAttribute('aria-expanded', 'false'); 17 mobileMenu.setAttribute('aria-hidden', 'true'); 18 19 mobileMenuToggle.addEventListener('click', function() { 20 const isActive = mobileMenu.classList.toggle('active'); 21 22 // Update ARIA attributes 23 mobileMenuToggle.setAttribute('aria-expanded', isActive ? 'true' : 'false'); 24 mobileMenu.setAttribute('aria-hidden', isActive ? 'false' : 'true'); 25 26 // Toggle body scroll 27 document.body.style.overflow = isActive ? 'hidden' : ''; 28 29 // Update icon 30 const icon = mobileMenuToggle.querySelector('svg'); 31 if (icon) { 32 if (isActive) { 33 icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />'; 34 icon.setAttribute('aria-label', 'Close menu'); 35 } else { 36 icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />'; 37 icon.setAttribute('aria-label', 'Open menu'); 38 } 39 } 40 }); 41 42 // Close menu on escape key 43 document.addEventListener('keydown', function(e) { 44 if (e.key === 'Escape' && mobileMenu.classList.contains('active')) { 45 mobileMenu.classList.remove('active'); 46 mobileMenuToggle.setAttribute('aria-expanded', 'false'); 47 mobileMenu.setAttribute('aria-hidden', 'true'); 48 document.body.style.overflow = ''; 49 mobileMenuToggle.focus(); 50 } 51 }); 52 53 // Mobile Submenu Toggle 54 document.querySelectorAll('.submenu-toggle').forEach(function(toggle) { 55 toggle.addEventListener('click', function(e) { 56 e.preventDefault(); 57 e.stopPropagation(); 58 this.classList.toggle('active'); 59 var submenu = this.nextElementSibling; 60 if (submenu && submenu.classList.contains('mobile-submenu')) { 61 submenu.classList.toggle('active'); 62 } 63 }); 64 }); 65 } 66 67 // Dropdown edge detection 68 document.querySelectorAll('.nav-item.has-dropdown').forEach(function(item) { 69 item.addEventListener('mouseenter', function() { 70 var dropdown = this.querySelector('.nav-dropdown'); 71 if (dropdown) { 72 var rect = dropdown.getBoundingClientRect(); 73 if (rect.right > window.innerWidth) { 74 dropdown.classList.add('dropdown-reverse'); 75 } 76 } 77 }); 78 item.addEventListener('mouseleave', function() { 79 var dropdown = this.querySelector('.nav-dropdown'); 80 if (dropdown) { 81 dropdown.classList.remove('dropdown-reverse'); 82 } 83 }); 84 }); 85 86 // Header scroll effect 87 const header = document.getElementById('main-header'); 88 if (header) { 89 let lastScroll = 0; 90 window.addEventListener('scroll', function() { 91 const currentScroll = window.pageYOffset; 92 93 if (currentScroll > 50) { 94 header.classList.add('scrolled'); 95 } else { 96 header.classList.remove('scrolled'); 97 } 98 99 lastScroll = currentScroll; 100 }); 101 } 102 103 // Smooth scroll for anchor links 104 // Respect prefers-reduced-motion 105 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 106 107 document.querySelectorAll('a[href^="#"]').forEach(anchor => { 108 anchor.addEventListener('click', function (e) { 109 const href = this.getAttribute('href'); 110 if (href !== '#' && href.length > 1) { 111 const target = document.querySelector(href); 112 if (target) { 113 e.preventDefault(); 114 const headerOffset = 80; 115 const elementPosition = target.getBoundingClientRect().top; 116 const offsetPosition = elementPosition + window.pageYOffset - headerOffset; 117 118 window.scrollTo({ 119 top: offsetPosition, 120 behavior: prefersReducedMotion ? 'auto' : 'smooth' 121 }); 122 123 // Close mobile menu if open 124 if (mobileMenu && mobileMenu.classList.contains('active')) { 125 mobileMenu.classList.remove('active'); 126 document.body.style.overflow = ''; 127 // Update icon 128 const icon = mobileMenuToggle.querySelector('svg'); 129 if (icon) { 130 icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />'; 131 } 132 } 133 134 // Focus target for accessibility 135 target.setAttribute('tabindex', '-1');
136 target.focus(); 137 } 138 } 139 }); 140 }); 141 142 // Card hover effects - removed to prevent layout shift 143 // Cards now use CSS-only hover effects (box-shadow changes only) 144 145 // Lazy loading images 146 if ('loading' in HTMLImageElement.prototype) { 147 const images = document.querySelectorAll('img[loading="lazy"]'); 148 images.forEach(img => { 149 if (img.dataset.src) { 150 img.src = img.dataset.src; 151 } 152 }); 153 } else { 154 // Fallback for browsers that don't support lazy loading 155 const script = document.createElement('script'); 156 script.src = '/assets/vendor/lazysizes-5.3.2/lazysizes.min.js'; 157 script.onerror = function() { 158 const fallbackScript = document.createElement('script'); 159 fallbackScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.3.2/lazysizes.min.js'; 160 document.body.appendChild(fallbackScript); 161 }; 162 document.body.appendChild(script); 163 } 164 165 // Intersection Observer for fade-in animations 166 // Respect prefers-reduced-motion (using variable declared above) 167 if (!prefersReducedMotion) { 168 const observerOptions = { 169 threshold: 0.1, 170 rootMargin: '0px 0px -50px 0px' 171 }; 172 173 const observer = new IntersectionObserver(function(entries) { 174 entries.forEach(entry => { 175 if (entry.isIntersecting) { 176 entry.target.style.opacity = '1'; 177 entry.target.style.transform = 'translateY(0)'; 178 } 179 }); 180 }, observerOptions); 181 182 // Observe elements for animation 183 document.querySelectorAll('.card, .home-section__widgets > *').forEach(el => { 184 el.style.opacity = '0'; 185 el.style.transform = 'translateY(20px)'; 186 el.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; 187 observer.observe(el); 188 }); 189 } 190 191 // Product gallery interactions 192 const galleryViewport = document.querySelector('.product-gallery__viewport'); 193 if (galleryViewport) { 194 const slides = Array.from(galleryViewport.querySelectorAll('.product-gallery__slide')); 195 const thumbButtons = Array.from(document.querySelectorAll('.product-gallery__thumb')); 196 197 const setActiveSlide = (index) => { 198 slides.forEach((slide, slideIndex) => { 199 slide.classList.toggle('product-gallery__slide--active', slideIndex === index); 200 }); 201 202 thumbButtons.forEach((thumb, thumbIndex) => { 203 thumb.classList.toggle('product-gallery__thumb--active', thumbIndex === index); 204 }); 205 }; 206 207 thumbButtons.forEach((thumb, index) => { 208 thumb.addEventListener('click', () => setActiveSlide(index)); 209 }); 210 } 211 // ============================================ 212 // Listing View Toggle (Grid / List) 213 // ============================================ 214 document.querySelectorAll('.listing-view-toggle').forEach(toggleGroup => { 215 const targetId = toggleGroup.dataset.listingTarget; 216 const container = targetId ? document.getElementById(targetId) : null; 217 if (!container) return; 218 219 const storageKey = 'polycms_view_' + targetId; 220 const buttons = toggleGroup.querySelectorAll('.view-toggle-btn'); 221 222 // Restore saved preference 223 const saved = localStorage.getItem(storageKey); 224 if (saved && (saved === 'grid' || saved === 'list')) { 225 container.classList.remove('is-grid', 'is-list'); 226 container.classList.add('is-' + saved); 227 buttons.forEach(btn => { 228 btn.classList.toggle('active', btn.dataset.view === saved); 229 }); 230 } 231 232 buttons.forEach(btn => { 233 btn.addEventListener('click', () => { 234 const view = btn.dataset.view; 235 container.classList.remove('is-grid', 'is-list'); 236 container.classList.add('is-' + view); 237 238 buttons.forEach(b => b.classList.remove('active')); 239 btn.classList.add('active'); 240 241 localStorage.setItem(storageKey, view); 242 }); 243 }); 244 }); 245 246 // ============================================ 247 // Post Scroll Tools 248 // ============================================ 249 const scrollToTopBtn = document.getElementById('scroll-to-top'); 250 if (scrollToTopBtn) { 251 scrollToTopBtn.addEventListener('click', () => { 252 window.scrollTo({ 253 top: 0, 254 behavior: prefersReducedMotion ? 'auto' : 'smooth' 255 }); 256 }); 257 } 258 259 const scrollToBottomBtn = document.getElementById('scroll-to-bottom'); 260 if (scrollToBottomBtn) { 261 scrollToBottomBtn.addEventListener('click', () => { 262 window.scrollTo({ 263 top: document.body.scrollHeight, 264 behavior: prefersReducedMotion ? 'auto' : 'smooth' 265 }); 266 }); 267 } 268 269 // Close admin card edit dropdowns when clicking outside 270 document.addEventListener('click', function(e) { 271 if (!e.target.closest('.admin-card-edit-dropdown')) { 272 document.querySelectorAll('.admin-card-edit-menu').forEach(function(m) { 273 m.style.display = 'none'; 274 }); 275 } 276 }); 277 278 console.log('PolyCMS Corporate Theme loaded successfully!'); 279})(); 280
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.