vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
483 lines • 17 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;
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { Module } from '../../base';
import { isString } from '../../internal/isString';
import { isUndefined } from '../../internal/isUndefined';
import { noopIfDestroyed } from '../../internal/noopIfDestroyed';
import { onResize, loop, damp, toPixels, closest, inRange, } from '../../utils';
import { SnapIdle } from './logic/Idle';
import { SnapInterval } from './logic/Interval';
import { SnapKeyboard } from './logic/Keyboard';
import { SnapSlide } from './logic/Slide';
import { SnapSwipe } from './logic/Swipe';
import { SnapTrack } from './logic/Track';
import { SnapWheel } from './logic/Wheel';
import { LERP_APPROXIMATION, MUTABLE_PROPS, STATIC_PROPS } from './props';
export * from './types';
export * from './logic/Slide';
/**
* Snap/Carousel handler.
* This class manages sliding progress with options like swipe, wheel interactions, and smooth transitions.
*
* Please not that the class does not apply any styles to the slides, it only handles the logic.
*
* [Documentation](https://vevetjs.com/docs/Snap)
*
* @group Components
*/
export class Snap extends Module {
/**
* Returns the default static properties.
*/
_getStatic() {
return Object.assign(Object.assign({}, super._getStatic()), STATIC_PROPS);
}
/**
* Returns the default mutable properties.
*/
_getMutable() {
return Object.assign(Object.assign({}, super._getMutable()), MUTABLE_PROPS);
}
constructor(props, onCallbacks) {
super(props, onCallbacks);
/** Container size */
this._containerSize = 0;
/** All slides */
this._slides = [];
/** Scrollable slides (which size is larger than the container) */
this._scrollableSlides = [];
const { container, activeIndex } = this.props;
// set vars
this._activeIndex = activeIndex;
// add resize event
this._resizer = onResize({
element: container,
viewportTarget: 'width',
callback: () => this._handleResize(),
name: this.name,
});
// initial resize
this._resizer.debounceResize();
// fetch slides
this._fetchSlides();
// add wheel listener
new SnapWheel(this);
// add swipe
this._swipe = new SnapSwipe(this);
// add track
this._track = new SnapTrack(this);
// add keyboard
new SnapKeyboard(this);
// add interval
new SnapInterval(this);
// add idle logic
this._idle = new SnapIdle(this);
}
/** Handles properties change */
_handleProps(props) {
// attach slides
if ('slides' in props) {
this._fetchSlides();
}
// resize immediately
this._resizer.resize();
// update props
super._handleProps(props);
}
/** Get container */
get container() {
return this.props.container;
}
/** Get events emitter */
get eventsEmitter() {
var _a;
return (_a = this.props.eventsEmitter) !== null && _a !== void 0 ? _a : this.container;
}
/** Container size depending on direction (width or height) */
get containerSize() {
const { containerSize } = this.props;
if (containerSize === 'auto') {
return this._containerSize;
}
return toPixels(containerSize);
}
/**
* Container size depending on direction (width or height)
* @deprecated
*/
get domSize() {
return this.containerSize;
}
/** All slides */
get slides() {
return this._slides;
}
/** Scrollable slides (which size is larger than the container) */
get scrollableSlides() {
return this._scrollableSlides;
}
/** Active slide index */
get activeIndex() {
return this._activeIndex;
}
/** Active slide */
get activeSlide() {
return this.slides[this._activeIndex];
}
get isEmpty() {
return this.slides.length === 0;
}
/** Get axis name depending on direction */
get axis() {
return this.props.direction === 'horizontal' ? 'x' : 'y';
}
/** If transition in progress */
get isTransitioning() {
return this._track.isTransitioning;
}
/** If swipe in progress */
get isSwiping() {
return this._swipe.isSwiping;
}
/** If swipe has inertia */
get hasInteria() {
return this._swipe.hasIntertia;
}
/** If track values are interpolating */
get isInterpolating() {
const track = this._track;
const diff = Math.abs(track.target - track.current);
return diff > LERP_APPROXIMATION;
}
/** Gets the interpolation influence */
get influence() {
return this._track.influence;
}
/** Gets the current track value. */
get current() {
return this._track.current;
}
/** Gets the target track value. */
get target() {
return this._track.target;
}
/** Detect if can loop */
get canLoop() {
return this._track.canLoop;
}
/** Get looped current value */
get loopedCurrent() {
return this._track.loopedCurrent;
}
/** Get loop count */
get loopCount() {
return this._track.loopCount;
}
/** Sets track to current & target value instantly */
set(value) {
this._track.set(value);
}
/** Loop a coordinate if can loop */
loopCoord(coord) {
return this._track.loopCoord(coord);
}
/** Get minimum track value */
get min() {
return this._track.min;
}
/** Get maximum track value */
get max() {
return this._track.max;
}
/** Get track progress. From 0 to 1 if not loop. From -Infinity to Infinity if loop */
get progress() {
return this.current / this.max;
}
/** If the start has been reached */
get isStart() {
return this._track.isStart;
}
/** If the end has been reached */
get isEnd() {
return this._track.isEnd;
}
/** Clamp target value between min and max values */
clampTarget() {
this._track.clampTarget();
}
/** Iterate track target value */
iterateTarget(delta) {
this._track.iterateTarget(delta);
}
/** Set track target value */
setTarget(value) {
this._track.setTarget(value);
}
/** Cancel slide transition */
cancelTransition() {
this._track.cancelTransition();
}
/** Check if the active slide is larger than the container and is being scrolled */
get isSlideScrolling() {
const { containerSize } = this;
return this.scrollableSlides.some(({ size, coord }) => inRange(coord, containerSize - size, 0));
}
/** Get first slide size */
get firstSlideSize() {
return this.slides[0].size;
}
/** If the scene is idle: not swiping, not interpolating, not transitioning */
get isIdle() {
return this._idle.isIdle;
}
/** Update slides list and attach them */
_fetchSlides() {
const { props } = this;
this._slides.forEach((slide) => slide.$_detach());
const rawChildren = props.slides
? props.slides
: Array.from(props.container.children);
const children = rawChildren.filter((slide) => {
if (slide instanceof HTMLElement &&
slide.hasAttribute('data-scrollbar')) {
return false;
}
return true;
});
this._slides = children.map((item) => {
if (item instanceof SnapSlide) {
return item;
}
return new SnapSlide(item);
});
this._slides.forEach((slide, index) => slide.$_attach(this, index));
}
/** Request resize (handled with debounce timeout) */
resize(isManual = true) {
if (isManual) {
this._resizer.resize();
}
else {
this._resizer.debounceResize();
}
}
/** Resize the scene and reflow */
_handleResize() {
const { container } = this.props;
// cancel sticky behavior
this._track.cancelTransition();
// update container size
this._containerSize =
this.axis === 'x' ? container.offsetWidth : container.offsetHeight;
// reflow
this._reflow();
// emit callbacks
this.callbacks.emit('resize', undefined);
}
/** Reflow: update static values of slides */
_reflow() {
const { slides, props } = this;
if (this.isEmpty) {
return;
}
// Reset scrollable slides
this._scrollableSlides = [];
// Calculate static values
slides.reduce((prev, slide) => {
slide.$_setStaticCoord(prev);
if (slide.size > this.containerSize) {
this._scrollableSlides.push(slide);
}
return prev + slide.size + toPixels(props.gap);
}, 0);
// Reset to active slide
const slide = slides.find(({ index }) => index === this.activeIndex);
if (props.stickOnResize && slide) {
this._track.clampTarget();
this._track.set(slide.magnets[0]);
}
// Emit callbacks
this.callbacks.emit('reflow', undefined);
// Render after resize
this.render();
}
/** Render slides */
render(frameDuration = 0) {
if (this.isEmpty) {
return;
}
const { _swipe: swipe, _track: track, props } = this;
// Update values
this._updateSlidesCoords();
this._updateSlideProgress();
// Get magnet after slide coordinates are updated
const { magnet } = this;
// Active index change
if (magnet &&
magnet.slide.index !== this._activeIndex &&
(isUndefined(this.$_targetIndex) ||
magnet.slide.index === this.$_targetIndex)) {
this._activeIndex = magnet.slide.index;
this.$_targetIndex = undefined;
this.callbacks.emit('activeSlide', this.activeSlide);
}
// Check if friction is allowed
const hasFriction = (swipe.isSwiping && swipe.allowFriction) || !swipe.isSwiping;
// Apply friction
if (magnet &&
hasFriction &&
frameDuration > 0 &&
props.friction >= 0 &&
!this.isSlideScrolling &&
!props.freemode) {
track.target = damp(track.target, track.current + magnet.diff, props.friction * props.lerp, frameDuration, 0.000001);
}
// Render slides
this.slides.forEach((slide) => slide.$_render());
// Emit Calbacks
this.callbacks.emit('update', undefined);
}
/** Update slides values */
_updateSlidesCoords() {
const { slides, props, containerSize, firstSlideSize } = this;
const offset = props.centered ? containerSize / 2 - firstSlideSize / 2 : 0;
slides.forEach((slide) => slide.$_updateCoords(offset));
}
/** Update slides progress */
_updateSlideProgress() {
this.slides.forEach((slide) => slide.$_updateProgress());
}
/** Get nearest magnet */
get magnet() {
const current = this._track.loopedCurrent;
return this.getNearestMagnet(current);
}
/** Get nearest magnet to the current position */
getNearestMagnet(coord) {
const magnets = this.slides.flatMap((slide) => slide.magnets.map((magnet) => ({ slide, magnet, index: slide.index })));
if (magnets.length === 0) {
return undefined;
}
const closestMagnet = magnets.reduce((p, c) => Math.abs(c.magnet - coord) < Math.abs(p.magnet - coord) ? c : p);
return Object.assign(Object.assign({}, closestMagnet), { diff: closestMagnet.magnet - coord });
}
/** Stick to the nearest magnet */
stick() {
const { magnet, isSlideScrolling } = this;
if (isSlideScrolling || !magnet) {
return;
}
this.toCoord(this._track.current + magnet.diff);
}
/** Go to a definite coordinate */
toCoord(coordinate, options) {
return this._track.toCoord(coordinate, options);
}
/** Go to a slide by index */
toSlide(targetIndex, _a = {}) {
var _b, _c;
var { direction = null } = _a, options = __rest(_a, ["direction"]);
const { isEmpty, activeIndex, slides, _track: track, props } = this;
const { current, max, loopCount } = track;
if (isEmpty || this.isDestroyed) {
return false;
}
const index = loop(targetIndex, 0, this.slides.length);
// Stick if the same slide
if (index === activeIndex) {
this.stick();
return false;
}
this.$_targetIndex = index;
const slideMagnets = slides[index].magnets;
let targetStaticMagnet = slideMagnets[0];
if (props.centered) {
if (direction === 'prev') {
targetStaticMagnet = (_b = slideMagnets[2]) !== null && _b !== void 0 ? _b : slideMagnets[0];
}
else if (direction === 'next') {
targetStaticMagnet = (_c = slideMagnets[1]) !== null && _c !== void 0 ? _c : slideMagnets[0];
}
}
else {
targetStaticMagnet =
direction === 'prev'
? slideMagnets[slideMagnets.length - 1]
: targetStaticMagnet;
}
// Use static magnet when not looping
if (!props.loop) {
return this.toCoord(targetStaticMagnet, options);
}
// Or calculate closest magnet
const targetMagnet = targetStaticMagnet + loopCount * max;
const targetMagnetMin = targetMagnet - max;
const targetMagnetMax = targetMagnet + max;
const allMagnets = [targetMagnetMin, targetMagnet, targetMagnetMax];
if (isString(direction)) {
const magnets = allMagnets.filter((magnet) => direction === 'next' ? magnet >= current : magnet <= current);
const magnet = closest(current, magnets);
return this.toCoord(magnet, options);
}
const magnet = closest(current, allMagnets);
return this.toCoord(magnet, options);
}
/** Go to next slide */
next(_a = {}) {
var { skip = this.props.slidesToScroll } = _a, options = __rest(_a, ["skip"]);
const { props, slides, activeIndex } = this;
const { length } = slides;
let index = loop(activeIndex + skip, 0, length);
if (!props.loop) {
index = props.rewind
? loop(activeIndex + skip, 0, length)
: Math.min(activeIndex + skip, length - 1);
}
return this.toSlide(index, Object.assign(Object.assign({}, options), { direction: 'next' }));
}
/** Go to previous slide */
prev(_a = {}) {
var { skip = this.props.slidesToScroll } = _a, options = __rest(_a, ["skip"]);
const { props, slides, activeIndex } = this;
let index = loop(activeIndex - skip, 0, slides.length);
if (!props.loop) {
index = props.rewind
? loop(activeIndex - skip, 0, slides.length)
: Math.max(activeIndex - skip, 0);
}
return this.toSlide(index, Object.assign(Object.assign({}, options), { direction: 'prev' }));
}
/**
* Destroys the component and clears all timeouts and resources.
*/
_destroy() {
super._destroy();
this._resizer.remove();
this._slides.forEach((slide) => slide.$_detach());
}
}
__decorate([
noopIfDestroyed
], Snap.prototype, "resize", null);
__decorate([
noopIfDestroyed
], Snap.prototype, "render", null);
__decorate([
noopIfDestroyed
], Snap.prototype, "toCoord", null);
//# sourceMappingURL=index.js.map