vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
336 lines • 10.9 kB
JavaScript
import { Module } from '../../base';
import { initVevet } from '../../global/initVevet';
import { addEventListener } from '../../utils';
import { Pointers } from '../Pointers';
import { SwipeCoords } from './Coords';
import { SwipeInertia } from './Inertia';
import { MUTABLE_PROPS, STATIC_PROPS } from './props';
import { SwipeStyles } from './Styles';
export * from './types';
export * from './global';
/**
* Manages swipe interactions:
* - Tracks movement and detects direction
* - Emits events on start, move, and end
* - Supports inertia-based movement
*
* Notes:
* - Does not transform elements, only computes coordinates.
* - Does not persist state after swipe completion.
*
* [Documentation](https://vevetjs.com/docs/Swipe)
*
* @group Components
*/
export class Swipe extends Module {
/**
* Returns default static properties.
*/
_getStatic() {
return Object.assign(Object.assign({}, super._getStatic()), STATIC_PROPS);
}
/**
* Returns default mutable properties.
*/
_getMutable() {
return Object.assign(Object.assign({}, super._getMutable()), MUTABLE_PROPS);
}
constructor(props, onCallbacks) {
super(props, onCallbacks);
/** If swiping has started */
this._isSwiping = false;
/** If swiping has been aborted */
this._isAborted = false;
const { container, thumb, buttons, pointers } = this.props;
this._coords = new SwipeCoords(this);
this._inertia = new SwipeInertia(this);
this._styles = new SwipeStyles(this);
// create pointers
this._pointers = new Pointers({
container: thumb || container,
buttons,
minPointers: pointers,
maxPointers: pointers,
enabled: this.props.enabled,
disableUserSelect: this.props.disableUserSelect,
});
// Set Events
this._setEvents();
}
/** Returns current swipe coordinates */
get coords() {
return this._coords.coords;
}
/** Event target element */
get container() {
return this.props.container;
}
/** Indicates if inertia is active */
get hasInertia() {
return this._inertia.has;
}
/** Indicates if a swipe is active */
get isSwiping() {
return this._isSwiping;
}
/** Handles property updates */
_handleProps(props) {
super._handleProps(props);
this._pointers.updateProps({ enabled: this.props.enabled });
this._styles.setInline();
}
/** Sets event listeners */
_setEvents() {
const { callbacks } = this;
const { container } = this.props;
this._pointers.on('start', () => this._handlePointersStart());
this._pointers.on('pointerdown', (data) => callbacks.emit('pointerdown', data));
this._pointers.on('pointermove', (data) => callbacks.emit('pointermove', data));
this._pointers.on('pointerup', (data) => callbacks.emit('pointerup', data));
const touchstart = addEventListener(container, 'touchstart', (event) => this._handleTouchStart(event), { passive: false });
this.onDestroy(() => touchstart());
}
/** Handles `touchstart` events */
_handleTouchStart(event) {
if (!this.props.enabled) {
return;
}
this.callbacks.emit('touchstart', event);
this._preventEdgeSwipe(event);
}
/** Prevents edge swipes if enabled */
_preventEdgeSwipe(event) {
const { props } = this;
if (!props.preventEdgeSwipe) {
return;
}
const threshold = props.edgeSwipeThreshold;
const x = event.targetTouches[0].pageX;
const shouldPrevent = x <= threshold || x >= initVevet().width - threshold;
if (event.cancelable && shouldPrevent) {
event.preventDefault();
this.callbacks.emit('preventEdgeSwipe', undefined);
}
}
/** Handles swipe start and tracking */
_handlePointersStart() {
const touchmove = addEventListener(window, 'touchmove', this._handleTouchMove.bind(this), { passive: false });
const mousemove = addEventListener(window, 'mousemove', this._handleMouseMove.bind(this));
const end = this._pointers.on('end', () => {
this._handleEnd();
end();
touchmove();
mousemove();
});
this.onDestroy(() => {
end();
touchmove();
mousemove();
});
}
/** Handles `touchmove` event */
_handleTouchMove(event) {
this.callbacks.emit('touchmove', event);
if (this._isSwiping && this.props.preventTouchMove && event.cancelable) {
event.preventDefault();
}
this._handleMove(event, 'touch');
}
/** Handles `mousemove` event */
_handleMouseMove(event) {
if (this.props.requireCtrlKey && !event.ctrlKey) {
return;
}
this.callbacks.emit('mousemove', event);
this._handleMove(event, 'mouse');
}
/** Handles move events */
_handleMove(event, type) {
const data = this._coords;
const matrix = data.decode(event);
if (this._isAborted) {
return;
}
// Save start coordinates
if (!this._startCoord) {
this._startCoord = Object.assign({}, matrix);
}
// Update start time
if (!this._startTime) {
this._startTime = +Date.now();
}
// check if can start
if (!this._isSwiping && !this._canStart(matrix, type)) {
return;
}
// start
if (!this._isSwiping) {
this._inertia.cancel();
this._isSwiping = true;
this._startCoord = Object.assign({}, matrix);
data.setStart(matrix);
this.callbacks.emit('start', this.coords);
this._styles.append();
}
// move
this._move(matrix);
}
/** Checks if swipe can start */
_canStart(matrix, type) {
const startCoord = this._startCoord;
const startTime = this._startTime;
if (!startCoord || !startTime) {
return false;
}
const { threshold, ratio, minTime, axis, willAbort } = this.props;
const speed = Math.abs(ratio);
const diff = {
x: matrix.x - startCoord.x,
y: matrix.y - startCoord.y,
};
// check threshold
const distX = diff.x / speed;
const distY = diff.y / speed;
const dist = Math.sqrt(Math.pow(distX, 2) + Math.pow(distY, 2));
if (dist < threshold) {
return false;
}
// check time
if (+new Date() - startTime < minTime) {
return false;
}
// check axis
if (axis) {
const rawAngle = (Math.atan2(Math.abs(diff.y), Math.abs(diff.x)) * 180) / Math.PI;
const normalizedAngle = axis === 'x' ? rawAngle : 90 - rawAngle;
if (normalizedAngle > 45) {
this._reset();
this._isAborted = true;
this.callbacks.emit('abort', undefined);
return false;
}
}
// Check if should abort
const shouldAbort = willAbort({
type,
matrix,
start: startCoord,
diff,
});
if (shouldAbort) {
this._reset();
this._isAborted = true;
this.callbacks.emit('abort', undefined);
return false;
}
return true;
}
/** Handles move events */
_move(matrix) {
const coords = this._coords;
// Update coords
coords.update(matrix);
// Update velocity
this._inertia.addVelocity(Object.assign(Object.assign({}, coords.current), { timestamp: coords.timestamp }));
// trigger callbacks
this.callbacks.emit('move', this.coords);
}
/** Handles swipe end */
_handleEnd() {
// reset
this._startTime = undefined;
this._isAborted = false;
// check swiping
if (!this.isSwiping) {
return;
}
// reset
this._reset();
// reset styles
this._styles.remove();
// calculate direction
const { x: diffX, y: diffY } = this._coords.diff;
const absDiffX = Math.abs(diffX);
const absDiffY = Math.abs(diffY);
const { directionThreshold } = this.props;
const endAxis = absDiffX > absDiffY ? 'x' : 'y';
if (endAxis === 'x' && absDiffX > directionThreshold) {
if (diffX > 0) {
this.callbacks.emit('toRight', undefined);
}
else if (diffX < 0) {
this.callbacks.emit('toLeft', undefined);
}
}
if (endAxis === 'y' && absDiffY > directionThreshold) {
if (diffY > 0) {
this.callbacks.emit('toBottom', undefined);
}
else if (diffY < 0) {
this.callbacks.emit('toTop', undefined);
}
}
// end callback
this.callbacks.emit('end', this.coords);
// modifiy last velocity time
this._inertia.updateLastTimestamp();
// end with inertia
if (this.props.inertia) {
this._releaseInertia();
}
}
/** Reset swipe states */
_reset() {
this._startCoord = undefined;
this._isSwiping = false;
}
/** Apply inertia-based movement */
_releaseInertia() {
const startMatrix = Object.assign({}, this.coords.current);
this._inertia.release((addMatrix) => {
this._move({
x: startMatrix.x + addMatrix.x,
y: startMatrix.y + addMatrix.y,
angle: startMatrix.angle + addMatrix.angle,
});
});
}
/** Cancel inertia */
cancelInertia() {
this._inertia.cancel();
}
/** Start coordinate */
get start() {
return this._coords.start;
}
/** Previous coordinate */
get prev() {
return this._coords.prev;
}
/** Current coordinate */
get current() {
return this._coords.current;
}
/** Difference between start and current coordinates */
get diff() {
return this._coords.diff;
}
/** Difference between start and previous coordinates */
get step() {
return this._coords.step;
}
/** Accumulated movement */
get accum() {
return this._coords.accum;
}
/**
* Destroys the component
*/
_destroy() {
super._destroy();
this._pointers.destroy();
this._inertia.destroy();
this._styles.remove();
}
}
//# sourceMappingURL=index.js.map