1/** 2 * Dilux One Tabs - Frontend JavaScript 3 */ 4 5class DiluxMagicTabs { 6 constructor(element) { 7 this.element = element; 8 this.groupId = element.dataset.groupId; 9 this.componentType = element.dataset.componentType || 'tabs'; 10 this.orientation = element.dataset.orientation || 'horizontal'; 11 this.animation = element.dataset.animation || 'fade'; 12 this.lazyLoading = element.dataset.lazyLoading === 'true'; 13 this.deepLinking = element.dataset.deepLinking === 'true'; 14 this.mobileBreakpoint = 768; 15 16 // Check if this is an accordion component 17 this.isAccordion = this.componentType === 'accordion'; 18 19 if (this.isAccordion) { 20 // Accordion elements 21 this.accordionBehavior = element.dataset.accordionBehavior || 'single'; 22 this.accordionItems = element.querySelectorAll('.dilux-accordion-item'); 23 this.accordionHeaders = element.querySelectorAll('.dilux-accordion-header'); 24 this.accordionPanels = element.querySelectorAll('.dilux-accordion-panel'); 25 this.loadedTabs = new Set(); // Track loaded content 26 27 // Find initially open panels 28 this.accordionPanels.forEach((panel, index) => { 29 if (panel.classList.contains('active')) { 30 this.loadedTabs.add(index); 31 } 32 }); 33 } else { 34 // Regular tabs elements 35 this.tabButtons = element.querySelectorAll('.dilux-tab-button'); 36 this.tabPanels = element.querySelectorAll('.dilux-tab-panel'); 37 this.activeIndex = 0; 38 this.loadedTabs = new Set([0]); // First tab is already loaded 39 } 40 41 this.init(); 42 } 43 44 init() { 45 if (this.isAccordion) { 46 // Initialize accordion 47 this.initAccordion(); 48 } else { 49 // Initialize regular tabs 50 this.initRegularTabs(); 51 } 52 53 // Handle deep linking 54 if (this.deepLinking) { 55 this.handleDeepLinking(); 56 window.addEventListener('hashchange', () => this.handleDeepLinking()); 57 } 58 } 59 60 initRegularTabs() { 61 // Attach click handlers to tab buttons 62 this.tabButtons.forEach((button, index) => { 63 button.addEventListener('click', () => { 64 this.switchTab(index); 65 }); 66 }); 67 68 // Transform to accordion on mobile 69 if (window.innerWidth <= this.mobileBreakpoint) { 70 this.transformToAccordion(); 71 } 72 73 // Re-check on resize (with debounce) 74 let resizeTimer; 75 window.addEventListener('resize', () => { 76 clearTimeout(resizeTimer); 77 resizeTimer = setTimeout(() => { 78 if (window.innerWidth <= this.mobileBreakpoint && !this.element.classList.contains('dilux-magic-tabs-accordion')) { 79 this.transformToAccordion(); 80 } else if (window.innerWidth > this.mobileBreakpoint && this.element.classList.contains('dilux-magic-tabs-accordion')) { 81 this.restoreFromAccordion(); 82 } 83 }, 250); 84 }); 85 } 86 87 initAccordion() { 88 // Attach click handlers to accordion headers 89 this.accordionHeaders.forEach((header, index) => { 90 header.addEventListener('click', () => { 91 this.toggleAccordionItem(index); 92 }); 93 }); 94 } 95 96 toggleAccordionItem(index) { 97 const item = this.accordionItems[index]; 98 const header = this.accordionHeaders[index]; 99 const panel = this.accordionPanels[index]; 100 const isActive = item.classList.contains('active'); 101 102 if (isActive) { 103 // Close this item 104 item.classList.remove('active'); 105 header.setAttribute('aria-expanded', 'false'); 106 panel.classList.remove('active'); 107 } else { 108 // If behavior is 'single', close all other items first 109 if (this.accordionBehavior === 'single') {
110 this.accordionItems.forEach((otherItem, i) => { 111 if (i !== index) { 112 otherItem.classList.remove('active'); 113 this.accordionHeaders[i].setAttribute('aria-expanded', 'false'); 114 this.accordionPanels[i].classList.remove('active'); 115 } 116 }); 117 } 118 119 // Open this item 120 item.classList.add('active'); 121 header.setAttribute('aria-expanded', 'true'); 122 panel.classList.add('active'); 123 124 // Load content if lazy loading 125 if (this.lazyLoading && !this.loadedTabs.has(index)) { 126 this.loadAccordionContent(panel, index); 127 } 128 129 // Update URL hash if deep linking enabled 130 if (this.deepLinking) { 131 const itemElement = this.accordionItems[index]; 132 const slug = itemElement.dataset.tabSlug; 133 if (slug) { 134 history.pushState(null, null, '#tab-' + slug); 135 } 136 } 137 } 138 } 139 140 loadAccordionContent(panel, index) { 141 const itemElement = this.accordionItems[index]; 142 const tabId = itemElement.dataset.tabId; 143 144 if (!tabId || !panel.dataset.lazy) { 145 return; 146 } 147 148 const contentWrapper = panel.querySelector('.dilux-accordion-content'); 149 150 // Show loading state 151 contentWrapper.innerHTML = '<div class="dilux-accordion-loading"><p>Loading...</p></div>'; 152 153 // AJAX request 154 fetch(diluxMagicTabsData.ajaxUrl, { 155 method: 'POST', 156 headers: { 157 'Content-Type': 'application/x-www-form-urlencoded', 158 }, 159 body: new URLSearchParams({ 160 action: 'dilux_magic_tabs_load_content', 161 nonce: diluxMagicTabsData.nonce, 162 tab_id: tabId, 163 }), 164 }) 165 .then(response => response.json()) 166 .then(data => { 167 if (data.success) { 168 contentWrapper.innerHTML = data.data.content; 169 panel.removeAttribute('data-lazy'); 170 this.loadedTabs.add(index); 171 } else { 172 contentWrapper.innerHTML = '<div class="dilux-accordion-error"><p>Error loading content.</p></div>'; 173 } 174 }) 175 .catch(error => { 176 contentWrapper.innerHTML = '<div class="dilux-accordion-error"><p>Error loading content.</p></div>'; 177 }); 178 } 179 180 switchTab(index) { 181 if (index === this.activeIndex) { 182 return; // Already active 183 } 184 185 const fromPanel = this.tabPanels[this.activeIndex]; 186 const toPanel = this.tabPanels[index]; 187 const toButton = this.tabButtons[index]; 188 189 // Deactivate current tab 190 this.tabButtons[this.activeIndex].classList.remove('active'); 191 this.tabButtons[this.activeIndex].setAttribute('aria-selected', 'false'); 192 fromPanel.classList.remove('active'); 193 194 // Update active index 195 this.activeIndex = index; 196 197 // Lazy load if needed 198 if (this.lazyLoading && !this.loadedTabs.has(index)) { 199 this.loadTabContent(toPanel, () => { 200 this.activateTab(toButton, toPanel); 201 }); 202 } else { 203 this.activateTab(toButton, toPanel); 204 } 205 206 // Update URL hash if deep linking enabled 207 if (this.deepLinking) { 208 const slug = toButton.dataset.tabSlug; 209 if (slug) { 210 history.pushState(null, null, '#tab-' + slug); 211 } 212 } 213 } 214 215 activateTab(button, panel) { 216 button.classList.add('active'); 217 button.setAttribute('aria-selected', 'true'); 218 panel.classList.add('active'); 219 220 // Scroll to tabs container (smooth scroll) 221 if (window.innerWidth <= this.mobileBreakpoint) { 222 this.element.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 223 } 224 } 225 226 loadTabContent(panel, callback) { 227 const tabId = panel.dataset.tabId; 228 229 if (!tabId || !panel.dataset.lazy) { 230 callback(); 231 return; 232 } 233 234 // Show loading state 235 panel.innerHTML = '<div class="dilux-tab-loading"><p>Loading...</p></div>'; 236 237 // AJAX request 238 fetch(diluxMagicTabsData.ajaxUrl, { 239 method: 'POST', 240 headers: { 241 'Content-Type': 'application/x-www-form-urlencoded', 242 }, 243 body: new URLSearchParams({ 244 action: 'dilux_magic_tabs_load_content', 245 nonce: diluxMagicTabsData.nonce, 246 tab_id: tabId, 247 }), 248 }) 249 .then(response => response.json()) 250 .then(data => { 251 if (data.success) { 252 panel.innerHTML = data.data.content; 253 panel.removeAttribute('data-lazy'); 254 this.loadedTabs.add(parseInt(panel.dataset.tabIndex)); 255 callback(); 256 } else { 257 panel.innerHTML = '<div class="dilux-tab-error"><p>Error loading content.</p></div>'; 258 } 259 }) 260 .catch(error => { 261 panel.innerHTML = '<div class="dilux-tab-error"><p>Error loading content.</p></div>'; 262 }); 263 } 264 265 handleDeepLinking() { 266 const hash = window.location.hash; 267 268 if (!hash || !hash.startsWith('#tab-')) { 269 return; 270 } 271 272 const slug = hash.replace('#tab-', ''); 273 274 if (this.isAccordionVertical) { 275 // Find accordion header with matching slug
276 this.accordionHeaders.forEach((header, index) => { 277 if (header.dataset.tabSlug === slug) { 278 this.toggleAccordionItem(index); 279 } 280 }); 281 } else { 282 // Find tab with matching slug 283 this.tabButtons.forEach((button, index) => { 284 if (button.dataset.tabSlug === slug) { 285 this.switchTab(index); 286 } 287 }); 288 } 289 } 290 291 transformToAccordion() { 292 this.element.classList.add('dilux-magic-tabs-accordion'); 293 294 // Create accordion structure 295 const accordion = document.createElement('div'); 296 accordion.className = 'dilux-accordion'; 297 298 this.tabButtons.forEach((button, index) => { 299 const panel = this.tabPanels[index]; 300 301 // Create accordion item 302 const item = document.createElement('div'); 303 item.className = 'dilux-accordion-item'; 304 305 // Create accordion header 306 const header = document.createElement('button'); 307 header.className = 'dilux-accordion-header' + (index === this.activeIndex ? ' active' : ''); 308 header.innerHTML = button.innerHTML + '<span class="dilux-accordion-arrow">â¼</span>'; 309 header.setAttribute('aria-expanded', index === this.activeIndex ? 'true' : 'false'); 310 311 // Create accordion content 312 const content = document.createElement('div'); 313 content.className = 'dilux-accordion-content' + (index === this.activeIndex ? ' active' : ''); 314 315 const contentInner = document.createElement('div'); 316 contentInner.className = 'dilux-accordion-content-inner'; 317 contentInner.innerHTML = panel.innerHTML; 318 319 content.appendChild(contentInner); 320 321 // Click handler 322 header.addEventListener('click', () => { 323 // Close all other items 324 accordion.querySelectorAll('.dilux-accordion-header').forEach(h => { 325 if (h !== header) { 326 h.classList.remove('active'); 327 h.setAttribute('aria-expanded', 'false'); 328 h.nextElementSibling.classList.remove('active'); 329 } 330 }); 331 332 // Toggle this item 333 const isActive = header.classList.contains('active'); 334 if (isActive) { 335 header.classList.remove('active'); 336 header.setAttribute('aria-expanded', 'false'); 337 content.classList.remove('active'); 338 } else { 339 header.classList.add('active'); 340 header.setAttribute('aria-expanded', 'true'); 341 content.classList.add('active'); 342 343 // Load content if lazy loading 344 if (this.lazyLoading && !this.loadedTabs.has(index)) { 345 this.loadTabContent(panel, () => { 346 contentInner.innerHTML = panel.innerHTML; 347 this.loadedTabs.add(index); 348 }); 349 } 350 } 351 }); 352 353 item.appendChild(header); 354 item.appendChild(content); 355 accordion.appendChild(item); 356 }); 357 358 this.element.appendChild(accordion); 359 } 360 361 restoreFromAccordion() { 362 this.element.classList.remove('dilux-magic-tabs-accordion'); 363 364 // Remove accordion structure 365 const accordion = this.element.querySelector('.dilux-accordion'); 366 if (accordion) { 367 accordion.remove(); 368 } 369 } 370} 371 372// Initialize all tab groups and accordions on page load 373document.addEventListener('DOMContentLoaded', () => { 374 // Initialize tabs 375 document.querySelectorAll('.dilux-magic-tabs-container').forEach(element => { 376 new DiluxMagicTabs(element); 377 }); 378 379 // Initialize accordions 380 document.querySelectorAll('.dilux-magic-accordion-container').forEach(element => { 381 new DiluxMagicTabs(element); 382 }); 383});
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.