1/** 2 * Dilux One Countdown - Frontend JavaScript 3 * Handles countdown timer logic and SVG progress animations 4 */ 5 6(function() { 7 'use strict'; 8 9 /** 10 * Initialize all countdowns on page 11 */ 12 function initCountdowns() { 13 const countdowns = document.querySelectorAll('.dilux-countdown:not(.dilux-countdown-expired-state)'); 14 15 countdowns.forEach(function(countdown) { 16 const countdownObj = new DiluxCountdown(countdown); 17 countdownObj.start(); 18 }); 19 } 20 21 /** 22 * Countdown Timer Class 23 */ 24 class DiluxCountdown { 25 constructor(element) { 26 this.element = element; 27 this.expiryTimestamp = parseInt(element.dataset.expiry); 28 this.showMessage = element.dataset.showMessage === '1'; 29 this.blurOnExpiry = element.dataset.blur === '1'; 30 this.message = element.dataset.message || 'This countdown has expired'; 31 this.animation = element.dataset.animation || 'none'; 32 this.celebrationType = element.dataset.celebrationType || 'classic'; 33 this.units = element.querySelectorAll('.dilux-countdown-unit'); 34 this.interval = null; 35 } 36 37 /** 38 * Start the countdown 39 */ 40 start() { 41 // Initial update 42 this.update(); 43 44 // Update every second 45 this.interval = setInterval(() => { 46 this.update(); 47 }, 1000); 48 } 49 50 /** 51 * Stop the countdown 52 */ 53 stop() { 54 if (this.interval) { 55 clearInterval(this.interval); 56 this.interval = null; 57 } 58 } 59 60 /** 61 * Update countdown display 62 */ 63 update() { 64 const now = Math.floor(Date.now() / 1000); 65 const remaining = this.expiryTimestamp - now; 66 67 if (remaining <= 0) { 68 this.handleExpiry(); 69 return; 70 } 71 72 const time = this.calculateTime(remaining); 73 this.updateDisplay(time); 74 } 75 76 /** 77 * Calculate time units from remaining seconds 78 */ 79 calculateTime(seconds) { 80 return { 81 days: Math.floor(seconds / 86400), 82 hours: Math.floor((seconds % 86400) / 3600), 83 minutes: Math.floor((seconds % 3600) / 60), 84 seconds: seconds % 60 85 }; 86 } 87 88 /** 89 * Update display for each unit 90 */ 91 updateDisplay(time) { 92 this.units.forEach((unit) => { 93 const unitType = unit.dataset.unit; 94 const value = time[unitType]; 95 const max = parseInt(unit.dataset.max); 96 97 // Update number 98 const numberEl = unit.querySelector('.dilux-countdown-number'); 99 if (numberEl) { 100 numberEl.textContent = value; 101 } 102 103 // Update progress circle 104 this.updateProgress(unit, value, max); 105 }); 106 } 107 108 /** 109 * Update SVG circle progress 110 */ 111 updateProgress(unit, currentValue, maxValue) { 112 const circle = unit.querySelector('.dilux-countdown-progress'); 113 if (!circle) return; 114 115 const radius = parseFloat(circle.getAttribute('r')); 116 const circumference = 2 * Math.PI * radius; 117 118 // Calculate progress (0 to 1) 119 // For countdown, we want progress to decrease as value decreases 120 const progress = currentValue / maxValue; 121 122 // Calculate stroke-dashoffset 123 // When progress = 1 (full), offset = 0 (full circle) 124 // When progress = 0 (empty), offset = circumference (empty circle) 125 const offset = circumference * (1 - progress); 126 127 circle.style.strokeDashoffset = offset; 128 } 129 130 /** 131 * Handle countdown expiry 132 * Supports: show_expiry_message, blur_on_expiry, expiry_animation (all independent) 133 */ 134 handleExpiry() { 135 this.stop(); 136 137 // Set all values to zero 138 const time = { days: 0, hours: 0, minutes: 0, seconds: 0 }; 139 this.updateDisplay(time); 140 141 // Set all progress circles to empty
142 this.units.forEach((unit) => { 143 this.updateProgress(unit, 0, parseInt(unit.dataset.max)); 144 }); 145 146 // Clone the current countdown element 147 const countdownClone = this.element.cloneNode(true); 148 149 // Build wrapper with appropriate classes 150 const wrapper = document.createElement('div'); 151 wrapper.className = 'dilux-countdown-expired-wrapper'; 152 wrapper.dataset.countdownId = this.element.dataset.countdownId; 153 wrapper.dataset.animation = this.animation; 154 wrapper.dataset.celebrationType = this.celebrationType; 155 wrapper.dataset.blur = this.blurOnExpiry ? '1' : '0'; 156 157 // Add animation class if specified 158 if (this.animation && this.animation !== 'none') { 159 wrapper.classList.add('dilux-animation-' + this.animation); 160 } 161 162 // Add blur class if specified 163 if (this.blurOnExpiry) { 164 wrapper.classList.add('dilux-expired-blur'); 165 } 166 167 // Case 1: Show message (with or without blur and animation) 168 if (this.showMessage) { 169 // Create background container with countdown 170 const background = document.createElement('div'); 171 background.className = 'dilux-countdown-expired-background'; 172 background.appendChild(countdownClone); 173 174 // Create message overlay 175 const messageDiv = document.createElement('div'); 176 messageDiv.className = 'dilux-countdown-expired-message'; 177 messageDiv.textContent = this.message; 178 179 // Assemble the structure 180 wrapper.appendChild(background); 181 wrapper.appendChild(messageDiv); 182 183 // Add animation elements 184 this.addAnimationElements(wrapper, this.animation); 185 186 // Replace the countdown element with the wrapper 187 this.element.parentNode.replaceChild(wrapper, this.element); 188 } 189 // Case 2: No message, just countdown with optional blur and animation 190 else { 191 // Wrap countdown without message 192 wrapper.appendChild(countdownClone); 193 194 // Add animation elements 195 this.addAnimationElements(wrapper, this.animation); 196 197 // Replace the countdown element with the wrapper 198 this.element.parentNode.replaceChild(wrapper, this.element); 199 } 200 } 201 202 /** 203 * Add animation elements based on animation type 204 */ 205 addAnimationElements(wrapper, animationType) { 206 if (!animationType || animationType === 'none') { 207 return; 208 } 209 210 switch (animationType) { 211 case 'celebration': 212 this.createConfetti(wrapper); 213 break; 214 case 'warning': 215 this.createWarningIcons(wrapper); 216 break; 217 case 'elegant': 218 this.createElegantOverlay(wrapper); 219 break; 220 } 221 } 222 223 /** 224 * Create confetti animation using canvas-confetti library 225 */ 226 createConfetti(wrapper) { 227 // Check if canvas-confetti is loaded 228 if (typeof confetti === 'undefined') { 229 return; 230 } 231 232 const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#6c5ce7', '#fd79a8']; 233 234 switch (this.celebrationType) { 235 case 'classic': 236 // Classic explosion from center 237 confetti({ 238 particleCount: 100, 239 spread: 70, 240 origin: { y: 0.6 }, 241 colors: colors 242 }); 243 break; 244 245 case 'cannons': 246 // Side cannons shooting confetti 247 const duration = 2 * 1000; 248 const end = Date.now() + duration; 249 250 (function frame() { 251 confetti({ 252 particleCount: 2, 253 angle: 60, 254 spread: 55, 255 origin: { x: 0, y: 0.6 }, 256 colors: colors 257 }); 258 confetti({ 259 particleCount: 2, 260 angle: 120, 261 spread: 55, 262 origin: { x: 1, y: 0.6 }, 263 colors: colors 264 }); 265 266 if (Date.now() < end) { 267 requestAnimationFrame(frame); 268 } 269 }()); 270 break; 271 272 case 'rain': 273 // Confetti falling from top for 3 seconds 274 const rainDuration = 3 * 1000; 275 const rainEnd = Date.now() + rainDuration; 276 277 (function rainFrame() { 278 confetti({ 279 particleCount: 3, 280 angle: 90, 281 spread: 45, 282 origin: { x: Math.random(), y: 0 }, 283 colors: colors, 284 gravity: 1.2, 285 scalar: 0.8 286 }); 287 288 if (Date.now() < rainEnd) { 289 requestAnimationFrame(rainFrame); 290 } 291 }()); 292 break; 293 294 case 'fireworks': 295 // Multiple coordinated bursts 296 const fireworksDuration = 3 * 1000; 297 const fireworksEnd = Date.now() + fireworksDuration; 298 const fireworksInterval = 400; 299 300 (function fireworksFrame() { 301 confetti({ 302 particleCount: 50, 303 spread: 100, 304 origin: { 305 x: Math.random() * 0.6 + 0.2, 306 y: Math.random() * 0.4 + 0.3 307 }, 308 colors: colors 309 }); 310 311 if (Date.now() < fireworksEnd) { 312 setTimeout(fireworksFrame, fireworksInterval); 313 } 314 }()); 315 break; 316 317 case 'stars': 318 // Star-shaped confetti explosion 319 const defaults = { 320 spread: 360, 321 ticks: 50, 322 gravity: 0, 323 decay: 0.94, 324 startVelocity: 30, 325 colors: colors 326 }; 327 328 function shoot() { 329 confetti({ 330 ...defaults, 331 particleCount: 40, 332 scalar: 1.2, 333 shapes: ['star'] 334 }); 335 336 confetti({ 337 ...defaults, 338 particleCount: 10, 339 scalar: 0.75, 340 shapes: ['circle'] 341 }); 342 } 343 344 setTimeout(shoot, 0); 345 setTimeout(shoot, 100); 346 setTimeout(shoot, 200); 347 break; 348 349 default: 350 // Fallback to classic 351 confetti({ 352 particleCount: 100, 353 spread: 70, 354 origin: { y: 0.6 }, 355 colors: colors 356 }); 357 } 358 } 359 360 /** 361 * Create warning icons animation 362 */ 363 createWarningIcons(wrapper) { 364 const positions = [ 365 { top: '10%', left: '10%' },
366 { top: '10%', right: '10%' }, 367 { bottom: '10%', left: '10%' }, 368 { bottom: '10%', right: '10%' } 369 ]; 370 371 positions.forEach((pos, index) => { 372 const icon = document.createElement('div'); 373 icon.className = 'dilux-warning-icon'; 374 icon.textContent = 'â '; 375 Object.assign(icon.style, pos); 376 icon.style.animationDelay = (index * 0.2) + 's'; 377 wrapper.appendChild(icon); 378 }); 379 } 380 381 /** 382 * Create elegant overlay animation 383 */ 384 createElegantOverlay(wrapper) { 385 const overlay = document.createElement('div'); 386 overlay.className = 'dilux-elegant-overlay'; 387 wrapper.appendChild(overlay); 388 } 389 390 /** 391 * Escape HTML to prevent XSS 392 */ 393 escapeHtml(text) { 394 const div = document.createElement('div'); 395 div.textContent = text; 396 return div.innerHTML; 397 } 398 } 399 400 // Initialize when DOM is ready 401 if (document.readyState === 'loading') { 402 document.addEventListener('DOMContentLoaded', initCountdowns); 403 } else { 404 initCountdowns(); 405 } 406 407 // Expose for debugging 408 window.DiluxCountdown = DiluxCountdown; 409 410})();
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.