UNPKG

vue-easyrefresh

Version:

Vue component for pull-refresh and push-load.

1,358 lines (1,004 loc) 59.5 kB
(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