vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
289 lines • 10.7 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Module } from '../../base/Module';
import { initVevet } from '../../global/initVevet';
import { isFiniteNumber } from '../../internal/isFiniteNumber';
import { noopIfDestroyed } from '../../internal/noopIfDestroyed';
import { getTextDirection } from '../../internal/textDirection';
import { toPixels } from '../../utils';
import { onResize, addEventListener } from '../../utils/listeners';
import { loop } from '../../utils/math';
import { Raf } from '../Raf';
import { MarqueeNodes } from './Nodes';
import { MUTABLE_PROPS, STATIC_PROPS } from './props';
import { appleMarqueeContainerStyles, removeMarqueeContainerStyles, } from './styles';
export * from './types';
/**
* A custom marquee component that smoothly scrolls its child elements.
*
* This component is designed to loop elements horizontally within a container,
* with support for customization such as speed, gap, pause on hover, and more.
*
* [Documentation](https://vevetjs.com/docs/Marquee)
*
* @group Components
*/
export class Marquee extends Module {
/** Get default static properties. */
_getStatic() {
return Object.assign(Object.assign({}, super._getStatic()), STATIC_PROPS);
}
/** Get default mutable properties. */
_getMutable() {
return Object.assign(Object.assign({}, super._getMutable()), MUTABLE_PROPS);
}
constructor(props, onCallbacks) {
super(props, onCallbacks);
/** Current container size (width or height depending on direction) */
this._containerSize = 0;
/** Array of sizes of each child element */
this._sizes = [];
/** Total size of all elements in the marquee */
this._totalSize = 0;
/** The current marquee coordinate. */
this._coord = 0;
/** Detects if the container is RTL */
this._isRtl = false;
const { container, direction } = this.props;
const { isVertical } = this;
if (!container) {
throw new Error('Marquee container is not defined');
}
// Update direction
const isRtl = getTextDirection(container) === 'rtl' && direction === 'horizontal';
this._isRtl = isRtl;
// Apply base styles to the container
appleMarqueeContainerStyles({ container, isVertical, isRtl });
// Create nodes manager
this._nodes = new MarqueeNodes(this);
// Setup elements in the marquee
this._setup();
// Create animation frame
this._raf = new Raf({ enabled: this.props.enabled, fpsRecalcFrames: 1 });
// Set events
this._setEvents();
}
/** Total size of all elements in the marquee (width or height depending on direction) */
get totalSize() {
return this._totalSize;
}
/**
* Total width of all elements in the marquee
* @deprecated Use `totalSize` instead
*/
get totalWidth() {
return this.totalSize;
}
/** The current marquee coordinate. */
get coord() {
return this._coord;
}
set coord(value) {
this._coord = value;
this.render(0);
}
/**
* The current coordinate of the marquee.
* @deprecated Use `coord` instead
*/
get x() {
return this.coord;
}
set x(value) {
this.coord = value;
}
/** Check if the marquee is vertical */
get isVertical() {
return this.props.direction === 'vertical';
}
/** Marquee gap */
get gap() {
return Math.max(toPixels(this.props.gap), 0);
}
/** Handles property changes */
_handleProps(props) {
super._handleProps(props);
if (this.props.enabled) {
this._raf.play();
}
else {
this._raf.pause();
}
// Rerender the elements
this.resize();
this.render(0);
}
/** Set marquee events */
_setEvents() {
const { container } = this.props;
this._raf.on('frame', () => {
const { props } = this;
const factor = props.adjustSpeed ? this._raf.fpsFactor : 1;
const speed = toPixels(props.speed);
this._render(speed * factor);
});
// Pause on hover
const mouseenter = addEventListener(container, 'mouseenter', () => {
if (this.props.pauseOnHover) {
this._raf.pause();
}
});
// Resume on mouse leave
const mouseleave = addEventListener(container, 'mouseleave', () => {
if (this.props.enabled) {
this._raf.play();
}
});
// Intersection observer
const intersection = new IntersectionObserver(this._handleIntersection.bind(this), { root: null });
intersection.observe(container);
this.onDestroy(() => {
mouseenter();
mouseleave();
intersection.disconnect();
});
}
/** Initializes the marquee setup, including resizing and cloning elements */
_setup() {
var _a;
(_a = this._lastSetup) === null || _a === void 0 ? void 0 : _a.call(this);
if (this.isDestroyed) {
return;
}
const { container, resizeDebounce } = this.props;
// Process nodes
this._nodes.save();
this._nodes.wrap();
this._nodes.applyStyles();
// initial resize
this.resize();
// Resize on page load
const onPageLoad = initVevet().onLoad(() => this.resize());
// Handle resizing
const resizeHandler = onResize({
callback: () => this.resize(),
element: [container, ...this._nodes.elements],
viewportTarget: 'width',
resizeDebounce,
name: this.name,
});
// Setup cleanup function
this._lastSetup = () => {
onPageLoad();
resizeHandler.remove();
};
}
/** Resizes the marquee, recalculating element positions and cloning if necessary. */
resize() {
const { props, isVertical, gap } = this;
const { container } = props;
// Update container width
const containerSize = isVertical
? container.offsetHeight
: container.offsetWidth;
this._containerSize = containerSize;
// Update element sizes
this._sizes = this._nodes.elements.map((el) => (isVertical ? el.offsetHeight : el.offsetWidth) + gap);
this._totalSize = this._sizes.reduce((a, b) => a + b, 0);
// Determine how many times to duplicate elements
const maxSize = Math.max(...this._sizes);
const copyTimes = Math.ceil((containerSize + maxSize) / this._totalSize);
// update total size
this._totalSize = Math.max(this._totalSize, containerSize + maxSize);
// Clone elements if necessary
if (props.cloneNodes && isFiniteNumber(copyTimes) && copyTimes > 1) {
this._nodes.cloneAll(copyTimes - 1);
this.resize();
}
// Trigger resize callbacks
this.callbacks.emit('resize', undefined);
// Rerender the marquee
setTimeout(() => this.render(0), 0);
}
/** Renders the marquee, adjusting element positions. */
render(step) {
this._render(step);
}
/**
* Renders the marquee, calculating element positions based on the provided speed.
*/
_render(stepProp = this.props.speed) {
if (this.isDestroyed) {
return;
}
const { isVertical, props, gap } = this;
const { elements } = this._nodes;
// Calculate step
const rawStep = this._isRtl ? -stepProp : stepProp;
const step = toPixels(rawStep);
if (!isFiniteNumber(step)) {
return;
}
// Update animation time
this._coord -= step;
// Calculate current position of the elements
const centerCoord = this._containerSize * 0.5 + this._sizes[0] / 2 - gap;
const position = this._coord + (props.centered ? centerCoord : 0);
// Update each element's position
let prevStaticCoord = 0;
for (let index = 0; index < elements.length; index += 1) {
const element = elements[index];
const elementSize = this._sizes[index];
const { style } = element;
const coord = loop(position + prevStaticCoord, -elementSize, this._totalSize - elementSize);
// Apply transformations to position the element
if (isVertical) {
const x = style.position === 'relative' ? '0' : '-50%';
style.transform = `translate(${x}, ${coord}px)`;
}
else {
const y = style.position === 'relative' ? '0' : '-50%';
style.transform = `translate(${coord}px, ${y})`;
}
prevStaticCoord += elementSize;
}
// Trigger render callbacks
this.callbacks.emit('render', undefined);
}
/** Handle intersection observer */
_handleIntersection(entries) {
if (!this.props.pauseOnOut) {
return;
}
entries.forEach((entry) => {
if (entry.isIntersecting && this.props.enabled) {
this._raf.play();
}
else {
this._raf.pause();
}
});
}
/** Destroys the instance and cleans up resources */
_destroy() {
var _a;
const { container } = this.props;
super._destroy();
this._raf.destroy();
(_a = this._lastSetup) === null || _a === void 0 ? void 0 : _a.call(this);
// Remove all children and restore the initial nodes
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// Restore the initial nodes
this._nodes.destroy();
// Restore container style
removeMarqueeContainerStyles(container);
}
}
__decorate([
noopIfDestroyed
], Marquee.prototype, "resize", null);
__decorate([
noopIfDestroyed
], Marquee.prototype, "render", null);
//# sourceMappingURL=index.js.map