| 1 |
/** |
| 2 |
* Auto Scrolling Text — keeps the marquee running without a gap. |
| 3 |
* |
| 4 |
* The track scrolls left by exactly the width of one copy of the content. For |
| 5 |
* that to look continuous, the track has to hold enough copies that the ones |
| 6 |
* still to the right cover the visible strip at the moment the animation |
| 7 |
* resets. One copy alone leaves a growing empty space and then jumps back, |
| 8 |
* which is what the widget used to do. |
| 9 |
* |
| 10 |
* Rebuilding the copies forces layout and paint on a wide element, so it is |
| 11 |
* done only when the number of copies actually has to change — otherwise a |
| 12 |
* resize (which fires constantly on mobile as the address bar hides) would |
| 13 |
* show up as a stutter in the animation. |
| 14 |
*/ |
| 15 |
(function () { |
| 16 |
'use strict'; |
| 17 |
|
| 18 |
var GROUP = 'king-addons-auto-scrolling-text-group'; |
| 19 |
|
| 20 |
function requiredCopies(groupWidth, visibleWidth) { |
| 21 |
// One copy scrolls out of view; the rest have to cover the strip. |
| 22 |
return Math.max(2, Math.ceil(visibleWidth / groupWidth) + 1); |
| 23 |
} |
| 24 |
|
| 25 |
function fill(track) { |
| 26 |
var strip = track.parentElement; |
| 27 |
if (!strip) { |
| 28 |
return; |
| 29 |
} |
| 30 |
|
| 31 |
var original = track.querySelector('.' + GROUP); |
| 32 |
if (!original) { |
| 33 |
return; |
| 34 |
} |
| 35 |
|
| 36 |
// Each copy is its own flex item, so the first one can be measured |
| 37 |
// without disturbing the copies already in place. |
| 38 |
var groupWidth = original.getBoundingClientRect().width; |
| 39 |
var visibleWidth = strip.getBoundingClientRect().width; |
| 40 |
if (groupWidth <= 0 || visibleWidth <= 0) { |
| 41 |
return; |
| 42 |
} |
| 43 |
|
| 44 |
var wanted = requiredCopies(groupWidth, visibleWidth); |
| 45 |
var current = track.querySelectorAll('.' + GROUP).length; |
| 46 |
if (current === wanted) { |
| 47 |
return; // nothing to do: leave the animation alone |
| 48 |
} |
| 49 |
|
| 50 |
if (current > wanted) { |
| 51 |
var extra = track.querySelectorAll('.' + GROUP); |
| 52 |
for (var i = extra.length - 1; i >= wanted; i--) { |
| 53 |
extra[i].remove(); |
| 54 |
} |
| 55 |
} else { |
| 56 |
var fragment = document.createDocumentFragment(); |
| 57 |
for (var c = current; c < wanted; c++) { |
| 58 |
var clone = original.cloneNode(true); |
| 59 |
// A screen reader should hear the message once, not once per copy. |
| 60 |
clone.setAttribute('aria-hidden', 'true'); |
| 61 |
fragment.appendChild(clone); |
| 62 |
} |
| 63 |
track.appendChild(fragment); |
| 64 |
} |
| 65 |
|
| 66 |
// Shifting by one copy lands the next copy exactly where this one began. |
| 67 |
track.style.setProperty('--king-addons-marquee-shift', (-100 / wanted) + '%'); |
| 68 |
} |
| 69 |
|
| 70 |
function observe(track) { |
| 71 |
if (track.kingAddonsMarqueeWatched) { |
| 72 |
return; |
| 73 |
} |
| 74 |
track.kingAddonsMarqueeWatched = true; |
| 75 |
|
| 76 |
var strip = track.parentElement; |
| 77 |
if (strip && window.ResizeObserver) { |
| 78 |
// Reacts to the strip actually changing width, unlike window resize, |
| 79 |
// which also fires on mobile scroll and on every orientation nudge. |
| 80 |
new ResizeObserver(function () { fill(track); }).observe(strip); |
| 81 |
} |
| 82 |
} |
| 83 |
|
| 84 |
function watchAll(root) { |
| 85 |
var scope = root && root.querySelectorAll ? root : document; |
| 86 |
Array.prototype.forEach.call( |
| 87 |
scope.querySelectorAll('.king-addons-auto-scrolling-text-wrapper'), |
| 88 |
function (track) { |
| 89 |
fill(track); // cheap: returns straight away when nothing changed |
| 90 |
observe(track); // one-time |
| 91 |
} |
| 92 |
); |
| 93 |
} |
| 94 |
|
| 95 |
function onReady() { |
| 96 |
watchAll(document); |
| 97 |
|
| 98 |
// Safety net for anything ResizeObserver does not catch (and for browsers |
| 99 |
// without it). fill() measures and returns when nothing has to change, |
| 100 |
// so this costs a measurement and never touches the DOM on its own. |
| 101 |
var timer = null; |
| 102 |
window.addEventListener('resize', function () { |
| 103 |
clearTimeout(timer); |
| 104 |
timer = setTimeout(function () { watchAll(document); }, 200); |
| 105 |
}); |
| 106 |
|
| 107 |
// Web fonts land after first paint and change how wide a copy is. |
| 108 |
if (document.fonts && document.fonts.ready) { |
| 109 |
document.fonts.ready.then(function () { watchAll(document); }); |
| 110 |
} |
| 111 |
} |
| 112 |
|
| 113 |
if (document.readyState === 'complete' || document.readyState === 'interactive') { |
| 114 |
onReady(); |
| 115 |
} else { |
| 116 |
document.addEventListener('DOMContentLoaded', onReady); |
| 117 |
} |
| 118 |
|
| 119 |
/** |
| 120 |
* Re-measure when Elementor re-renders the widget, which it does on every |
| 121 |
* edit in the editor. Elementor fires this through jQuery, so it has to be |
| 122 |
* bound with jQuery — a native listener never hears it. |
| 123 |
*/ |
| 124 |
function bindElementor() { |
| 125 |
if (!window.elementorFrontend || !elementorFrontend.hooks) { |
| 126 |
return false; |
| 127 |
} |
| 128 |
elementorFrontend.hooks.addAction( |
| 129 |
'frontend/element_ready/king-addons-auto-scrolling-text.default', |
| 130 |
function ($scope) { watchAll($scope && $scope[0] ? $scope[0] : document); } |
| 131 |
); |
| 132 |
return true; |
| 133 |
} |
| 134 |
|
| 135 |
if (window.jQuery) { |
| 136 |
// Covers both orders: Elementor still to start, or already started. |
| 137 |
if (!bindElementor()) { |
| 138 |
jQuery(window).on('elementor/frontend/init', bindElementor); |
| 139 |
} |
| 140 |
} |
| 141 |
}()); |
| 142 |
|