PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.5.0
FrontBlocks for Gutenberg/GeneratePress v1.5.0
1.5.2 1.5.1 1.4.0 1.5.0 trunk 0.2.0 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 ci-artifacts
frontblocks / assets / headline / frontblocks-headline-marquee.js
frontblocks / assets / headline Last commit date
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