hscroll
Version:
make element horizon scroll
646 lines (542 loc) • 17.2 kB
JavaScript
'use strict';
exports.__esModule = true;
var _raf = require('raf');
var _raf2 = _interopRequireDefault(_raf);
var _tween = require('tween');
var _tween2 = _interopRequireDefault(_tween);
var _propDetect = require('prop-detect');
var _propDetect2 = _interopRequireDefault(_propDetect);
var _events = require('events');
var _events2 = _interopRequireDefault(_events);
var _emitter = require('emitter');
var _emitter2 = _interopRequireDefault(_emitter);
var _computedStyle = require('computed-style');
var _computedStyle2 = _interopRequireDefault(_computedStyle);
var _hasTouch = require('has-touch');
var _hasTouch2 = _interopRequireDefault(_hasTouch);
var _debounce = require('debounce');
var _debounce2 = _interopRequireDefault(_debounce);
var _mouseWheelEvent = require('mouse-wheel-event');
var _mouseWheelEvent2 = _interopRequireDefault(_mouseWheelEvent);
var _resizelistener = require('resizelistener');
var _resizelistener2 = _interopRequireDefault(_resizelistener);
var _removed = require('removed');
var _removed2 = _interopRequireDefault(_removed);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var has3d = _propDetect2['default'].has3d;
var transform = _propDetect2['default'].transform;
/**
* Hscroll constructor
*
* @public
* @param {Element} el
* @param {Object} opt
*/
var Hscroll = function (_Emitter) {
_inherits(Hscroll, _Emitter);
function Hscroll(el) {
var opt = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
_classCallCheck(this, Hscroll);
var _this = _possibleConstructorReturn(this, _Emitter.call(this));
_this.el = el;
el.style.overflow = 'hidden';
_this.loop = opt.loop || false;
_this.interval = opt.interval || 1000;
_this.duration = opt.duration || 300;
_this.wrapper = _this.el.firstElementChild;
if (!_this.wrapper) throw new Error('Child element required for hscroll');
_this.type = opt.type || 'normal';
// maximun duration in ms for fast swipe
_this.threshold = opt.threshold || 200;
// minimum moved distance for fast swipe
_this.fastThreshold = opt.fastThreshold || 30;
_this.autoWidth = opt.autoWidth || false;
_this.autoHeight = opt.autoHeight || false;
// transformX
_this.tx = 0;
_this.bind();
_this.refresh();
(0, _removed2['default'])(el, function () {
_this.unbind();
});
return _this;
}
/**
* Bind event handlers.
*
* @api public
*/
Hscroll.prototype.bind = function bind() {
this.events = (0, _events2['default'])(this.wrapper, this);
this.docEvents = (0, _events2['default'])(document, this);
// standard mouse click events
if (!_hasTouch2['default'] && document.addEventListener) {
this.events.bind('mousedown', 'ontouchstart');
this.events.bind('mousemove', 'ontouchmove');
this.events.bind('mouseup', 'ontouchend');
this.docEvents.bind('mouseup', 'ontouchend');
this._wheelUnbind = (0, _mouseWheelEvent2['default'])(this.el, this.onwheel.bind(this), false);
} else if (_hasTouch2['default']) {
// W3C touch events
this.events.bind('touchstart');
this.events.bind('touchmove');
this.docEvents.bind('touchend');
// MS IE touch events
this.events.bind('PointerDown', 'ontouchstart');
this.events.bind('PointerMove', 'ontouchmove');
this.docEvents.bind('PointerUp', 'ontouchstart');
}
this.unbindResize = (0, _resizelistener2['default'])(this.el, (0, _debounce2['default'])(this.refresh.bind(this), 100));
};
Hscroll.prototype.getTouch = function getTouch(e) {
// "mouse" and "Pointer" events just use the event object itself
var touch = e;
if (e.touches && e.touches.length > 1) return;
if (e.changedTouches && e.changedTouches.length > 0) {
// W3C "touch" events use the `changedTouches` array
touch = e.changedTouches[0];
}
return touch;
};
/**
* Handle touchstart.
*
* @api private
*/
Hscroll.prototype.ontouchstart = function ontouchstart(e) {
var _target = e.target || e.srcElement;
if (this.animating) this.tween.stop();
var target = _target || e.srcElement;
if (target.tagName.toLowerCase() == 'input') {
if (/^(text|password|tel|search|number|email|url)$/.test(target.type) && target.value) return;
}
var touch = this.getTouch(e);
if (!touch) return;
this.start = this.curr();
this.speed = 0;
var d = Date.now();
var sx = touch.pageX;
var sy = touch.pageY;
var self = this;
var tx = this.tx;
var limit = this.getLimitation();
var pad = 20;
this.down = {
x: sx,
y: sy,
tx: tx,
at: d
};
this.move = function (e, touch) {
var cx = touch.pageX;
var cy = touch.pageY;
var px = self.previous ? self.previous.x : sx;
var py = self.previous ? self.previous.y : sy;
var leftOrRight = Math.abs(cx - px) > Math.abs(cy - py);
if (!leftOrRight) return;
e.preventDefault ? e.preventDefault() : e.returnValue = false;
e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
self.calcuteSpeed(cx, cy);
var tx = self.down.tx + cx - sx;
tx = tx < limit.min - pad ? limit.min - pad : tx;
tx = tx > limit.max + pad ? limit.max + pad : tx;
self.setTransform(tx);
};
};
/**
* Handle touchmove.
*
* @api private
*/
Hscroll.prototype.ontouchmove = function ontouchmove(e) {
var touch = this.getTouch(e);
if (!touch || this.animating || !this.move) {
this.move = null;
return;
}
this.move(e, touch);
};
/**
* Handle touchend.
*
* @api private
*/
Hscroll.prototype.ontouchend = function ontouchend(e) {
if (!this.move || !this.down || this.animating) return;
this.move = null;
var touch = this.getTouch(e);
if (!touch) return;
var t = Date.now();
var x = touch.pageX;
var y = touch.pageY;
var dx = Math.abs(x - this.down.x);
var dy = Math.abs(y - this.down.y);
if (dx > 5) {
e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
e.stopImmediatePropagation ? e.stopImmediatePropagation() : void 0;
}
if (Math.sqrt(dx * dx + dy * dy) < 5) {
this.emit('select', this.curr());
}
if (this.type == 'swipe' && dx > dy && dx > this.fastThreshold && t - this.down.at < this.threshold) {
// fast swipe
var dir = x > this.down.x ? 1 : -1;
this.show(this.start - dir);
} else {
if (this.type == 'swipe') {
this.reset();
} else if (this.speed) {
this.momentum();
}
}
this.down = this.previous = null;
};
Hscroll.prototype.onwheel = function onwheel(dx, dy, dz, e) {
if (Math.abs(dy) > Math.abs(dx)) return;
e.preventDefault ? e.preventDefault() : e.returnValue = false;
this.stop();
if (this.ts && !this.animating) {
var speed = Math.abs(dx) / (Date.now() - this.ts);
if (this.type == 'swipe' && speed > 2) this.swipe(dx < 0 ? 1 : -1);
if (this.type == 'normal') {
var tx = this.tx - dx;
var limit = this.getLimitation();
tx = Math.max(limit.min, tx);
tx = Math.min(limit.max, tx);
this.setTransform(tx);
}
}
this.ts = Date.now();
};
Hscroll.prototype.momentum = function momentum() {
var deceleration = 0.001;
var speed = this.speed;
var x = this.tx;
speed = Math.min(speed, 2);
var limit = this.getLimitation();
var minX = limit.min;
var rate = (4 - Math.PI) / 2;
var destination = x + rate * (speed * speed) / (2 * deceleration) * this.direction;
var duration = speed / deceleration;
var newX = void 0;
var ease = 'out-circ';
if (destination > 0) {
newX = 0;
ease = 'out-back';
} else if (destination < minX) {
newX = minX;
ease = 'out-back';
}
if (typeof newX === 'number') {
duration = duration * Math.abs((newX - x + 60) / (destination - x));
destination = newX;
}
if (x > 0 || x < minX) {
duration = 500;
ease = 'out-circ';
}
if (this.type == 'fix') {
var width = this.itemWidth;
destination = Math.round(destination / width) * width;
}
this.animate(destination, duration, ease);
return;
};
/**
* Unbind event handlers.
*
* @api public
*/
Hscroll.prototype.unbind = function unbind() {
this.emit('ubind');
this.stop();
this.events.unbind();
this.docEvents.unbind();
this.unbindResize();
if (this._wheelUnbind) this._wheelUnbind();
};
/**
* Show the previous item/slide, if any.
*
* @return {Swipe} self
* @api public
*/
Hscroll.prototype.prev = function prev() {
if (this.type == 'swipe') {
return this.swipe(1);
} else {
return this.show(this.toFixed(1));
}
};
/**
* Show the next item/slide, if any.
*
* @return {Swipe} self
* @api public
*/
Hscroll.prototype.next = function next() {
if (this.type == 'swipe') {
return this.swipe(-1);
} else {
return this.show(this.toFixed(-1));
}
};
/**
* Swipe to previous/next piece
*
* @public
* @param {Number} dir 1 or -1
*/
Hscroll.prototype.swipe = function swipe(dir) {
var to = this.toFixed(dir);
var self = this;
var x = -to * this.viewWidth;
if (x === this.tx) return Promise.resolve(null);
return this.animate(x).then(function (stopped) {
if (stopped) return;
self.emit('show', to);
});
};
/**
* Get a sane item index from direction
*
* @private
* @param {Number} dir
* @returns {Number}
*/
Hscroll.prototype.toFixed = function toFixed(dir) {
var to = this.curr() - dir;
var max = this.type == 'swipe' ? this.itemCount - 1 : this.itemCount - Math.floor(this.viewWidth / this.itemWidth);
if (to < 0) {
to = this.loop ? max : 0;
} else if (to > max) {
to = this.loop ? 0 : max;
}
return to;
};
/**
* show nth item with scroll and animation
*
* @public
* @param {Number} n
* @param {Number} duration
* @param {String} ease
*/
Hscroll.prototype.show = function show(n, duration, ease) {
var _this2 = this;
if (this.animating) this.tween.stop();
var width = this.type == 'swipe' ? this.viewWidth : this.itemWidth;
n = Math.max(n, 0);
n = Math.min(n, this.itemCount - 1);
var tx = -n * width;
var limit = this.getLimitation();
tx = Math.max(tx, limit.min);
if (tx == this.tx) return Promise.resolve(null);
var self = this;
if (duration === 0) {
this.setTransform(tx);
this.refresh();
this.emit('show', n);
return Promise.resolve(null);
}
return this.animate(tx, duration, ease).then(function (stopped) {
if (stopped) return;
_this2.refresh();
self.emit('show', n);
});
};
/**
* show last item with scroll
*
* @public
* @param {Number} n
*/
Hscroll.prototype.last = function last() {
return this.show(Infinity);
};
/**
* show first item with scroll
*
* @public
* @param {Number} n
*/
Hscroll.prototype.first = function first() {
return this.show(0);
};
/**
* autoplay like sliders
*
* @public
*/
Hscroll.prototype.play = function play() {
var _this3 = this;
if (this.playing) return;
this.playing = true;
if (this.inter != null) clearInterval(this.inter);
this.inter = setInterval(function () {
if (!_this3.playing) return;
var curr = _this3.curr();
var max = _this3.type == 'swipe' ? _this3.itemCount - 1 : _this3.itemCount - Math.floor(_this3.viewWidth / _this3.itemWidth);
if (curr >= max) {
_this3.first();
} else {
_this3.next();
}
}, this.interval);
};
/**
* stop playing sliders
*
* @public
*/
Hscroll.prototype.stop = function stop() {
this.playing = false;
window.clearInterval(this.inter);
};
/**
* Restore to sane position
*
* @public
*/
Hscroll.prototype.reset = function reset() {
var limit = this.getLimitation();
var tx = this.tx;
if (tx < limit.min) {
this.animate(limit.min);
} else if (tx > limit.max) {
this.animate(limit.max);
} else if (this.type == 'swipe' && tx % this.viewWidth !== 0) {
this.swipe(0);
}
};
/**
* Get current item number
*
* @public
* @returns {Number}
*/
Hscroll.prototype.curr = function curr() {
if (this.type == 'swipe') {
return Math.round(-this.tx / this.viewWidth);
} else {
return Math.round(-this.tx / this.itemWidth);
}
};
/**
* Recalcute wrapper and set the position if invalid
*
* @public
*/
Hscroll.prototype.refresh = function refresh() {
var parent = this.wrapper;
this.viewWidth = this.el.clientWidth;
if (this.viewWidth == 0) return;
var items = parent.children;
this.itemWidth = this.autoWidth ? this.viewWidth : items[0].clientWidth;
if (this.autoWidth) {
// set height and width
for (var i = 0, l = items.length; i < l; i++) {
if (this.autoWidth) items[i].style.width = this.viewWidth + 'px';
}
}
var item = items[this.curr()];
if (!item) return;
var h = item.clientHeight;
var pb = parseInt((0, _computedStyle2['default'])(parent, 'padding-bottom'), 10) || 0;
if (this.autoHeight) parent.style.height = h + pb + 'px';
var width = items.length * this.itemWidth;
parent.style.width = width + 'px';
if (this.type == 'swipe') {
this.itemCount = Math.ceil(width / this.viewWidth);
} else {
this.itemCount = items.length;
}
this.reset();
};
/**
* get min and max value for transform
*
* @private
*/
Hscroll.prototype.getLimitation = function getLimitation() {
var max = void 0;
if (this.type == 'swipe') {
max = (this.itemCount - 1) * this.viewWidth;
} else {
max = parseInt((0, _computedStyle2['default'])(this.wrapper, 'width'), 10) - this.viewWidth;
}
return {
max: 0,
min: -max
};
};
/**
* set transform properties of element
*
* @public
* @param {Number} x
*/
Hscroll.prototype.setTransform = function setTransform(x) {
this.tx = x;
if (typeof transform === 'string') {
if (has3d) {
this.wrapper.style[transform] = 'translate3d(' + x + 'px, 0, 0) ';
} else {
this.wrapper.style[transform] = 'translateX(' + x + 'px)';
}
} else {
// for old ie which have no transform
this.wrapper.style.left = x + 'px';
}
};
/**
* Set translateX with animate duration (in milisecond) and ease
*
* @private
* @param {Number} x
* @param {Number} duration
* @param {String} ease
*/
Hscroll.prototype.animate = function animate(x) {
var duration = arguments.length <= 1 || arguments[1] === undefined ? this.duration : arguments[1];
var ease = arguments.length <= 2 || arguments[2] === undefined ? 'out-circ' : arguments[2];
var self = this;
this.animating = true;
var tween = this.tween = (0, _tween2['default'])({ x: this.tx }).ease(ease).to({ x: x }).duration(duration);
tween.update(function (o) {
self.setTransform(o.x);
});
var promise = new Promise(function (resolve) {
tween.on('end', function () {
animate = function animate() {}; // eslint-disable-line
self.animating = false;
resolve(tween.stopped);
});
});
function animate() {
(0, _raf2['default'])(animate);
tween.update();
}
animate();
return promise;
};
Hscroll.prototype.calcuteSpeed = function calcuteSpeed(x, y) {
var previous = this.previous || this.down;
var ts = Date.now();
var dt = ts - previous.at;
if (ts - this.down.at < 100 || dt > 100) {
var distance = Math.abs(x - previous.x);
this.speed = distance / dt;
this.direction = x > previous.x ? 1 : -1;
}
if (dt > 100) {
this.previous = { x: x, y: y, at: ts };
}
};
return Hscroll;
}(_emitter2['default']);
exports['default'] = Hscroll;
module.exports = exports['default'];