You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1305 lines
41 KiB
1305 lines
41 KiB
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _Animate = require("./Animate");
|
|
|
|
var _Animate2 = _interopRequireDefault(_Animate);
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
|
|
|
|
var Scroller; /*
|
|
* Scroller
|
|
* http://github.com/zynga/scroller
|
|
*
|
|
* Copyright 2011, Zynga Inc.
|
|
* Licensed under the MIT License.
|
|
* https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
|
|
*
|
|
* Based on the work of: Unify Project (unify-project.org)
|
|
* http://unify-project.org
|
|
* Copyright 2011, Deutsche Telekom AG
|
|
* License: MIT + Apache (V2)
|
|
*/
|
|
|
|
var NOOP = function NOOP() {};
|
|
|
|
/**
|
|
* A pure logic 'component' for 'virtual' scrolling/zooming.
|
|
*/
|
|
Scroller = function Scroller(callback, options) {
|
|
|
|
this.__callback = callback;
|
|
|
|
this.options = {
|
|
|
|
/** Enable scrolling on x-axis */
|
|
scrollingX: true,
|
|
|
|
/** Enable scrolling on y-axis */
|
|
scrollingY: true,
|
|
|
|
/** Enable animations for deceleration, snap back, zooming and scrolling */
|
|
animating: true,
|
|
|
|
/** duration for animations triggered by scrollTo/zoomTo */
|
|
animationDuration: 250,
|
|
|
|
/** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
|
|
bouncing: true,
|
|
|
|
/** Enable locking to the main axis if user moves only slightly on one of them at start */
|
|
locking: true,
|
|
|
|
/** Enable pagination mode (switching between full page content panes) */
|
|
paging: false,
|
|
|
|
/** Enable snapping of content to a configured pixel grid */
|
|
snapping: false,
|
|
|
|
/** Enable zooming of content via API, fingers and mouse wheel */
|
|
zooming: false,
|
|
|
|
/** Minimum zoom level */
|
|
minZoom: 0.5,
|
|
|
|
/** Maximum zoom level */
|
|
maxZoom: 3,
|
|
|
|
/** Multiply or decrease scrolling speed **/
|
|
speedMultiplier: 1,
|
|
|
|
/** Callback that is fired on the later of touch end or deceleration end,
|
|
provided that another scrolling action has not begun. Used to know
|
|
when to fade out a scrollbar. */
|
|
scrollingComplete: NOOP,
|
|
|
|
/** This configures the amount of change applied to deceleration when reaching boundaries **/
|
|
penetrationDeceleration: 0.03,
|
|
|
|
/** This configures the amount of change applied to acceleration when reaching boundaries **/
|
|
penetrationAcceleration: 0.08
|
|
|
|
};
|
|
|
|
for (var key in options) {
|
|
this.options[key] = options[key];
|
|
}
|
|
};
|
|
|
|
// Easing Equations (c) 2003 Robert Penner, all rights reserved.
|
|
// Open source under the BSD License.
|
|
|
|
/**
|
|
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
|
|
**/
|
|
var easeOutCubic = function easeOutCubic(pos) {
|
|
return Math.pow(pos - 1, 3) + 1;
|
|
};
|
|
|
|
/**
|
|
* @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
|
|
**/
|
|
var easeInOutCubic = function easeInOutCubic(pos) {
|
|
if ((pos /= 0.5) < 1) {
|
|
return 0.5 * Math.pow(pos, 3);
|
|
}
|
|
|
|
return 0.5 * (Math.pow(pos - 2, 3) + 2);
|
|
};
|
|
|
|
var members = {
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
INTERNAL FIELDS :: STATUS
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/** {Boolean} Whether only a single finger is used in touch handling */
|
|
__isSingleTouch: false,
|
|
|
|
/** {Boolean} Whether a touch event sequence is in progress */
|
|
__isTracking: false,
|
|
|
|
/** {Boolean} Whether a deceleration animation went to completion. */
|
|
__didDecelerationComplete: false,
|
|
|
|
/**
|
|
* {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when
|
|
* a gesturestart event happens. This has higher priority than dragging.
|
|
*/
|
|
__isGesturing: false,
|
|
|
|
/**
|
|
* {Boolean} Whether the user has moved by such a distance that we have enabled
|
|
* dragging mode. Hint: It's only enabled after some pixels of movement to
|
|
* not interrupt with clicks etc.
|
|
*/
|
|
__isDragging: false,
|
|
|
|
/**
|
|
* {Boolean} Not touching and dragging anymore, and smoothly animating the
|
|
* touch sequence using deceleration.
|
|
*/
|
|
__isDecelerating: false,
|
|
|
|
/**
|
|
* {Boolean} Smoothly animating the currently configured change
|
|
*/
|
|
__isAnimating: false,
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
INTERNAL FIELDS :: DIMENSIONS
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/** {Integer} Available outer left position (from document perspective) */
|
|
__clientLeft: 0,
|
|
|
|
/** {Integer} Available outer top position (from document perspective) */
|
|
__clientTop: 0,
|
|
|
|
/** {Integer} Available outer width */
|
|
__clientWidth: 0,
|
|
|
|
/** {Integer} Available outer height */
|
|
__clientHeight: 0,
|
|
|
|
/** {Integer} Outer width of content */
|
|
__contentWidth: 0,
|
|
|
|
/** {Integer} Outer height of content */
|
|
__contentHeight: 0,
|
|
|
|
/** {Integer} Snapping width for content */
|
|
__snapWidth: 100,
|
|
|
|
/** {Integer} Snapping height for content */
|
|
__snapHeight: 100,
|
|
|
|
/** {Integer} Height to assign to refresh area */
|
|
__refreshHeight: null,
|
|
|
|
/** {Boolean} Whether the refresh process is enabled when the event is released now */
|
|
__refreshActive: false,
|
|
|
|
/** {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
|
|
__refreshActivate: null,
|
|
|
|
/** {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
|
|
__refreshDeactivate: null,
|
|
|
|
/** {Function} Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
|
|
__refreshStart: null,
|
|
|
|
/** {Number} Zoom level */
|
|
__zoomLevel: 1,
|
|
|
|
/** {Number} Scroll position on x-axis */
|
|
__scrollLeft: 0,
|
|
|
|
/** {Number} Scroll position on y-axis */
|
|
__scrollTop: 0,
|
|
|
|
/** {Integer} Maximum allowed scroll position on x-axis */
|
|
__maxScrollLeft: 0,
|
|
|
|
/** {Integer} Maximum allowed scroll position on y-axis */
|
|
__maxScrollTop: 0,
|
|
|
|
/* {Number} Scheduled left position (final position when animating) */
|
|
__scheduledLeft: 0,
|
|
|
|
/* {Number} Scheduled top position (final position when animating) */
|
|
__scheduledTop: 0,
|
|
|
|
/* {Number} Scheduled zoom level (final scale when animating) */
|
|
__scheduledZoom: 0,
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
INTERNAL FIELDS :: LAST POSITIONS
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/** {Number} Left position of finger at start */
|
|
__lastTouchLeft: null,
|
|
|
|
/** {Number} Top position of finger at start */
|
|
__lastTouchTop: null,
|
|
|
|
/** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
|
|
__lastTouchMove: null,
|
|
|
|
/** {Array} List of positions, uses three indexes for each state: left, top, timestamp */
|
|
__positions: null,
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
INTERNAL FIELDS :: DECELERATION SUPPORT
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/** {Integer} Minimum left scroll position during deceleration */
|
|
__minDecelerationScrollLeft: null,
|
|
|
|
/** {Integer} Minimum top scroll position during deceleration */
|
|
__minDecelerationScrollTop: null,
|
|
|
|
/** {Integer} Maximum left scroll position during deceleration */
|
|
__maxDecelerationScrollLeft: null,
|
|
|
|
/** {Integer} Maximum top scroll position during deceleration */
|
|
__maxDecelerationScrollTop: null,
|
|
|
|
/** {Number} Current factor to modify horizontal scroll position with on every step */
|
|
__decelerationVelocityX: null,
|
|
|
|
/** {Number} Current factor to modify vertical scroll position with on every step */
|
|
__decelerationVelocityY: null,
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
PUBLIC API
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Configures the dimensions of the client (outer) and content (inner) elements.
|
|
* Requires the available space for the outer element and the outer size of the inner element.
|
|
* All values which are falsy (null or zero etc.) are ignored and the old value is kept.
|
|
*
|
|
* @param clientWidth {Integer ? null} Inner width of outer element
|
|
* @param clientHeight {Integer ? null} Inner height of outer element
|
|
* @param contentWidth {Integer ? null} Outer width of inner element
|
|
* @param contentHeight {Integer ? null} Outer height of inner element
|
|
*/
|
|
setDimensions: function setDimensions(clientWidth, clientHeight, contentWidth, contentHeight) {
|
|
|
|
var self = this;
|
|
|
|
// Only update values which are defined
|
|
if (clientWidth === +clientWidth) {
|
|
self.__clientWidth = clientWidth;
|
|
}
|
|
|
|
if (clientHeight === +clientHeight) {
|
|
self.__clientHeight = clientHeight;
|
|
}
|
|
|
|
if (contentWidth === +contentWidth) {
|
|
self.__contentWidth = contentWidth;
|
|
}
|
|
|
|
if (contentHeight === +contentHeight) {
|
|
self.__contentHeight = contentHeight;
|
|
}
|
|
|
|
// Refresh maximums
|
|
self.__computeScrollMax();
|
|
|
|
// Refresh scroll position
|
|
self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
|
|
},
|
|
|
|
/**
|
|
* Sets the client coordinates in relation to the document.
|
|
*
|
|
* @param left {Integer ? 0} Left position of outer element
|
|
* @param top {Integer ? 0} Top position of outer element
|
|
*/
|
|
setPosition: function setPosition(left, top) {
|
|
|
|
var self = this;
|
|
|
|
self.__clientLeft = left || 0;
|
|
self.__clientTop = top || 0;
|
|
},
|
|
|
|
/**
|
|
* Configures the snapping (when snapping is active)
|
|
*
|
|
* @param width {Integer} Snapping width
|
|
* @param height {Integer} Snapping height
|
|
*/
|
|
setSnapSize: function setSnapSize(width, height) {
|
|
|
|
var self = this;
|
|
|
|
self.__snapWidth = width;
|
|
self.__snapHeight = height;
|
|
},
|
|
|
|
/**
|
|
* Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
|
|
* the user event is released during visibility of this zone. This was introduced by some apps on iOS like
|
|
* the official Twitter client.
|
|
*
|
|
* @param height {Integer} Height of pull-to-refresh zone on top of rendered list
|
|
* @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
|
|
* @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
|
|
* @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
|
|
*/
|
|
activatePullToRefresh: function activatePullToRefresh(height, activateCallback, deactivateCallback, startCallback) {
|
|
|
|
var self = this;
|
|
|
|
self.__refreshHeight = height;
|
|
self.__refreshActivate = activateCallback;
|
|
self.__refreshDeactivate = deactivateCallback;
|
|
self.__refreshStart = startCallback;
|
|
},
|
|
|
|
/**
|
|
* Starts pull-to-refresh manually.
|
|
*/
|
|
triggerPullToRefresh: function triggerPullToRefresh() {
|
|
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
|
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
|
this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);
|
|
|
|
if (this.__refreshStart) {
|
|
this.__refreshStart();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Signalizes that pull-to-refresh is finished.
|
|
*/
|
|
finishPullToRefresh: function finishPullToRefresh() {
|
|
|
|
var self = this;
|
|
|
|
self.__refreshActive = false;
|
|
if (self.__refreshDeactivate) {
|
|
self.__refreshDeactivate();
|
|
}
|
|
|
|
self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
|
|
},
|
|
|
|
/**
|
|
* Returns the scroll position and zooming values
|
|
*
|
|
* @return {Map} `left` and `top` scroll position and `zoom` level
|
|
*/
|
|
getValues: function getValues() {
|
|
|
|
var self = this;
|
|
|
|
return {
|
|
left: self.__scrollLeft,
|
|
top: self.__scrollTop,
|
|
zoom: self.__zoomLevel
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Returns the maximum scroll values
|
|
*
|
|
* @return {Map} `left` and `top` maximum scroll values
|
|
*/
|
|
getScrollMax: function getScrollMax() {
|
|
|
|
var self = this;
|
|
|
|
return {
|
|
left: self.__maxScrollLeft,
|
|
top: self.__maxScrollTop
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Zooms to the given level. Supports optional animation. Zooms
|
|
* the center when no coordinates are given.
|
|
*
|
|
* @param level {Number} Level to zoom to
|
|
* @param animate {Boolean ? false} Whether to use animation
|
|
* @param originLeft {Number ? null} Zoom in at given left coordinate
|
|
* @param originTop {Number ? null} Zoom in at given top coordinate
|
|
* @param callback {Function ? null} A callback that gets fired when the zoom is complete.
|
|
*/
|
|
zoomTo: function zoomTo(level, animate, originLeft, originTop, callback) {
|
|
|
|
var self = this;
|
|
|
|
if (!self.options.zooming) {
|
|
throw new Error("Zooming is not enabled!");
|
|
}
|
|
|
|
// Add callback if exists
|
|
if (callback) {
|
|
self.__zoomComplete = callback;
|
|
}
|
|
|
|
// Stop deceleration
|
|
if (self.__isDecelerating) {
|
|
_Animate2["default"].stop(self.__isDecelerating);
|
|
self.__isDecelerating = false;
|
|
}
|
|
|
|
var oldLevel = self.__zoomLevel;
|
|
|
|
// Normalize input origin to center of viewport if not defined
|
|
if (originLeft == null) {
|
|
originLeft = self.__clientWidth / 2;
|
|
}
|
|
|
|
if (originTop == null) {
|
|
originTop = self.__clientHeight / 2;
|
|
}
|
|
|
|
// Limit level according to configuration
|
|
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
|
|
|
|
// Recompute maximum values while temporary tweaking maximum scroll ranges
|
|
self.__computeScrollMax(level);
|
|
|
|
// Recompute left and top coordinates based on new zoom level
|
|
var left = (originLeft + self.__scrollLeft) * level / oldLevel - originLeft;
|
|
var top = (originTop + self.__scrollTop) * level / oldLevel - originTop;
|
|
|
|
// Limit x-axis
|
|
if (left > self.__maxScrollLeft) {
|
|
left = self.__maxScrollLeft;
|
|
} else if (left < 0) {
|
|
left = 0;
|
|
}
|
|
|
|
// Limit y-axis
|
|
if (top > self.__maxScrollTop) {
|
|
top = self.__maxScrollTop;
|
|
} else if (top < 0) {
|
|
top = 0;
|
|
}
|
|
|
|
// Push values out
|
|
self.__publish(left, top, level, animate);
|
|
},
|
|
|
|
/**
|
|
* Zooms the content by the given factor.
|
|
*
|
|
* @param factor {Number} Zoom by given factor
|
|
* @param animate {Boolean ? false} Whether to use animation
|
|
* @param originLeft {Number ? 0} Zoom in at given left coordinate
|
|
* @param originTop {Number ? 0} Zoom in at given top coordinate
|
|
* @param callback {Function ? null} A callback that gets fired when the zoom is complete.
|
|
*/
|
|
zoomBy: function zoomBy(factor, animate, originLeft, originTop, callback) {
|
|
|
|
var self = this;
|
|
|
|
self.zoomTo(self.__zoomLevel * factor, animate, originLeft, originTop, callback);
|
|
},
|
|
|
|
/**
|
|
* Scrolls to the given position. Respect limitations and snapping automatically.
|
|
*
|
|
* @param left {Number?null} Horizontal scroll position, keeps current if value is <code>null</code>
|
|
* @param top {Number?null} Vertical scroll position, keeps current if value is <code>null</code>
|
|
* @param animate {Boolean?false} Whether the scrolling should happen using an animation
|
|
* @param zoom {Number?null} Zoom level to go to
|
|
*/
|
|
scrollTo: function scrollTo(left, top, animate, zoom, callback) {
|
|
|
|
var self = this;
|
|
|
|
// Stop deceleration
|
|
if (self.__isDecelerating) {
|
|
_Animate2["default"].stop(self.__isDecelerating);
|
|
self.__isDecelerating = false;
|
|
}
|
|
|
|
// Correct coordinates based on new zoom level
|
|
if (zoom != null && zoom !== self.__zoomLevel) {
|
|
|
|
if (!self.options.zooming) {
|
|
throw new Error("Zooming is not enabled!");
|
|
}
|
|
|
|
left *= zoom;
|
|
top *= zoom;
|
|
|
|
// Recompute maximum values while temporary tweaking maximum scroll ranges
|
|
self.__computeScrollMax(zoom);
|
|
} else {
|
|
|
|
// Keep zoom when not defined
|
|
zoom = self.__zoomLevel;
|
|
}
|
|
|
|
if (!self.options.scrollingX) {
|
|
|
|
left = self.__scrollLeft;
|
|
} else {
|
|
|
|
if (self.options.paging) {
|
|
left = Math.round(left / self.__clientWidth) * self.__clientWidth;
|
|
} else if (self.options.snapping) {
|
|
left = Math.round(left / self.__snapWidth) * self.__snapWidth;
|
|
}
|
|
}
|
|
|
|
if (!self.options.scrollingY) {
|
|
|
|
top = self.__scrollTop;
|
|
} else {
|
|
|
|
if (self.options.paging) {
|
|
top = Math.round(top / self.__clientHeight) * self.__clientHeight;
|
|
} else if (self.options.snapping) {
|
|
top = Math.round(top / self.__snapHeight) * self.__snapHeight;
|
|
}
|
|
}
|
|
|
|
// Limit for allowed ranges
|
|
left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
|
|
top = Math.max(Math.min(self.__maxScrollTop, top), 0);
|
|
|
|
// Don't animate when no change detected, still call publish to make sure
|
|
// that rendered position is really in-sync with internal data
|
|
if (left === self.__scrollLeft && top === self.__scrollTop) {
|
|
animate = false;
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
// Publish new values
|
|
if (!self.__isTracking) {
|
|
self.__publish(left, top, zoom, animate);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Scroll by the given offset
|
|
*
|
|
* @param left {Number ? 0} Scroll x-axis by given offset
|
|
* @param top {Number ? 0} Scroll x-axis by given offset
|
|
* @param animate {Boolean ? false} Whether to animate the given change
|
|
*/
|
|
scrollBy: function scrollBy(left, top, animate) {
|
|
|
|
var self = this;
|
|
|
|
var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
|
|
var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
|
|
|
|
self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
|
|
},
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
EVENT CALLBACKS
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Mouse wheel handler for zooming support
|
|
*/
|
|
doMouseZoom: function doMouseZoom(wheelDelta, timeStamp, pageX, pageY) {
|
|
|
|
var self = this;
|
|
var change = wheelDelta > 0 ? 0.97 : 1.03;
|
|
|
|
return self.zoomTo(self.__zoomLevel * change, false, pageX - self.__clientLeft, pageY - self.__clientTop);
|
|
},
|
|
|
|
/**
|
|
* Touch start handler for scrolling support
|
|
*/
|
|
doTouchStart: function doTouchStart(touches, timeStamp) {
|
|
|
|
// Array-like check is enough here
|
|
if (touches.length == null) {
|
|
throw new Error("Invalid touch list: " + touches);
|
|
}
|
|
|
|
if (timeStamp instanceof Date) {
|
|
timeStamp = timeStamp.valueOf();
|
|
}
|
|
if (typeof timeStamp !== "number") {
|
|
throw new Error("Invalid timestamp value: " + timeStamp);
|
|
}
|
|
|
|
var self = this;
|
|
|
|
// Reset interruptedAnimation flag
|
|
self.__interruptedAnimation = true;
|
|
|
|
// Stop deceleration
|
|
if (self.__isDecelerating) {
|
|
_Animate2["default"].stop(self.__isDecelerating);
|
|
self.__isDecelerating = false;
|
|
self.__interruptedAnimation = true;
|
|
}
|
|
|
|
// Stop animation
|
|
if (self.__isAnimating) {
|
|
_Animate2["default"].stop(self.__isAnimating);
|
|
self.__isAnimating = false;
|
|
self.__interruptedAnimation = true;
|
|
}
|
|
|
|
// Use center point when dealing with two fingers
|
|
var currentTouchLeft, currentTouchTop;
|
|
var isSingleTouch = touches.length === 1;
|
|
if (isSingleTouch) {
|
|
currentTouchLeft = touches[0].pageX;
|
|
currentTouchTop = touches[0].pageY;
|
|
} else {
|
|
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
|
|
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
|
|
}
|
|
|
|
// Store initial positions
|
|
self.__initialTouchLeft = currentTouchLeft;
|
|
self.__initialTouchTop = currentTouchTop;
|
|
|
|
// Store current zoom level
|
|
self.__zoomLevelStart = self.__zoomLevel;
|
|
|
|
// Store initial touch positions
|
|
self.__lastTouchLeft = currentTouchLeft;
|
|
self.__lastTouchTop = currentTouchTop;
|
|
|
|
// Store initial move time stamp
|
|
self.__lastTouchMove = timeStamp;
|
|
|
|
// Reset initial scale
|
|
self.__lastScale = 1;
|
|
|
|
// Reset locking flags
|
|
self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
|
|
self.__enableScrollY = !isSingleTouch && self.options.scrollingY;
|
|
|
|
// Reset tracking flag
|
|
self.__isTracking = true;
|
|
|
|
// Reset deceleration complete flag
|
|
self.__didDecelerationComplete = false;
|
|
|
|
// Dragging starts directly with two fingers, otherwise lazy with an offset
|
|
self.__isDragging = !isSingleTouch;
|
|
|
|
// Some features are disabled in multi touch scenarios
|
|
self.__isSingleTouch = isSingleTouch;
|
|
|
|
// Clearing data structure
|
|
self.__positions = [];
|
|
},
|
|
|
|
/**
|
|
* Touch move handler for scrolling support
|
|
*/
|
|
doTouchMove: function doTouchMove(touches, timeStamp, scale) {
|
|
|
|
// Array-like check is enough here
|
|
if (touches.length == null) {
|
|
throw new Error("Invalid touch list: " + touches);
|
|
}
|
|
|
|
if (timeStamp instanceof Date) {
|
|
timeStamp = timeStamp.valueOf();
|
|
}
|
|
if (typeof timeStamp !== "number") {
|
|
throw new Error("Invalid timestamp value: " + timeStamp);
|
|
}
|
|
|
|
var self = this;
|
|
|
|
// Ignore event when tracking is not enabled (event might be outside of element)
|
|
if (!self.__isTracking) {
|
|
return;
|
|
}
|
|
|
|
var currentTouchLeft, currentTouchTop;
|
|
|
|
// Compute move based around of center of fingers
|
|
if (touches.length === 2) {
|
|
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
|
|
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
|
|
} else {
|
|
currentTouchLeft = touches[0].pageX;
|
|
currentTouchTop = touches[0].pageY;
|
|
}
|
|
|
|
var positions = self.__positions;
|
|
|
|
// Are we already is dragging mode?
|
|
if (self.__isDragging) {
|
|
|
|
// Compute move distance
|
|
var moveX = currentTouchLeft - self.__lastTouchLeft;
|
|
var moveY = currentTouchTop - self.__lastTouchTop;
|
|
|
|
// Read previous scroll position and zooming
|
|
var scrollLeft = self.__scrollLeft;
|
|
var scrollTop = self.__scrollTop;
|
|
var level = self.__zoomLevel;
|
|
|
|
// Work with scaling
|
|
if (scale != null && self.options.zooming) {
|
|
|
|
var oldLevel = level;
|
|
|
|
// Recompute level based on previous scale and new scale
|
|
level = level / self.__lastScale * scale;
|
|
|
|
// Limit level according to configuration
|
|
level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
|
|
|
|
// Only do further compution when change happened
|
|
if (oldLevel !== level) {
|
|
|
|
// Compute relative event position to container
|
|
var currentTouchLeftRel = currentTouchLeft - self.__clientLeft;
|
|
var currentTouchTopRel = currentTouchTop - self.__clientTop;
|
|
|
|
// Recompute left and top coordinates based on new zoom level
|
|
scrollLeft = (currentTouchLeftRel + scrollLeft) * level / oldLevel - currentTouchLeftRel;
|
|
scrollTop = (currentTouchTopRel + scrollTop) * level / oldLevel - currentTouchTopRel;
|
|
|
|
// Recompute max scroll values
|
|
self.__computeScrollMax(level);
|
|
}
|
|
}
|
|
|
|
if (self.__enableScrollX) {
|
|
|
|
scrollLeft -= moveX * this.options.speedMultiplier;
|
|
var maxScrollLeft = self.__maxScrollLeft;
|
|
|
|
if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
|
|
|
|
// Slow down on the edges
|
|
if (self.options.bouncing) {
|
|
|
|
scrollLeft += moveX / 2 * this.options.speedMultiplier;
|
|
} else if (scrollLeft > maxScrollLeft) {
|
|
|
|
scrollLeft = maxScrollLeft;
|
|
} else {
|
|
|
|
scrollLeft = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compute new vertical scroll position
|
|
if (self.__enableScrollY) {
|
|
|
|
scrollTop -= moveY * this.options.speedMultiplier;
|
|
var maxScrollTop = self.__maxScrollTop;
|
|
|
|
if (scrollTop > maxScrollTop || scrollTop < 0) {
|
|
|
|
// Slow down on the edges
|
|
if (self.options.bouncing) {
|
|
|
|
scrollTop += moveY / 2 * this.options.speedMultiplier;
|
|
|
|
// Support pull-to-refresh (only when only y is scrollable)
|
|
if (!self.__enableScrollX && self.__refreshHeight != null) {
|
|
|
|
if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {
|
|
|
|
self.__refreshActive = true;
|
|
if (self.__refreshActivate) {
|
|
self.__refreshActivate();
|
|
}
|
|
} else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {
|
|
|
|
self.__refreshActive = false;
|
|
if (self.__refreshDeactivate) {
|
|
self.__refreshDeactivate();
|
|
}
|
|
}
|
|
}
|
|
} else if (scrollTop > maxScrollTop) {
|
|
|
|
scrollTop = maxScrollTop;
|
|
} else {
|
|
|
|
scrollTop = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep list from growing infinitely (holding min 10, max 20 measure points)
|
|
if (positions.length > 60) {
|
|
positions.splice(0, 30);
|
|
}
|
|
|
|
// Track scroll movement for decleration
|
|
positions.push(scrollLeft, scrollTop, timeStamp);
|
|
|
|
// Sync scroll position
|
|
self.__publish(scrollLeft, scrollTop, level);
|
|
|
|
// Otherwise figure out whether we are switching into dragging mode now.
|
|
} else {
|
|
|
|
var minimumTrackingForScroll = 3;
|
|
var minimumTrackingForDrag = 5;
|
|
|
|
var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
|
|
var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);
|
|
|
|
self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
|
|
self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;
|
|
|
|
var radian = void 0;
|
|
|
|
if (self.options.locking && self.__enableScrollY) {
|
|
radian = radian || Math.atan2(distanceY, distanceX);
|
|
if (radian < Math.PI / 4) {
|
|
self.__enableScrollY = false;
|
|
}
|
|
}
|
|
|
|
if (self.options.locking && self.__enableScrollX) {
|
|
radian = radian || Math.atan2(distanceY, distanceX);
|
|
if (radian > Math.PI / 4) {
|
|
self.__enableScrollX = false;
|
|
}
|
|
}
|
|
|
|
positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);
|
|
|
|
self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
|
|
if (self.__isDragging) {
|
|
self.__interruptedAnimation = false;
|
|
}
|
|
}
|
|
|
|
// Update last touch positions and time stamp for next event
|
|
self.__lastTouchLeft = currentTouchLeft;
|
|
self.__lastTouchTop = currentTouchTop;
|
|
self.__lastTouchMove = timeStamp;
|
|
self.__lastScale = scale;
|
|
},
|
|
|
|
/**
|
|
* Touch end handler for scrolling support
|
|
*/
|
|
doTouchEnd: function doTouchEnd(timeStamp) {
|
|
|
|
if (timeStamp instanceof Date) {
|
|
timeStamp = timeStamp.valueOf();
|
|
}
|
|
if (typeof timeStamp !== "number") {
|
|
throw new Error("Invalid timestamp value: " + timeStamp);
|
|
}
|
|
|
|
var self = this;
|
|
|
|
// Ignore event when tracking is not enabled (no touchstart event on element)
|
|
// This is required as this listener ('touchmove') sits on the document and not on the element itself.
|
|
if (!self.__isTracking) {
|
|
return;
|
|
}
|
|
|
|
// Not touching anymore (when two finger hit the screen there are two touch end events)
|
|
self.__isTracking = false;
|
|
|
|
// Be sure to reset the dragging flag now. Here we also detect whether
|
|
// the finger has moved fast enough to switch into a deceleration animation.
|
|
if (self.__isDragging) {
|
|
|
|
// Reset dragging flag
|
|
self.__isDragging = false;
|
|
|
|
// Start deceleration
|
|
// Verify that the last move detected was in some relevant time frame
|
|
if (self.__isSingleTouch && self.options.animating && timeStamp - self.__lastTouchMove <= 100) {
|
|
|
|
// Then figure out what the scroll position was about 100ms ago
|
|
var positions = self.__positions;
|
|
var endPos = positions.length - 1;
|
|
var startPos = endPos;
|
|
|
|
// Move pointer to position measured 100ms ago
|
|
for (var i = endPos; i > 0 && positions[i] > self.__lastTouchMove - 100; i -= 3) {
|
|
startPos = i;
|
|
}
|
|
|
|
// If start and stop position is identical in a 100ms timeframe,
|
|
// we cannot compute any useful deceleration.
|
|
if (startPos !== endPos) {
|
|
|
|
// Compute relative movement between these two points
|
|
var timeOffset = positions[endPos] - positions[startPos];
|
|
var movedLeft = self.__scrollLeft - positions[startPos - 2];
|
|
var movedTop = self.__scrollTop - positions[startPos - 1];
|
|
|
|
// Based on 50ms compute the movement to apply for each render step
|
|
self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60);
|
|
self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);
|
|
|
|
// How much velocity is required to start the deceleration
|
|
var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? 4 : 1;
|
|
|
|
// Verify that we have enough velocity to start deceleration
|
|
if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {
|
|
|
|
// Deactivate pull-to-refresh when decelerating
|
|
if (!self.__refreshActive) {
|
|
self.__startDeceleration(timeStamp);
|
|
}
|
|
} else {
|
|
self.options.scrollingComplete();
|
|
}
|
|
} else {
|
|
self.options.scrollingComplete();
|
|
}
|
|
} else if (timeStamp - self.__lastTouchMove > 100) {
|
|
self.options.scrollingComplete();
|
|
}
|
|
}
|
|
|
|
// If this was a slower move it is per default non decelerated, but this
|
|
// still means that we want snap back to the bounds which is done here.
|
|
// This is placed outside the condition above to improve edge case stability
|
|
// e.g. touchend fired without enabled dragging. This should normally do not
|
|
// have modified the scroll positions or even showed the scrollbars though.
|
|
if (!self.__isDecelerating) {
|
|
|
|
if (self.__refreshActive && self.__refreshStart) {
|
|
|
|
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
|
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
|
self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);
|
|
|
|
if (self.__refreshStart) {
|
|
self.__refreshStart();
|
|
}
|
|
} else {
|
|
|
|
if (self.__interruptedAnimation || self.__isDragging) {
|
|
self.options.scrollingComplete();
|
|
}
|
|
self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);
|
|
|
|
// Directly signalize deactivation (nothing todo on refresh?)
|
|
if (self.__refreshActive) {
|
|
|
|
self.__refreshActive = false;
|
|
if (self.__refreshDeactivate) {
|
|
self.__refreshDeactivate();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fully cleanup list
|
|
self.__positions.length = 0;
|
|
},
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
PRIVATE API
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Applies the scroll position to the content element
|
|
*
|
|
* @param left {Number} Left scroll position
|
|
* @param top {Number} Top scroll position
|
|
* @param animate {Boolean?false} Whether animation should be used to move to the new coordinates
|
|
*/
|
|
__publish: function __publish(left, top, zoom, animate) {
|
|
|
|
var self = this;
|
|
|
|
// Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
|
|
var wasAnimating = self.__isAnimating;
|
|
if (wasAnimating) {
|
|
_Animate2["default"].stop(wasAnimating);
|
|
self.__isAnimating = false;
|
|
}
|
|
|
|
if (animate && self.options.animating) {
|
|
|
|
// Keep scheduled positions for scrollBy/zoomBy functionality
|
|
self.__scheduledLeft = left;
|
|
self.__scheduledTop = top;
|
|
self.__scheduledZoom = zoom;
|
|
|
|
var oldLeft = self.__scrollLeft;
|
|
var oldTop = self.__scrollTop;
|
|
var oldZoom = self.__zoomLevel;
|
|
|
|
var diffLeft = left - oldLeft;
|
|
var diffTop = top - oldTop;
|
|
var diffZoom = zoom - oldZoom;
|
|
|
|
var step = function step(percent, now, render) {
|
|
|
|
if (render) {
|
|
|
|
self.__scrollLeft = oldLeft + diffLeft * percent;
|
|
self.__scrollTop = oldTop + diffTop * percent;
|
|
self.__zoomLevel = oldZoom + diffZoom * percent;
|
|
|
|
// Push values out
|
|
if (self.__callback) {
|
|
self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel);
|
|
}
|
|
}
|
|
};
|
|
|
|
var verify = function verify(id) {
|
|
return self.__isAnimating === id;
|
|
};
|
|
|
|
var completed = function completed(renderedFramesPerSecond, animationId, wasFinished) {
|
|
if (animationId === self.__isAnimating) {
|
|
self.__isAnimating = false;
|
|
}
|
|
|
|
if (self.__didDecelerationComplete || wasFinished) {
|
|
self.options.scrollingComplete();
|
|
}
|
|
|
|
if (self.options.zooming) {
|
|
self.__computeScrollMax();
|
|
if (self.__zoomComplete) {
|
|
self.__zoomComplete();
|
|
self.__zoomComplete = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
// When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
|
|
self.__isAnimating = _Animate2["default"].start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);
|
|
} else {
|
|
|
|
self.__scheduledLeft = self.__scrollLeft = left;
|
|
self.__scheduledTop = self.__scrollTop = top;
|
|
self.__scheduledZoom = self.__zoomLevel = zoom;
|
|
|
|
// Push values out
|
|
if (self.__callback) {
|
|
self.__callback(left, top, zoom);
|
|
}
|
|
|
|
// Fix max scroll ranges
|
|
if (self.options.zooming) {
|
|
self.__computeScrollMax();
|
|
if (self.__zoomComplete) {
|
|
self.__zoomComplete();
|
|
self.__zoomComplete = null;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Recomputes scroll minimum values based on client dimensions and content dimensions.
|
|
*/
|
|
__computeScrollMax: function __computeScrollMax(zoomLevel) {
|
|
|
|
var self = this;
|
|
|
|
if (zoomLevel == null) {
|
|
zoomLevel = self.__zoomLevel;
|
|
}
|
|
|
|
self.__maxScrollLeft = Math.max(self.__contentWidth * zoomLevel - self.__clientWidth, 0);
|
|
self.__maxScrollTop = Math.max(self.__contentHeight * zoomLevel - self.__clientHeight, 0);
|
|
},
|
|
|
|
/*
|
|
---------------------------------------------------------------------------
|
|
ANIMATION (DECELERATION) SUPPORT
|
|
---------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Called when a touch sequence end and the speed of the finger was high enough
|
|
* to switch into deceleration mode.
|
|
*/
|
|
__startDeceleration: function __startDeceleration(timeStamp) {
|
|
|
|
var self = this;
|
|
|
|
if (self.options.paging) {
|
|
|
|
var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0);
|
|
var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0);
|
|
var clientWidth = self.__clientWidth;
|
|
var clientHeight = self.__clientHeight;
|
|
|
|
// We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
|
|
// Each page should have exactly the size of the client area.
|
|
self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
|
|
self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
|
|
self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
|
|
self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;
|
|
} else {
|
|
|
|
self.__minDecelerationScrollLeft = 0;
|
|
self.__minDecelerationScrollTop = 0;
|
|
self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
|
|
self.__maxDecelerationScrollTop = self.__maxScrollTop;
|
|
}
|
|
|
|
// Wrap class method
|
|
var step = function step(percent, now, render) {
|
|
self.__stepThroughDeceleration(render);
|
|
};
|
|
|
|
// How much velocity is required to keep the deceleration running
|
|
// added by yiminghe
|
|
var minVelocityToKeepDecelerating = self.options.minVelocityToKeepDecelerating;
|
|
|
|
if (!minVelocityToKeepDecelerating) {
|
|
minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.001;
|
|
}
|
|
|
|
// Detect whether it's still worth to continue animating steps
|
|
// If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
|
|
var verify = function verify() {
|
|
var shouldContinue = Math.abs(self.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating;
|
|
if (!shouldContinue) {
|
|
self.__didDecelerationComplete = true;
|
|
}
|
|
return shouldContinue;
|
|
};
|
|
|
|
var completed = function completed(renderedFramesPerSecond, animationId, wasFinished) {
|
|
self.__isDecelerating = false;
|
|
// Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
|
|
// fixed by yiminghe, in case call scrollingComplete twice
|
|
self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping, null, self.__didDecelerationComplete && self.options.scrollingComplete);
|
|
};
|
|
|
|
// Start animation and switch on flag
|
|
self.__isDecelerating = _Animate2["default"].start(step, verify, completed);
|
|
},
|
|
|
|
/**
|
|
* Called on every step of the animation
|
|
*
|
|
* @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only!
|
|
*/
|
|
__stepThroughDeceleration: function __stepThroughDeceleration(render) {
|
|
|
|
var self = this;
|
|
|
|
//
|
|
// COMPUTE NEXT SCROLL POSITION
|
|
//
|
|
|
|
// Add deceleration to scroll position
|
|
var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;
|
|
var scrollTop = self.__scrollTop + self.__decelerationVelocityY;
|
|
|
|
//
|
|
// HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
|
|
//
|
|
|
|
if (!self.options.bouncing) {
|
|
|
|
var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
|
|
if (scrollLeftFixed !== scrollLeft) {
|
|
scrollLeft = scrollLeftFixed;
|
|
self.__decelerationVelocityX = 0;
|
|
}
|
|
|
|
var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
|
|
if (scrollTopFixed !== scrollTop) {
|
|
scrollTop = scrollTopFixed;
|
|
self.__decelerationVelocityY = 0;
|
|
}
|
|
}
|
|
|
|
//
|
|
// UPDATE SCROLL POSITION
|
|
//
|
|
|
|
if (render) {
|
|
|
|
self.__publish(scrollLeft, scrollTop, self.__zoomLevel);
|
|
} else {
|
|
|
|
self.__scrollLeft = scrollLeft;
|
|
self.__scrollTop = scrollTop;
|
|
}
|
|
|
|
//
|
|
// SLOW DOWN
|
|
//
|
|
|
|
// Slow down velocity on every iteration
|
|
if (!self.options.paging) {
|
|
|
|
// This is the factor applied to every iteration of the animation
|
|
// to slow down the process. This should emulate natural behavior where
|
|
// objects slow down when the initiator of the movement is removed
|
|
var frictionFactor = 0.95;
|
|
|
|
self.__decelerationVelocityX *= frictionFactor;
|
|
self.__decelerationVelocityY *= frictionFactor;
|
|
}
|
|
|
|
//
|
|
// BOUNCING SUPPORT
|
|
//
|
|
|
|
if (self.options.bouncing) {
|
|
|
|
var scrollOutsideX = 0;
|
|
var scrollOutsideY = 0;
|
|
|
|
// This configures the amount of change applied to deceleration/acceleration when reaching boundaries
|
|
var penetrationDeceleration = self.options.penetrationDeceleration;
|
|
var penetrationAcceleration = self.options.penetrationAcceleration;
|
|
|
|
// Check limits
|
|
if (scrollLeft < self.__minDecelerationScrollLeft) {
|
|
scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
|
|
} else if (scrollLeft > self.__maxDecelerationScrollLeft) {
|
|
scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
|
|
}
|
|
|
|
if (scrollTop < self.__minDecelerationScrollTop) {
|
|
scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
|
|
} else if (scrollTop > self.__maxDecelerationScrollTop) {
|
|
scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
|
|
}
|
|
|
|
// Slow down until slow enough, then flip back to snap position
|
|
if (scrollOutsideX !== 0) {
|
|
if (scrollOutsideX * self.__decelerationVelocityX <= 0) {
|
|
self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
|
|
} else {
|
|
self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
|
|
}
|
|
}
|
|
|
|
if (scrollOutsideY !== 0) {
|
|
if (scrollOutsideY * self.__decelerationVelocityY <= 0) {
|
|
self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
|
|
} else {
|
|
self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Copy over members to prototype
|
|
for (var key in members) {
|
|
Scroller.prototype[key] = members[key];
|
|
}
|
|
|
|
exports["default"] = Scroller;
|
|
module.exports = exports['default']; |