1/** 2 * LSC New Theme - Main JavaScript 3 * Kunco-inspired navigation and interactions 4 */ 5 6(function (Drupal, once) { 7 'use strict'; 8 9 /** 10 * Offcanvas Menu Toggle 11 * Opens/closes mobile offcanvas menu 12 */ 13 Drupal.behaviors.offcanvasMenu = { 14 attach: function (context) { 15 once('offcanvas-menu', '.menu-toggle', context).forEach(function (toggle) { 16 const menu = document.querySelector('.offcanvas-menu'); 17 const overlay = document.querySelector('.offcanvas-overlay'); 18 const closeBtn = document.querySelector('.offcanvas-close'); 19 20 if (!menu || !overlay) return; 21 22 // Open menu 23 toggle.addEventListener('click', function (e) { 24 e.preventDefault(); 25 e.stopPropagation(); 26 27 menu.classList.add('active'); 28 overlay.classList.add('active'); 29 document.body.classList.add('offcanvas-active'); 30 toggle.setAttribute('aria-expanded', 'true'); 31 32 // Focus on close button for accessibility 33 setTimeout(function() { 34 if (closeBtn) closeBtn.focus(); 35 }, 300); 36 }); 37 38 // Close menu function 39 const closeMenu = function () { 40 menu.classList.remove('active'); 41 overlay.classList.remove('active'); 42 document.body.classList.remove('offcanvas-active'); 43 toggle.setAttribute('aria-expanded', 'false'); 44 45 // Return focus to toggle button 46 toggle.focus(); 47 }; 48 49 // Close button click 50 if (closeBtn) { 51 closeBtn.addEventListener('click', function (e) { 52 e.preventDefault(); 53 closeMenu(); 54 }); 55 } 56 57 // Overlay click 58 overlay.addEventListener('click', function () { 59 closeMenu(); 60 }); 61 62 // Close on ESC key 63 document.addEventListener('keydown', function (e) { 64 if (e.key === 'Escape' && menu.classList.contains('active')) { 65 closeMenu(); 66 } 67 }); 68 }); 69 } 70 }; 71 72 /** 73 * Offcanvas Submenu Accordion 74 * Expands/collapses submenu items in offcanvas menu 75 * Uses event delegation on document to ensure it works 76 */ 77 Drupal.behaviors.offcanvasSubmenu = { 78 attach: function (context, settings) { 79 // Only attach once to the document 80 once('offcanvas-submenu', 'body', context).forEach(function () { 81 document.addEventListener('click', function (e) { 82 // Check if click is within offcanvas menu 83 const offcanvasMenu = e.target.closest('.offcanvas-menu'); 84 if (!offcanvasMenu) return; 85 86 // Find the clicked link with data-submenu-toggle 87 const link = e.target.closest('[data-submenu-toggle]'); 88 if (!link) return; 89 90 // Get the parent dropdown item 91 const item = link.parentElement; 92 if (!item || !item.classList.contains('dropdown')) return; 93 94 // Check if this dropdown has a submenu 95 const submenu = item.querySelector(':scope > .dropdown-menu'); 96 if (!submenu) return; 97 98 // Prevent default and stop propagation 99 e.preventDefault(); 100 e.stopPropagation(); 101 102 // Close other open submenus at same level 103 const parent = item.parentElement; 104 if (parent) { 105 const siblings = Array.from(parent.children); 106 siblings.forEach(function (sibling) { 107 if (sibling !== item && 108 sibling.classList.contains('dropdown') && 109 sibling.classList.contains('active')) { 110 sibling.classList.remove('active'); 111 } 112 }); 113 } 114 115 // Toggle current submenu 116 item.classList.toggle('active'); 117 }, true); // Use capture phase 118 }); 119 } 120 }; 121 122 /** 123 * Sticky Header with Scroll Effect 124 * Adds shadow when scrolling 125 */ 126 Drupal.behaviors.stickyHeader = { 127 attach: function (context) { 128 once('sticky-header', '.site-header', context).forEach(function (header) { 129 let ticking = false; 130 131 const updateHeader = function () { 132 const currentScroll = window.pageYOffset || document.documentElement.scrollTop; 133 134 if (currentScroll > 80) { 135 header.classList.add('scrolled'); 136 } else { 137 header.classList.remove('scrolled'); 138 } 139 140 ticking = false;
141 }; 142 143 // Initial check 144 updateHeader(); 145 146 window.addEventListener('scroll', function () { 147 if (!ticking) { 148 window.requestAnimationFrame(updateHeader); 149 ticking = true; 150 } 151 }, { passive: true }); 152 }); 153 } 154 }; 155 156 /** 157 * More Menu Toggle (Desktop 3-dots) 158 * Optional: Can be used for additional menu options 159 */ 160 Drupal.behaviors.moreMenuToggle = { 161 attach: function (context) { 162 once('more-menu', '.more-menu-toggle', context).forEach(function (toggle) { 163 toggle.addEventListener('click', function (e) { 164 e.preventDefault(); 165 // Add your more menu functionality here 166 console.log('More menu clicked'); 167 }); 168 }); 169 } 170 }; 171 172 // Stats Counter Animation 173 Drupal.behaviors.statsCounter = { 174 attach: function (context) { 175 once('stats-counter', '.stat-number[data-count]', context).forEach(function (counter) { 176 const target = parseInt(counter.getAttribute('data-count')); 177 const duration = 2000; // 2 seconds 178 const increment = target / (duration / 16); // 60 FPS 179 let current = 0; 180 181 const observer = new IntersectionObserver(function (entries) { 182 entries.forEach(function (entry) { 183 if (entry.isIntersecting) { 184 const updateCounter = function () { 185 current += increment; 186 if (current < target) { 187 counter.textContent = Math.ceil(current).toLocaleString(); 188 requestAnimationFrame(updateCounter); 189 } else { 190 counter.textContent = target.toLocaleString(); 191 } 192 }; 193 updateCounter(); 194 observer.unobserve(counter); 195 } 196 }); 197 }, { threshold: 0.5 }); 198 199 observer.observe(counter); 200 }); 201 } 202 }; 203 204 // Video Modal 205 Drupal.behaviors.videoModal = { 206 attach: function (context) { 207 once('video-modal', '.video-play-button', context).forEach(function (button) { 208 button.addEventListener('click', function () { 209 const videoUrl = button.getAttribute('data-video-url'); 210 const modal = document.querySelector('.video-modal'); 211 const modalContent = modal.querySelector('.video-modal-content'); 212 const closeBtn = modal.querySelector('.video-modal-close'); 213 214 if (!modal || !videoUrl) return; 215 216 // Create iframe 217 const iframe = document.createElement('iframe'); 218 iframe.src = videoUrl + '?autoplay=1'; 219 iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'; 220 iframe.allowFullscreen = true; 221 222 // Add iframe to modal 223 modalContent.innerHTML = ''; 224 modalContent.appendChild(iframe); 225 226 // Show modal 227 modal.classList.add('active'); 228 document.body.style.overflow = 'hidden'; 229 230 // Close modal 231 const closeModal = function () { 232 modal.classList.remove('active'); 233 modalContent.innerHTML = ''; 234 document.body.style.overflow = ''; 235 }; 236 237 closeBtn.addEventListener('click', closeModal); 238 239 modal.addEventListener('click', function (e) { 240 if (e.target === modal) { 241 closeModal(); 242 } 243 }); 244 245 // Close on ESC 246 document.addEventListener('keydown', function (e) { 247 if (e.key === 'Escape' && modal.classList.contains('active')) { 248 closeModal(); 249 } 250 }); 251 }); 252 }); 253 } 254 }; 255 256 // Progress Bar Animation 257 Drupal.behaviors.progressBar = { 258 attach: function (context) { 259 once('progress-bar', '.progress-bar-fill', context).forEach(function (bar) { 260 const percentage = bar.getAttribute('data-percentage'); 261 262 const observer = new IntersectionObserver(function (entries) { 263 entries.forEach(function (entry) { 264 if (entry.isIntersecting) { 265 setTimeout(function () { 266 bar.style.width = percentage + '%'; 267 }, 100); 268 observer.unobserve(bar); 269 } 270 }); 271 }, { threshold: 0.5 }); 272 273 observer.observe(bar); 274 }); 275 } 276 }; 277 278 // Smooth Scroll for Anchor Links 279 Drupal.behaviors.smoothScroll = { 280 attach: function (context) { 281 once('smooth-scroll', 'a[href^="#"]', context).forEach(function (link) { 282 link.addEventListener('click', function (e) { 283 const targetId = this.getAttribute('href'); 284 if (targetId === '#') return; 285 286 const target = document.querySelector(targetId); 287 if (target) { 288 e.preventDefault(); 289 const headerHeight = document.querySelector('.site-header').offsetHeight; 290 const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - headerHeight; 291 292 window.scrollTo({ 293 top: targetPosition, 294 behavior: 'smooth' 295 }); 296 } 297 }); 298 }); 299 } 300 }; 301 302 // Fade In Animation on Scroll 303 Drupal.behaviors.fadeInOnScroll = { 304 attach: function (context) { 305 once('fade-in', '.fade-in-up', context).forEach(function (element) { 306 const observer = new IntersectionObserver(function (entries) {
307 entries.forEach(function (entry) { 308 if (entry.isIntersecting) { 309 entry.target.style.opacity = '1'; 310 entry.target.style.transform = 'translateY(0)'; 311 observer.unobserve(entry.target); 312 } 313 }); 314 }, { threshold: 0.1 }); 315 316 element.style.opacity = '0'; 317 element.style.transform = 'translateY(30px)'; 318 element.style.transition = 'opacity 0.6s ease-out, transform 0.6s ease-out'; 319 320 observer.observe(element); 321 }); 322 } 323 }; 324 325 // Bootstrap Carousel Enhancement 326 Drupal.behaviors.carouselEnhancement = { 327 attach: function (context) { 328 once('carousel-enhance', '.carousel', context).forEach(function (carousel) { 329 // Initialize Bootstrap carousel if not already initialized 330 if (typeof bootstrap !== 'undefined' && bootstrap.Carousel) { 331 new bootstrap.Carousel(carousel, { 332 interval: 5000, 333 wrap: true, 334 touch: true 335 }); 336 } 337 }); 338 } 339 }; 340 341})(Drupal, once); 342// Image Text Lightbox 343(function() { 344 var lightbox = document.createElement('div'); 345 lightbox.className = 'image-text-lightbox'; 346 lightbox.innerHTML = '<img src="" alt="">'; 347 document.body.appendChild(lightbox); 348 var lbImg = lightbox.querySelector('img'); 349 350 document.querySelectorAll('.image-text-zoom').forEach(function(a) { 351 a.addEventListener('click', function(e) { 352 e.preventDefault(); 353 var img = this.querySelector('img'); 354 if (img) { 355 lbImg.src = img.src.replace(/\/styles\/[^/]+\/public\//, '/'); 356 lightbox.classList.add('is-open'); 357 } 358 }); 359 }); 360 361 lightbox.addEventListener('click', function() { 362 this.classList.remove('is-open'); 363 }); 364})();
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.