UNPKG

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
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