Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
231 lines
6.4 KiB
JavaScript
Executable file
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);
|
|
}
|
|
|
|
});
|