VulpesOS/gaia/apps/calendar/js/views/pan.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

231 lines
6.4 KiB
JavaScript
Executable file

// this module is responsible for the touch/panning of MultiDay views
define(function(require, exports, module) {
'use strict';
var EventEmitter2 = require('ext/eventemitter2');
var clamp = require('utils/mout').clamp;
var lerp = require('utils/mout').lerp;
var norm = require('utils/mout').norm;
function Pan(options) {
EventEmitter2.call(this);
this.eventTarget = options.eventTarget;
this.targets = options.targets;
this.overflows = options.overflows || [];
this._gridSize = Math.max(options.gridSize || 0, 1);
this._visibleCells = Math.max(options.visibleCells || 0, 1);
this._startMouseX = this._startMouseY = 0;
this._isVertical = false;
this._startTime = 0;
this._touchStart = 0;
this._dx = 0;
// _lockedAxis is used to control if we detected if the movement is
// vertical/horizontal, very important for ignoring clicks and also to be
// able to set a threshold for the axis detection
this._lockedAxis = false;
this._onTouchStart = this._onTouchStart.bind(this);
this._onTouchMove = this._onTouchMove.bind(this);
this._onTouchEnd = this._onTouchEnd.bind(this);
this._tick = this._tick.bind(this);
this._setBaseValues = this._setBaseValues.bind(this);
this._onTweenEnd = null;
}
module.exports = Pan;
Pan.prototype = {
__proto__: EventEmitter2.prototype,
TRANSITION_DURATION: 800,
setup: function() {
var element = this.eventTarget;
element.addEventListener('touchstart', this._onTouchStart);
element.addEventListener('touchmove', this._onTouchMove);
element.addEventListener('touchend', this._onTouchEnd);
element.addEventListener('touchcancel', this._onTouchEnd);
window.addEventListener('localized', this._setBaseValues);
this._setBaseValues();
},
_setBaseValues: function() {
var delta = this._gridSize * this._visibleCells;
if (document.documentElement.dir === 'rtl') {
this._dir = -1;
this._minX = 0;
this._maxX = delta * 2;
} else {
this._dir = 1;
this._minX = delta * -2;
this._maxX = 0;
}
this._origX = this._startX = this._curX = this._destX = delta * -this._dir;
this._set(this._origX);
},
_onTouchStart: function(evt) {
this._startMouseX = evt.touches[0].clientX;
this._startMouseY = evt.touches[0].clientY;
this._isVertical = false;
this._lockedAxis = false;
this._touchStart = Date.now();
// we need to reset the tween callback because we should only call it
// once and only if user did not trigger a new touch
this._onTweenEnd = null;
},
_onTouchMove: function(evt) {
if (this._isVertical) {
return;
}
var dx = this._startMouseX - evt.touches[0].clientX;
var dy = this._startMouseY - evt.touches[0].clientY;
this._dx = dx;
if (!this._lockedAxis) {
var adx = Math.abs(dx);
var ady = Math.abs(dy);
// we wait until we are sure movement is horizontal before we do anything.
// if absolute difference between x/y movement is over a threshold (10px)
// we assume drag follows a single axis.
if (Math.abs(adx - ady) < 10) {
return;
}
this._isVertical = adx < ady;
this._lockedAxis = true;
this.emit('start');
if (this._isVertical) {
return;
}
// we should only lock scroll once and only if dragging horizontally
this._lockScroll();
}
this._updateDestination(this._origX - dx, 0);
},
_lockScroll: function() {
this.overflows.forEach(el => el.style.overflowY = 'hidden');
},
_unlockScroll: function() {
this.overflows.forEach(el => el.style.overflowY = 'scroll');
},
_updateDestination: function(x, duration) {
duration = duration != null ? duration : this.TRANSITION_DURATION;
this._startX = this._curX;
this._destX = clamp(x, this._minX, this._maxX);
var now = Date.now();
this._endTime = now + duration;
if (!this._requestId) {
this._startTime = now;
this._tween();
}
},
_tween: function() {
this._requestId = window.requestAnimationFrame(this._tick);
},
_tick: function() {
var t = norm(Date.now(), this._startTime, this._endTime);
if (t >= 1 || this._curX === this._destX) {
this._killTween();
this._set(this._destX);
return;
}
var x = lerp(ease(t), this._startX, this._destX);
this._set(x);
this._tween();
},
_killTween: function() {
if (this._requestId) {
window.cancelAnimationFrame(this._requestId);
this._requestId = null;
}
this._onTweenEnd && this._onTweenEnd.call(this);
},
_onTouchEnd: function() {
// if touch is very fast momentum would be bigger than our threshold,
// this is very important for click events otherwise they wouldn't open
// the event details
if (this._isVertical || !this._lockedAxis) {
this._unlockScroll();
return;
}
var duration = Date.now() - this._touchStart;
var momentum = Math.abs(this._dx) / duration;
var snap;
if (momentum > 0.5) {
// if the drag was fast we consider it as a swipe (move multiple cells
// at once)
var direction = this._dx > 0 ? -1 : 1;
snap = this._origX + (direction * this._gridSize * this._visibleCells);
} else {
// we only round up if very close to the next column, this behavior is
// better for the user than a regular round/ceil/floor
snap = Math.round((this._destX / this._gridSize) + 0.2) *
this._gridSize;
}
// we only unlock the scroll after the tween is complete to make multiple
// consecutive swipes faster (also avoids flickering y-axis position)
this._onTweenEnd = this._unlockScroll;
this._updateDestination(snap);
this.emit('release', {
diff: Math.round((this._origX - this._destX) / this._gridSize) * this._dir
});
},
_set: function(x) {
x = clamp(x, this._minX, this._maxX);
this.targets.forEach(el => {
el.style.transform = 'translateX(' + x +'px)';
});
this._curX = x;
},
refresh: function() {
var diff = Math.abs(this._curX - this._destX);
diff *= this._curX < this._destX ? -1 : 1;
// we update the position based on the relative distance to keep a smooth
// transition
if (diff) {
this._set(this._origX + diff);
this._updateDestination(this._origX);
} else {
this._set(this._origX);
}
}
};
// borrowed from zeh/ztween (expoOut)
function ease(t) {
return (t >= 0.999) ? 1 : 1.001 * (-Math.pow(2, -10 * t) + 1);
}
});