1/** 2 * FrontBlocks Headline Marquee Effect 3 * Duplicates content for infinite scrolling marquee effect 4 */ 5 6(function() { 7 'use strict'; 8 9 /** 10 * Initialize marquee effect for headlines 11 */ 12 function initMarquee() { 13 const marqueeElements = document.querySelectorAll('.gb-marquee-infinite-scroll:not([data-marquee-initialized="true"])'); 14 15 marqueeElements.forEach(function(element) { 16 // Skip if already initialized or has wrapper 17 if (element.dataset.marqueeInitialized === 'true' || element.querySelector('.gb-marquee-wrapper')) { 18 return; 19 } 20 21 // Find the text content - prioritize .gb-headline-text 22 let textElement = element.querySelector('.gb-headline-text'); 23 24 // If no .gb-headline-text, look for direct text content or first text node 25 if (!textElement) { 26 // Check if element has direct text content 27 const hasDirectText = element.childNodes.length > 0 && 28 Array.from(element.childNodes).some(node => 29 node.nodeType === 3 && node.textContent.trim() !== '' 30 ); 31 32 if (hasDirectText) { 33 textElement = element; 34 } else { 35 // Try to find a span or other inline element 36 textElement = element.querySelector('span, a, strong, em, b, i') || element; 37 } 38 } 39 40 if (!textElement) { 41 return; 42 } 43 44 // Get the HTML content (preserves formatting) 45 const textContent = textElement.innerHTML || textElement.textContent; 46 47 if (!textContent || textContent.trim() === '') { 48 return; 49 } 50 51 // Marquee speed presets mapping 52 const MARQUEE_SPEEDS = { 53 'fast': 10, // 10 seconds - fast 54 'medium': 20, // 20 seconds - medium 55 'slow': 40 // 40 seconds - slow 56 }; 57 58 // Function to get marquee speed from element or parent - VERY AGGRESSIVE SEARCH 59 const getMarqueeSpeed = function(el) { 60 let speed = null; 61 62 // Method 1: Check dataset 63 if (el.dataset && el.dataset.marqueeSpeed) { 64 speed = el.dataset.marqueeSpeed; 65 } 66 67 // Method 2: Check getAttribute 68 if (!speed) { 69 speed = el.getAttribute('data-marquee-speed'); 70 } 71 72 // Method 3: Check all attributes manually 73 if (!speed && el.attributes) { 74 try { 75 Array.from(el.attributes).forEach(function(attr) { 76 if (attr.name === 'data-marquee-speed' && attr.value) { 77 speed = attr.value; 78 } 79 }); 80 } catch(e) {} 81 } 82 83 // Method 4: Check parent element 84 if (!speed && el.parentElement) { 85 if (el.parentElement.dataset && el.parentElement.dataset.marqueeSpeed) { 86 speed = el.parentElement.dataset.marqueeSpeed; 87 } 88 if (!speed) { 89 speed = el.parentElement.getAttribute('data-marquee-speed'); 90 } 91 } 92 93 // Method 5: Search in all ancestors 94 if (!speed) { 95 let parent = el.parentElement; 96 while (parent && parent !== document.body) { 97 if (parent.dataset && parent.dataset.marqueeSpeed) { 98 speed = parent.dataset.marqueeSpeed; 99 break; 100 } 101 if (parent.getAttribute && parent.getAttribute('data-marquee-speed')) { 102 speed = parent.getAttribute('data-marquee-speed'); 103 break; 104 } 105 parent = parent.parentElement; 106 } 107 } 108 109 // Method 6: Search in children 110 if (!speed) { 111 const speedElement = el.querySelector('[data-marquee-speed]'); 112 if (speedElement) { 113 speed = speedElement.dataset.marqueeSpeed || speedElement.getAttribute('data-marquee-speed'); 114 } 115 } 116 117 // Parse and validate - handle both numeric and preset values 118 let speedValue = 20; // default 119 120 if (speed) { 121 // Check if it's a preset string (fast, medium, slow) 122 if (MARQUEE_SPEEDS.hasOwnProperty(speed)) { 123 speedValue = MARQUEE_SPEEDS[speed]; 124 } else { 125 // Try to parse as number 126 const parsed = parseFloat(speed); 127 if (!isNaN(parsed) && parsed > 0) { 128 speedValue = parsed; 129 } 130 } 131 } 132 133 return speedValue; 134 }; 135 136 // Get initial speed value 137 const speedValue = getMarqueeSpeed(element); 138 139 // Function to calculate and setup marquee 140 const setupMarquee = function() { 141 // Create a temporary element to measure text width with same font styles 142 const tempElement = document.createElement('span'); 143 tempElement.style.cssText = 'position: absolute; visibility: hidden; white-space: nowrap; padding-right: 2em;'; 144 tempElement.innerHTML = textContent; 145 146 // Copy computed styles from original element if possible 147 if (textElement !== element) { 148 const computedStyle = window.getComputedStyle(textElement); 149 tempElement.style.fontSize = computedStyle.fontSize; 150 tempElement.style.fontFamily = computedStyle.fontFamily; 151 tempElement.style.fontWeight = computedStyle.fontWeight; 152 tempElement.style.letterSpacing = computedStyle.letterSpacing; 153 } 154 155 document.body.appendChild(tempElement); 156 157 // Get container width - ensure we have the actual width 158 let containerWidth = element.offsetWidth || element.clientWidth; 159 if (!containerWidth || containerWidth === 0) { 160 // Try to get from computed style 161 const computedWidth = window.getComputedStyle(element).width; 162 containerWidth = parseFloat(computedWidth) || 0; 163 } 164 165 // Fallback to parent width if still 0 166 if (!containerWidth || containerWidth === 0) { 167 containerWidth = element.parentElement ? (element.parentElement.offsetWidth || 0) : 0; 168 } 169 170 // Get single text copy width 171 const singleTextWidth = tempElement.offsetWidth; 172 173 // Remove temporary element 174 document.body.removeChild(tempElement); 175 176 // Calculate how many copies we need to fill the container width 177 // We need at least 2 copies for seamless loop, but more if text is short 178 let copiesNeeded = 2; 179 if (containerWidth > 0 && singleTextWidth > 0) { 180 // Calculate copies needed to fill container width 181 // Add 2 extra copies to ensure smooth continuous scrolling 182 copiesNeeded = Math.ceil((containerWidth / singleTextWidth) + 2); 183 // Ensure minimum of 2 copies for seamless loop 184 copiesNeeded = Math.max(copiesNeeded, 2); 185 } 186 187 // Create a wrapper for the marquee content 188 const wrapper = document.createElement('div'); 189 wrapper.className = 'gb-marquee-wrapper'; 190 // Store speed directly in wrapper as data attribute AND CSS variable 191 wrapper.setAttribute('data-marquee-speed', speedValue); 192 wrapper.style.setProperty('--marquee-speed', speedValue + 's'); 193 wrapper.style.cssText += 'display: flex; white-space: nowrap; will-change: transform; backface-visibility: hidden; -webkit-backface-visibility: hidden; transform: translateZ(0); -webkit-transform: translateZ(0); width: auto; min-width: 100%;'; 194 195 // Create copies based on calculated need 196 for (let i = 0; i < copiesNeeded; i++) { 197 const copy = document.createElement('span'); 198 copy.className = 'gb-marquee-copy'; 199 copy.innerHTML = textContent; 200 copy.style.cssText = 'display: inline-block; padding-right: 2em; flex-shrink: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden;'; 201 wrapper.appendChild(copy); 202 } 203 204 return wrapper; 205 }; 206 207 // Setup marquee - use requestAnimationFrame to ensure layout is ready 208 requestAnimationFrame(function() { 209 const wrapper = setupMarquee(); 210 211 // Replace content with wrapper 212 if (textElement === element) { 213 // Save any attributes that might be on the element 214 const savedAttributes = {};
215 Array.from(element.attributes).forEach(function(attr) { 216 if (attr.name !== 'class' && attr.name !== 'data-marquee-initialized') { 217 savedAttributes[attr.name] = attr.value; 218 } 219 }); 220 221 element.innerHTML = ''; 222 element.appendChild(wrapper); 223 224 // Restore attributes 225 Object.keys(savedAttributes).forEach(function(key) { 226 element.setAttribute(key, savedAttributes[key]); 227 }); 228 } else { 229 textElement.innerHTML = ''; 230 textElement.appendChild(wrapper); 231 } 232 233 // Function to update speed - can be called from outside 234 const updateSpeed = function(newSpeed) { 235 if (newSpeed && !isNaN(newSpeed) && newSpeed > 0) { 236 wrapper.setAttribute('data-marquee-speed', newSpeed); 237 wrapper.style.setProperty('--marquee-speed', newSpeed + 's'); 238 applyAnimation(); 239 } 240 }; 241 242 // Function to apply animation - reads speed from wrapper 243 const applyAnimation = function() { 244 // Get speed from wrapper (most reliable source) 245 let currentSpeed = wrapper.getAttribute('data-marquee-speed'); 246 247 // If not in wrapper, try to get from element and save to wrapper 248 if (!currentSpeed) { 249 currentSpeed = getMarqueeSpeed(element); 250 if (currentSpeed) { 251 wrapper.setAttribute('data-marquee-speed', currentSpeed); 252 wrapper.style.setProperty('--marquee-speed', currentSpeed + 's'); 253 } 254 } else { 255 currentSpeed = parseFloat(currentSpeed); 256 } 257 258 // Validate speed 259 if (!currentSpeed || isNaN(currentSpeed) || currentSpeed <= 0) { 260 currentSpeed = 20; 261 wrapper.setAttribute('data-marquee-speed', currentSpeed); 262 wrapper.style.setProperty('--marquee-speed', currentSpeed + 's'); 263 } 264 265 // Get the actual width of one copy after it's rendered 266 const firstCopy = wrapper.querySelector('.gb-marquee-copy'); 267 if (!firstCopy) return; 268 269 const copyWidth = firstCopy.offsetWidth; 270 271 if (copyWidth > 0) { 272 // Calculate animation distance in pixels (exactly one copy width) 273 // This ensures perfect seamless loop - no jumps or resets 274 const animationDistancePx = copyWidth; 275 276 // Get or create style ID 277 let styleId = wrapper.getAttribute('data-marquee-style-id'); 278 if (!styleId) { 279 styleId = 'marquee-style-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); 280 wrapper.setAttribute('data-marquee-style-id', styleId); 281 } 282 283 // Check if style already exists, if not create it 284 let style = document.getElementById(styleId); 285 if (!style) { 286 style = document.createElement('style'); 287 style.id = styleId; 288 document.head.appendChild(style); 289 } 290 291 // Update keyframes 292 style.textContent = '@keyframes marquee-scroll-' + styleId + ' { 0% { transform: translateX(0) translateZ(0); } 100% { transform: translateX(-' + animationDistancePx + 'px) translateZ(0); } }'; 293 294 // FORCE UPDATE - Remove animation completely
295 wrapper.style.animation = 'none'; 296 wrapper.style.animationName = 'none'; 297 wrapper.style.animationDuration = 'none'; 298 299 // Force reflow 300 void wrapper.offsetWidth; 301 302 // Re-apply animation with new speed 303 requestAnimationFrame(function() { 304 // Use CSS variable for speed 305 wrapper.style.setProperty('--marquee-speed', currentSpeed + 's'); 306 wrapper.style.animation = 'marquee-scroll-' + styleId + ' var(--marquee-speed, ' + currentSpeed + 's) linear infinite'; 307 wrapper.style.animationName = 'marquee-scroll-' + styleId; 308 wrapper.style.animationDuration = 'var(--marquee-speed, ' + currentSpeed + 's)'; 309 wrapper.style.animationTimingFunction = 'linear'; 310 wrapper.style.animationIterationCount = 'infinite'; 311 wrapper.style.animationFillMode = 'none'; 312 wrapper.style.animationPlayState = 'running'; 313 314 // Force another reflow to ensure it applies 315 void wrapper.offsetWidth; 316 }); 317 } 318 }; 319 320 // Wait for layout to calculate actual copy width after wrapper is in DOM 321 requestAnimationFrame(function() { 322 applyAnimation(); 323 }); 324 325 // Watch for changes in data-marquee-speed attribute - VERY AGGRESSIVE 326 if (typeof MutationObserver !== 'undefined') { 327 const speedObserver = new MutationObserver(function(mutations) { 328 let shouldUpdate = false; 329 mutations.forEach(function(mutation) { 330 // Check for attribute changes on element or wrapper 331 if (mutation.type === 'attributes') { 332 if (mutation.attributeName === 'data-marquee-speed') { 333 // Update wrapper if element changed 334 if (mutation.target === element || mutation.target === element.parentElement) { 335 const newSpeed = getMarqueeSpeed(element); 336 wrapper.setAttribute('data-marquee-speed', newSpeed); 337 shouldUpdate = true; 338 } 339 // Update if wrapper changed 340 if (mutation.target === wrapper) { 341 shouldUpdate = true; 342 } 343 } 344 } 345 // Also check for child changes (in case element is re-rendered) 346 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 347 shouldUpdate = true; 348 } 349 }); 350 351 if (shouldUpdate) { 352 // Get new speed and update 353 const newSpeed = getMarqueeSpeed(element); 354 updateSpeed(newSpeed); 355 } 356 }); 357 358 // Observe wrapper for speed changes 359 speedObserver.observe(wrapper, { 360 attributes: true, 361 attributeFilter: ['data-marquee-speed'] 362 }); 363 364 // Observe element for speed changes 365 speedObserver.observe(element, { 366 attributes: true, 367 attributeFilter: ['data-marquee-speed', 'class'], 368 childList: true, 369 subtree: false 370 }); 371 372 // Also observe parent element 373 if (element.parentElement) { 374 speedObserver.observe(element.parentElement, { 375 attributes: true, 376 attributeFilter: ['data-marquee-speed'] 377 }); 378 } 379 380 // Store observer reference for cleanup if needed 381 element.setAttribute('data-marquee-observer', 'active'); 382 } 383 384 // Expose updateSpeed function globally on wrapper for external access 385 wrapper.updateMarqueeSpeed = updateSpeed; 386 387 // AGGRESSIVE periodic check - check very frequently 388 // Read speed from element and update wrapper if changed 389 let lastKnownSpeed = speedValue; 390 const speedCheckInterval = setInterval(function() { 391 // Only check if element is still in DOM 392 if (!document.body.contains(element)) { 393 clearInterval(speedCheckInterval); 394 return; 395 } 396 397 // Always get fresh speed value from element 398 const currentSpeed = getMarqueeSpeed(element); 399 const wrapperSpeed = parseFloat(wrapper.getAttribute('data-marquee-speed')) || 0; 400 401 // If speed from element is different from wrapper, update 402 if (Math.abs(currentSpeed - wrapperSpeed) > 0.01) { 403 updateSpeed(currentSpeed); 404 } 405 }, 100); // Check every 100ms - EXTREMELY AGGRESSIVE 406 407 // Store interval ID for potential cleanup 408 wrapper.setAttribute('data-speed-check-interval', 'active'); 409 410 // Mark as initialized 411 element.dataset.marqueeInitialized = 'true'; 412 }); 413 }); 414 } 415 416 // Initialize on DOM ready 417 if (document.readyState === 'loading') { 418 document.addEventListener('DOMContentLoaded', initMarquee); 419 } else { 420 initMarquee(); 421 } 422 423 // Re-initialize for dynamically loaded content (e.g., AJAX) 424 if (typeof MutationObserver !== 'undefined') { 425 const observer = new MutationObserver(function(mutations) { 426 let shouldReinit = false;
427 mutations.forEach(function(mutation) { 428 if (mutation.addedNodes.length > 0) { 429 mutation.addedNodes.forEach(function(node) { 430 if (node.nodeType === 1) { // Element node 431 if (node.classList && node.classList.contains('gb-marquee-infinite-scroll')) { 432 shouldReinit = true; 433 } else if (node.querySelector && node.querySelector('.gb-marquee-infinite-scroll')) { 434 shouldReinit = true; 435 } 436 } 437 }); 438 } 439 }); 440 if (shouldReinit) { 441 initMarquee(); 442 } 443 }); 444 445 observer.observe(document.body, { 446 childList: true, 447 subtree: true 448 }); 449 } 450})(); 451
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.