import { ISwipeCoords, ISwipeMatrix, Swipe } from '@/components/Swipe'; import { clamp } from '@/utils'; import { Snap } from '../..'; import { SnapLogic } from '../SnapLogic'; export class SnapSwipe extends SnapLogic { /** Swipe events */ private swipe: Swipe; /** Active index on swipe start */ private _startIndex: number; /** Swipe start time */ private _startTime: number; constructor(snap: Snap) { super(snap); this._startIndex = snap.activeIndex; this._startTime = 0; const swipe = new Swipe({ container: snap.eventsEmitter, inertia: false, velocityModifier: this._handleVelocityModifier.bind(this), inertiaDistanceThreshold: 5, ...this.swipeProps, }); this.swipe = swipe; this.addDestructor(() => swipe.destroy()); swipe.on('start', (data) => this._handleSwipeStart(data)); swipe.on('move', (data) => this._handleSwipeMove(data)); swipe.on('end', (data) => this._handleSwipeEnd(data)); swipe.on('inertiaStart', () => this._handleSwipeInertiaStart()); swipe.on('inertiaEnd', () => this._handleSwipeInertiaEnd()); swipe.on('inertiaFail', () => this._handleSwipeInertiaFail()); swipe.on('inertiaCancel', () => this._handleSwipeInertiaCancel()); // handle props change snap.on('props', () => swipe.updateProps(this.swipeProps), { protected: true, }); } /** Check if swiping in action */ get isSwiping() { return this.swipe.isSwiping; } /** Check if swipe has inertia */ get hasIntertia() { return this.swipe.hasInertia; } /** Snap track */ private get track() { // @ts-ignore // eslint-disable-next-line no-underscore-dangle return this.snap._track; } /** Axis name depending on swipe direction */ private get axis() { const { props, axis } = this.snap; return props.swipeAxis === 'auto' ? axis : props.swipeAxis; } /** Detect if swipe is short */ private get isShort() { const { props } = this.snap; if (!props.shortSwipes) { return false; } const diff = +new Date() - this._startTime; return diff <= props.shortSwipesDuration; } /** Checks if resistance is allowed */ get allowFriction() { return !this.isShort && this.snap.props.swipeFriction; } /** Swipe difference between start and current coordinates */ private get diff() { const { diff } = this.swipe; const initialDiff = diff[this.axis]; return initialDiff / Math.abs(this.snap.props.swipeSpeed); } /** Get swipe properties */ private get swipeProps() { const { props } = this.snap; return { enabled: props.swipe, grabCursor: props.grabCursor, minTime: props.swipeMinTime, threshold: props.swipeThreshold, axis: this.axis === 'angle' ? null : this.axis, relative: this.axis === 'angle', ratio: this.axis === 'angle' ? 1 : props.swipeSpeed, inertiaDuration: props.swipeInertiaDuration, inertiaRatio: props.swipeInertiaRatio, }; } /** Modify swipe velocity */ private _handleVelocityModifier(source: ISwipeMatrix) { const { snap, track } = this; const { coord: slideCoord, size: slideSize } = snap.activeSlide; // Simple freemode if (snap.props.freemode === true) { return source; } // Update target coordinate track.target = track.current; // Sticky freemode if (snap.props.freemode === 'sticky' && !snap.isSlideScrolling) { const virtualCoord = track.loopedCurrent - source[this.axis]; const magnet = snap.getNearestMagnet(virtualCoord); if (!magnet) { return source; } const newVelocity = track.loopedCurrent - virtualCoord - magnet.diff; return { ...source, [this.axis]: newVelocity, }; } // Freemode: false, when slides are scrolled const value = clamp( source[this.axis], -slideCoord, snap.containerSize - slideSize - slideCoord, ); const output = { ...source, [this.axis]: value }; return output; } /** Handles swipe `start` event */ private _handleSwipeStart(coords: ISwipeCoords) { const { snap } = this; this._startIndex = snap.activeIndex; this._startTime = +new Date(); // disable pointer events snap.eventsEmitter.style.pointerEvents = 'none'; // cancel sticky behavior if (snap.props.followSwipe) { snap.cancelTransition(); } // Emit callbacks snap.callbacks.emit('swipeStart', coords); } /** Handles swipe `move` event */ private _handleSwipeMove(coords: ISwipeCoords) { const { snap, track, swipe, axis } = this; const { props, callbacks } = snap; const { followSwipe: shouldFollow } = props; if (!shouldFollow && !snap.isSlideScrolling) { return; } // Normalize swipe delta let swipeDelta = coords.step[axis]; if (axis === 'angle') { const trackLength = snap.max - snap.min; swipeDelta = trackLength * (swipeDelta / 360); } const delta = swipeDelta * -1; // Update track target track.iterateTarget(delta); // Clamp target if inertia is animating if (swipe.hasInertia) { track.clampTarget(); } // Emit move callbacks callbacks.emit('swipe', coords); } /** Handles swipe `end` event */ private _handleSwipeEnd(coords: ISwipeCoords) { this._end(); // Enable pointer events this.snap.eventsEmitter.style.pointerEvents = ''; // Emit end callbacks this.snap.callbacks.emit('swipeEnd', coords); } /** Handles swipe inertia start */ private _handleSwipeInertiaStart() { this.snap.callbacks.emit('swipeInertiaStart', undefined); } /** Handles swipe inertia end */ private _handleSwipeInertiaEnd() { this.snap.callbacks.emit('swipeInertiaEnd', undefined); } /** Handles swipe inertia fail */ private _handleSwipeInertiaFail() { const { snap } = this; if (snap.props.freemode === 'sticky' && !snap.isSlideScrolling) { if (this.isShort) { this._endShort(); } else { snap.stick(); } } else { this.snap.render(); } this.snap.callbacks.emit('swipeInertiaFail', undefined); } /** Handles swipe inertia cancel */ private _handleSwipeInertiaCancel() { this.snap.callbacks.emit('swipeInertiaCancel', undefined); } /** End swipe action */ private _end() { const { snap, swipe, track } = this; const { props } = snap; // Handle freemode if (props.freemode) { swipe.updateProps({ inertia: true }); // Clamp & stick if out of bounds if ( !track.canLoop && (track.target < track.min || track.target > track.max) ) { swipe.cancelInertia(); snap.stick(); } // End short swipe if (this.isShort && props.freemode === 'sticky') { swipe.updateProps({ inertia: false }); swipe.cancelInertia(); this._endShort(); } return; } // Enable inertia if active slide is being scrolled if (snap.isSlideScrolling) { swipe.updateProps({ inertia: true }); return; } // Disable inertia swipe.updateProps({ inertia: false }); // Return if followSwipe is disabled if (!props.followSwipe) { this._endNoFollow(); return; } // Short swipe if (this.isShort) { this._endShort(); return; } // Or just stick to the nearest slide snap.stick(); } /** End short swipe */ private _endShort() { const { diff, snap } = this; const { props, activeSlide } = snap; if (Math.abs(diff) < props.shortSwipesThreshold) { snap.stick(); return; } const normalizedDiff = Math.sign(diff); if (this._startIndex !== snap.activeIndex) { if (normalizedDiff < 0 && activeSlide.progress > 0) { snap.next(); } else if (normalizedDiff > 0 && activeSlide.progress < 0) { snap.prev(); } else { snap.stick(); } return; } if (normalizedDiff < 0) { snap.next(); } else { snap.prev(); } } /** End action when `followSwipe` is disabled */ private _endNoFollow() { const { diff, snap } = this; if (Math.abs(diff) < 20) { snap.stick(); return; } if (diff < 0) { snap.next(); } else { snap.prev(); } } }