web/wp-content/themes/mozilla-builders/static/js/plugins/marquee.js (54 lines of code) (raw):
/** @type {import('alpinejs').PluginCallback} */
export function marquee(Alpine) {
Alpine.directive('marquee', (el, { value, expression }, { evaluate, cleanup }) => {
if (!value) {
const options = evaluate(expression);
const unregister = registerRoot(Alpine, el, options.speed ?? 1);
cleanup(() => unregister());
} else if (value === 'track') {
const unregister = registerTrack(Alpine, el);
cleanup(() => unregister());
}
});
}
/**
* Registers the root element with speed and spaceX options
*
* @param {import('alpinejs').Alpine} Alpine
* @param {HTMLElement} el
* @param {number} speed
*/
function registerRoot(Alpine, el, speed) {
return Alpine.bind(el, {
'x-data': () => ({ __root: el, speed }),
});
}
/**
* @param {Function} func
* @param {number} timeout
*/
function debounce(func, timeout = 100) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}
/**
* Resizes the marquee to fit the screen
*
* @param {HTMLElement} rootElement
* @param {HTMLElement} el
* @param {HTMLElement} originalElement
*/
async function resize(rootElement, el, originalElement) {
// Reset to original number of elements
el.innerHTML = originalElement.innerHTML;
// Keep cloning elements until marquee starts to overflow
do {
originalElement.childNodes.forEach(child => el.appendChild(child.cloneNode(true)));
} while (el.scrollWidth < rootElement.clientWidth);
originalElement.childNodes.forEach(child => el.appendChild(child.cloneNode(true)));
}
/**
* Registers the track element to animate the marquee.
*
* @param {import('alpinejs').Alpine} Alpine
* @param {HTMLElement} el
*/
function registerTrack(Alpine, el) {
const options = Alpine.$data(el);
const { speed, __root: rootElement } = options;
// Store the original element so we can restore it on screen resize later
const originalElement = el.cloneNode(true);
const originalWidth = el.scrollWidth;
// Required for the marquee scroll animation
// to loop smoothly without jumping
rootElement.style.setProperty('--marquee-width', `${originalWidth}px`);
rootElement.style.setProperty('--marquee-time', `${((1 / speed) * originalWidth) / 100}s`);
const onResize = () => {
resize(rootElement, el, originalElement);
};
onResize();
const listener = debounce(onResize);
window.addEventListener('resize', listener);
rootElement.classList.add('loaded');
return () => {
rootElement.classList.remove('loaded');
rootElement.style.removeProperty('--marquee-width');
rootElement.style.removeProperty('--marquee-time');
window.removeEventListener('resize', listener);
};
}