vue-easyrefresh
Version:
Vue component for pull-refresh and push-load.
1,358 lines (1,004 loc) • 59.5 kB
JavaScript
(function (window) {
var NOOP = function () {
}
var core = (function Animate(global) {
var time = Date.now || function () {
return +new Date()
}
var desiredFrames = 60
var millisecondsPerSecond = 1000
var running = {}
var counter = 1
var core = {effect: {}}
core.effect.Animate = {
/**
* A requestAnimationFrame wrapper / polyfill.
*
* @param callback {Function} The callback to be invoked before the next repaint.
* @param root {HTMLElement} The root element for the repaint
*/
requestAnimationFrame: (function () {
// Check for request animation Frame support
var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame
var isNative = !!requestFrame
if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
isNative = false
}
if (isNative) {
return function (callback, root) {
requestFrame(callback, root)
}
}
var TARGET_FPS = 60
var requests = {}
var requestCount = 0
var rafHandle = 1
var intervalHandle = null
var lastActive = +new Date()
return function (callback, root) {
var callbackHandle = rafHandle++
// Store callback
requests[callbackHandle] = callback
requestCount++
// Create timeout at first request
if (intervalHandle === null) {
intervalHandle = setInterval(function () {
var time = +new Date()
var currentRequests = requests
// Reset data structure before executing callbacks
requests = {}
requestCount = 0
for (var key in currentRequests) {
if (currentRequests.hasOwnProperty(key)) {
currentRequests[key](time)
lastActive = time
}
}
// Disable the timeout when nothing happens for a certain
// period of time
if (time - lastActive > 2500) {
clearInterval(intervalHandle)
intervalHandle = null
}
}, 1000 / TARGET_FPS)
}
return callbackHandle
}
})(),
/**
* Stops the given animation.
*
* @param id {Integer} Unique animation ID
* @return {Boolean} Whether the animation was stopped (aka, was running before)
*/
stop: function (id) {
var cleared = running[id] != null
if (cleared) {
running[id] = null
}
return cleared
},
/**
* Whether the given animation is still running.
*
* @param id {Integer} Unique animation ID
* @return {Boolean} Whether the animation is still running
*/
isRunning: function (id) {
return running[id] != null
},
/**
* Start the animation.
*
* @param stepCallback {Function} Pointer to function which is executed on every step.
* Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
* @param verifyCallback {Function} Executed before every animation step.
* Signature of the method should be `function() { return continueWithAnimation; }`
* @param completedCallback {Function}
* Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
* @param duration {Integer} Milliseconds to run the animation
* @param easingMethod {Function} Pointer to easing function
* Signature of the method should be `function(percent) { return modifiedValue; }`
* @param root {Element ? document.body} Render root, when available. Used for internal
* usage of requestAnimationFrame.
* @return {Integer} Identifier of animation. Can be used to stop it any time.
*/
start: function (stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
var start = time()
var lastFrame = start
var percent = 0
var dropCounter = 0
var id = counter++
if (!root) {
root = document.body
}
// Compacting running db automatically every few new animations
if (id % 20 === 0) {
var newRunning = {}
for (var usedId in running) {
newRunning[usedId] = true
}
running = newRunning
}
// This is the internal step method which is called every few milliseconds
var step = function (virtual) {
// Normalize virtual value
var render = virtual !== true
// Get current time
var now = time()
// Verification is executed before next animation step
if (!running[id] || (verifyCallback && !verifyCallback(id))) {
running[id] = null
completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false)
return
}
// For the current rendering to apply let's update omitted steps in memory.
// This is important to bring internal state variables up-to-date with progress in time.
if (render) {
var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1
for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
step(true)
dropCounter++
}
}
// Compute percent value
if (duration) {
percent = (now - start) / duration
if (percent > 1) {
percent = 1
}
}
// Execute step callback, then...
var value = easingMethod ? easingMethod(percent) : percent
if ((stepCallback(value, now, render) === false || percent === 1) && render) {
running[id] = null
completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null)
} else if (render) {
lastFrame = now
core.effect.Animate.requestAnimationFrame(step, root)
}
}
// Mark as running
running[id] = true
// Init first step
core.effect.Animate.requestAnimationFrame(step, root)
// Return unique animation ID
return id
}
}
return core
})(window)
/**
* A pure logic 'component' for 'virtual' scrolling/zooming.
*/
var Scroller = function (callback, options) {
this.__callback = callback
// core = animate;
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 (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 (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 (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 (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 (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 (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() {
// 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() {
var self = this;
self.__refreshActive = false;
if (self.__refreshDeactivate) {
self.__refreshDeactivate();
}
self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
},*/
/**
* 触发上拉刷新
*/
triggerPullToRefresh: function(height, callBack) {
this.__publish(this.__scrollLeft, -height, this.__zoomLevel, true, callBack)
},
/**
* 完成下拉刷新
*/
finishPullToRefresh: function() {
//this.scrollTo(this.__scrollLeft, this.__scrollTop, true)
},
/**
* 触发上拉加载
*/
triggerPushToLoad: function(height, callBack, scroll) {
if (scroll) {
this.__publish(this.__scrollLeft, this.__maxScrollTop + height, this.__zoomLevel, true, callBack)
} else {
this.__publish(this.__scrollLeft, this.__scrollTop, this.__zoomLevel, true)
callBack(false)
}
},
/**
* 完成上拉加载
*/
finishPushToLoad: function() {
this.scrollTo(this.__scrollLeft, this.__maxScrollTop, true)
},
/**
* 计算列表大小
*/
computeScrollMax: function() {
this.__computeScrollMax()
},
/**
* 滚动到指定高度
*/
scrollPublishTo: function(left, top, animate) {
this.__publish(this.__scrollLeft, top, this.__zoomLevel, animate)
},
/**
* Returns the scroll position and zooming values
*
* @return {Map} `left` and `top` scroll position and `zoom` level
*/
getValues: function () {
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 () {
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 (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) {
core.effect.Animate.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 (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 (left, top, animate, zoom) {
var self = this
// Stop deceleration
if (self.__isDecelerating) {
core.effect.Animate.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
}
// 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 (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 (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 (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) {
core.effect.Animate.stop(self.__isDecelerating)
self.__isDecelerating = false
self.__interruptedAnimation = true
}
// Stop animation
if (self.__isAnimating) {
core.effect.Animate.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 (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 = self.options.locking ? 3 : 0
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
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 (timeStamp, refreshActivate) {
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 && !refreshActivate) {
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 (left, top, zoom, animate, callBack) {
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) {
core.effect.Animate.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 (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 (id) {
return self.__isAnimating === id
}
var completed = function (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
}
}
if (callBack) {
callBack(true)
}
}
// When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
self.__isAnimating = core.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeO