import {extend, defer, requestAnimationFrame, prefixed} from "../../utils/core"; import { EVENTS, DOM_EVENTS } from "../../utils/constants"; import EventEmitter from "event-emitter"; // easing equations from https://github.com/danro/easing-js/blob/master/easing.js const PI_D2 = (Math.PI / 2); const EASING_EQUATIONS = { easeOutSine: function (pos) { return Math.sin(pos * PI_D2); }, easeInOutSine: function (pos) { return (-0.5 * (Math.cos(Math.PI * pos) - 1)); }, easeInOutQuint: function (pos) { if ((pos /= 0.5) < 1) { return 0.5 * Math.pow(pos, 5); } return 0.5 * (Math.pow((pos - 2), 5) + 2); }, easeInCubic: function(pos) { return Math.pow(pos, 3); } }; class Snap { constructor(manager, options) { this.settings = extend({ duration: 80, minVelocity: 0.2, minDistance: 10, easing: EASING_EQUATIONS['easeInCubic'] }, options || {}); this.supportsTouch = this.supportsTouch(); if (this.supportsTouch) { this.setup(manager); } } setup(manager) { this.manager = manager; this.layout = this.manager.layout; this.fullsize = this.manager.settings.fullsize; if (this.fullsize) { this.element = this.manager.stage.element; this.scroller = window; this.disableScroll(); } else { this.element = this.manager.stage.container; this.scroller = this.element; this.element.style["WebkitOverflowScrolling"] = "touch"; } // this.overflow = this.manager.overflow; // set lookahead offset to page width this.manager.settings.offset = this.layout.width; this.manager.settings.afterScrolledTimeout = this.settings.duration * 2; this.isVertical = this.manager.settings.axis === "vertical"; // disable snapping if not paginated or axis in not horizontal if (!this.manager.isPaginated || this.isVertical) { return; } this.touchCanceler = false; this.resizeCanceler = false; this.snapping = false; this.scrollLeft; this.scrollTop; this.startTouchX = undefined; this.startTouchY = undefined; this.startTime = undefined; this.endTouchX = undefined; this.endTouchY = undefined; this.endTime = undefined; this.addListeners(); } supportsTouch() { if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { return true; } return false; } disableScroll() { this.element.style.overflow = "hidden"; } enableScroll() { this.element.style.overflow = ""; } addListeners() { this._onResize = this.onResize.bind(this); window.addEventListener('resize', this._onResize); this._onScroll = this.onScroll.bind(this); this.scroller.addEventListener('scroll', this._onScroll); this._onTouchStart = this.onTouchStart.bind(this); this.scroller.addEventListener('touchstart', this._onTouchStart, { passive: true }); this.on('touchstart', this._onTouchStart); this._onTouchMove = this.onTouchMove.bind(this); this.scroller.addEventListener('touchmove', this._onTouchMove, { passive: true }); this.on('touchmove', this._onTouchMove); this._onTouchEnd = this.onTouchEnd.bind(this); this.scroller.addEventListener('touchend', this._onTouchEnd, { passive: true }); this.on('touchend', this._onTouchEnd); this._afterDisplayed = this.afterDisplayed.bind(this); this.manager.on(EVENTS.MANAGERS.ADDED, this._afterDisplayed); } removeListeners() { window.removeEventListener('resize', this._onResize); this._onResize = undefined; this.scroller.removeEventListener('scroll', this._onScroll); this._onScroll = undefined; this.scroller.removeEventListener('touchstart', this._onTouchStart, { passive: true }); this.off('touchstart', this._onTouchStart); this._onTouchStart = undefined; this.scroller.removeEventListener('touchmove', this._onTouchMove, { passive: true }); this.off('touchmove', this._onTouchMove); this._onTouchMove = undefined; this.scroller.removeEventListener('touchend', this._onTouchEnd, { passive: true }); this.off('touchend', this._onTouchEnd); this._onTouchEnd = undefined; this.manager.off(EVENTS.MANAGERS.ADDED, this._afterDisplayed); this._afterDisplayed = undefined; } afterDisplayed(view) { let contents = view.contents; ["touchstart", "touchmove", "touchend"].forEach((e) => { contents.on(e, (ev) => this.triggerViewEvent(ev, contents)); }); } triggerViewEvent(e, contents){ this.emit(e.type, e, contents); } onScroll(e) { this.scrollLeft = this.fullsize ? window.scrollX : this.scroller.scrollLeft; this.scrollTop = this.fullsize ? window.scrollY : this.scroller.scrollTop; } onResize(e) { this.resizeCanceler = true; } onTouchStart(e) { let { screenX, screenY } = e.touches[0]; if (this.fullsize) { this.enableScroll(); } this.touchCanceler = true; if (!this.startTouchX) { this.startTouchX = screenX; this.startTouchY = screenY; this.startTime = this.now(); } this.endTouchX = screenX; this.endTouchY = screenY; this.endTime = this.now(); } onTouchMove(e) { let { screenX, screenY } = e.touches[0]; let deltaY = Math.abs(screenY - this.endTouchY); this.touchCanceler = true; if (!this.fullsize && deltaY < 10) { this.element.scrollLeft -= screenX - this.endTouchX; } this.endTouchX = screenX; this.endTouchY = screenY; this.endTime = this.now(); } onTouchEnd(e) { if (this.fullsize) { this.disableScroll(); } this.touchCanceler = false; let swipped = this.wasSwiped(); if (swipped !== 0) { this.snap(swipped); } else { this.snap(); } this.startTouchX = undefined; this.startTouchY = undefined; this.startTime = undefined; this.endTouchX = undefined; this.endTouchY = undefined; this.endTime = undefined; } wasSwiped() { let snapWidth = this.layout.pageWidth * this.layout.divisor; let distance = (this.endTouchX - this.startTouchX); let absolute = Math.abs(distance); let time = this.endTime - this.startTime; let velocity = (distance / time); let minVelocity = this.settings.minVelocity; if (absolute <= this.settings.minDistance || absolute >= snapWidth) { return 0; } if (velocity > minVelocity) { // previous return -1; } else if (velocity < -minVelocity) { // next return 1; } } needsSnap() { let left = this.scrollLeft; let snapWidth = this.layout.pageWidth * this.layout.divisor; return (left % snapWidth) !== 0; } snap(howMany=0) { let left = this.scrollLeft; let snapWidth = this.layout.pageWidth * this.layout.divisor; let snapTo = Math.round(left / snapWidth) * snapWidth; if (howMany) { snapTo += (howMany * snapWidth); } return this.smoothScrollTo(snapTo); } smoothScrollTo(destination) { const deferred = new defer(); const start = this.scrollLeft; const startTime = this.now(); const duration = this.settings.duration; const easing = this.settings.easing; this.snapping = true; // add animation loop function tick() { const now = this.now(); const time = Math.min(1, ((now - startTime) / duration)); const timeFunction = easing(time); if (this.touchCanceler || this.resizeCanceler) { this.resizeCanceler = false; this.snapping = false; deferred.resolve(); return; } if (time < 1) { window.requestAnimationFrame(tick.bind(this)); this.scrollTo(start + ((destination - start) * time), 0); } else { this.scrollTo(destination, 0); this.snapping = false; deferred.resolve(); } } tick.call(this); return deferred.promise; } scrollTo(left=0, top=0) { if (this.fullsize) { window.scroll(left, top); } else { this.scroller.scrollLeft = left; this.scroller.scrollTop = top; } } now() { return ('now' in window.performance) ? performance.now() : new Date().getTime(); } destroy() { if (!this.scroller) { return; } if (this.fullsize) { this.enableScroll(); } this.removeListeners(); this.scroller = undefined; } } EventEmitter(Snap.prototype); export default Snap;