frontblocks-headline-marquee.js
6 months ago
frontblocks-headline-option.jsx
2 months ago
frontblocks-headline.css
6 months ago
frontblocks-headline.js
2 months ago
frontblocks-headline-marquee.js
452 lines
| 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 | |
| 452 |