932 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			932 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| /* istanbul ignore file */
 | |
| /*
 | |
|  * Based on the work of: 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
 | |
|  *
 | |
|  */
 | |
| 
 | |
| import {noop, warn, extend} from './index'
 | |
| 
 | |
| import Animate, {easeOutCubic, easeInOutCubic} from './animate'
 | |
| 
 | |
| const members = {
 | |
|   _isSingleTouch: false,
 | |
|   _isTracking: false,
 | |
|   _didDecelerationComplete: false,
 | |
|   _isGesturing: false,
 | |
|   _isDragging: false,
 | |
|   _isDecelerating: false,
 | |
|   _isAnimating: false,
 | |
|   _clientLeft: 0,
 | |
|   _clientTop: 0,
 | |
|   _clientWidth: 0,
 | |
|   _clientHeight: 0,
 | |
|   _contentWidth: 0,
 | |
|   _contentHeight: 0,
 | |
|   _snapWidth: 100,
 | |
|   _snapHeight: 100,
 | |
|   _refreshHeight: null,
 | |
|   _refreshActive: false,
 | |
|   _refreshActivate: null,
 | |
|   _refreshDeactivate: null,
 | |
|   _refreshStart: null,
 | |
|   _zoomLevel: 1,
 | |
|   _scrollLeft: 0,
 | |
|   _scrollTop: 0,
 | |
|   _maxScrollLeft: 0,
 | |
|   _maxScrollTop: 0,
 | |
|   _scheduledLeft: 0,
 | |
|   _scheduledTop: 0,
 | |
|   _lastTouchLeft: null,
 | |
|   _lastTouchTop: null,
 | |
|   _lastTouchMove: null,
 | |
|   _positions: null,
 | |
|   _minDecelerationScrollLeft: null,
 | |
|   _minDecelerationScrollTop: null,
 | |
|   _maxDecelerationScrollLeft: null,
 | |
|   _maxDecelerationScrollTop: null,
 | |
|   _decelerationVelocityX: null,
 | |
|   _decelerationVelocityY: null,
 | |
| }
 | |
| /* istanbul ignore next */
 | |
| export default class Scroller {
 | |
|   constructor(callback = noop, options) {
 | |
|     this.options = {
 | |
|       scrollingX: true,
 | |
|       scrollingY: true,
 | |
|       animating: true,
 | |
|       animationDuration: 250,
 | |
|       inRequestAnimationFrame: false,
 | |
|       bouncing: true,
 | |
|       locking: true,
 | |
|       paging: false,
 | |
|       snapping: false,
 | |
|       snappingVelocity: 4,
 | |
|       zooming: false,
 | |
|       minZoom: 0.5,
 | |
|       maxZoom: 3,
 | |
|       speedMultiplier: 1,
 | |
|       scrollingComplete: noop,
 | |
|       penetrationDeceleration: 0.03,
 | |
|       penetrationAcceleration: 0.08,
 | |
|     }
 | |
|     extend(this.options, options)
 | |
|     this._callback = callback
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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(clientWidth, clientHeight, contentWidth, contentHeight) {
 | |
|     // Only update values which are defined
 | |
|     if (clientWidth === +clientWidth) {
 | |
|       this._clientWidth = clientWidth
 | |
|     }
 | |
| 
 | |
|     if (clientHeight === +clientHeight) {
 | |
|       this._clientHeight = clientHeight
 | |
|     }
 | |
| 
 | |
|     if (contentWidth === +contentWidth) {
 | |
|       this._contentWidth = contentWidth
 | |
|     }
 | |
| 
 | |
|     if (contentHeight === +contentHeight) {
 | |
|       this._contentHeight = contentHeight
 | |
|     }
 | |
| 
 | |
|     // Refresh maximums
 | |
|     this._computeScrollMax()
 | |
| 
 | |
|     // Refresh scroll position
 | |
|     this.scrollTo(this._scrollLeft, this._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(left, top) {
 | |
|     this._clientLeft = left || 0
 | |
|     this._clientTop = top || 0
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Configures the snapping (when snapping is active)
 | |
|    *
 | |
|    * @param width {Integer} Snapping width
 | |
|    * @param height {Integer} Snapping height
 | |
|    */
 | |
|   setSnapSize(width, height) {
 | |
|     this._snapWidth = width
 | |
|     this._snapHeight = height
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the scroll position and zooming values
 | |
|    *
 | |
|    * @return {Map} `left` and `top` scroll position and `zoom` level
 | |
|    */
 | |
|   getValues() {
 | |
|     return {
 | |
|       left: this._scrollLeft,
 | |
|       top: this._scrollTop,
 | |
|       zoom: this._zoomLevel,
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the maximum scroll values
 | |
|    *
 | |
|    * @return {Map} `left` and `top` maximum scroll values
 | |
|    */
 | |
|   getScrollMax() {
 | |
|     return {
 | |
|       left: this._maxScrollLeft,
 | |
|       top: this._maxScrollTop,
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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(height, activateCallback, deactivateCallback, startCallback) {
 | |
|     this._refreshHeight = height
 | |
|     this._refreshActivate = activateCallback
 | |
|     this._refreshDeactivate = deactivateCallback
 | |
|     this._refreshStart = startCallback
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Starts pull-to-refresh manually.
 | |
|    */
 | |
|   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() {
 | |
|     this._refreshActive = false
 | |
| 
 | |
|     if (this._refreshDeactivate) {
 | |
|       this._refreshDeactivate()
 | |
|     }
 | |
| 
 | |
|     this.scrollTo(this._scrollLeft, this._scrollTop, true)
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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(left, top, animate, zoom = 1) {
 | |
|     // Stop deceleration
 | |
|     if (this._isDecelerating) {
 | |
|       Animate.stop(this._isDecelerating)
 | |
|       this._isDecelerating = false
 | |
|     }
 | |
| 
 | |
|     // Correct coordinates based on new zoom level
 | |
|     if (zoom != null && zoom !== this._zoomLevel) {
 | |
|       if (!this.options.zooming) {
 | |
|         warn('Zooming is not enabled!')
 | |
|       }
 | |
|       zoom = zoom ? zoom : 1
 | |
|       left *= zoom
 | |
|       top *= zoom
 | |
| 
 | |
|       // // Recompute maximum values while temporary tweaking maximum scroll ranges
 | |
|       this._computeScrollMax(zoom)
 | |
|     } else {
 | |
|       // Keep zoom when not defined
 | |
|       zoom = this._zoomLevel
 | |
|     }
 | |
| 
 | |
|     if (!this.options.scrollingX) {
 | |
|       left = this._scrollLeft
 | |
|     } else {
 | |
|       if (this.options.paging) {
 | |
|         left = Math.round(left / this._clientWidth) * this._clientWidth
 | |
|       } else if (this.options.snapping) {
 | |
|         left = Math.round(left / this._snapWidth) * this._snapWidth
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!this.options.scrollingY) {
 | |
|       top = this._scrollTop
 | |
|     } else {
 | |
|       if (this.options.paging) {
 | |
|         top = Math.round(top / this._clientHeight) * this._clientHeight
 | |
|       } else if (this.options.snapping) {
 | |
|         top = Math.round(top / this._snapHeight) * this._snapHeight
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Limit for allowed ranges
 | |
|     left = Math.max(Math.min(this._maxScrollLeft, left), 0)
 | |
|     top = Math.max(Math.min(this._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 === this._scrollLeft && top === this._scrollTop) {
 | |
|       animate = false
 | |
|     }
 | |
|     // Publish new values
 | |
|     if (!this._isTracking) {
 | |
|       this._publish(left, top, zoom, animate)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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(level, animate, originLeft, originTop, callback) {
 | |
|     if (!this.options.zooming) {
 | |
|       warn('Zooming is not enabled!')
 | |
|     }
 | |
| 
 | |
|     // Add callback if exists
 | |
|     if (callback) {
 | |
|       this._zoomComplete = callback
 | |
|     }
 | |
| 
 | |
|     // Stop deceleration
 | |
|     if (this._isDecelerating) {
 | |
|       Animate.stop(this._isDecelerating)
 | |
|       this._isDecelerating = false
 | |
|     }
 | |
| 
 | |
|     const oldLevel = this._zoomLevel
 | |
| 
 | |
|     // Normalize input origin to center of viewport if not defined
 | |
|     if (originLeft == null) {
 | |
|       originLeft = this._clientWidth / 2
 | |
|     }
 | |
| 
 | |
|     if (originTop == null) {
 | |
|       originTop = this._clientHeight / 2
 | |
|     }
 | |
| 
 | |
|     // Limit level according to configuration
 | |
|     level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom)
 | |
| 
 | |
|     // Recompute maximum values while temporary tweaking maximum scroll ranges
 | |
|     this._computeScrollMax(level)
 | |
| 
 | |
|     // Recompute left and top coordinates based on new zoom level
 | |
|     let left = (originLeft + this._scrollLeft) * level / oldLevel - originLeft
 | |
|     let top = (originTop + this._scrollTop) * level / oldLevel - originTop
 | |
| 
 | |
|     // Limit x-axis
 | |
|     if (left > this._maxScrollLeft) {
 | |
|       left = this._maxScrollLeft
 | |
|     } else if (left < 0) {
 | |
|       left = 0
 | |
|     }
 | |
| 
 | |
|     // Limit y-axis
 | |
|     if (top > this._maxScrollTop) {
 | |
|       top = this._maxScrollTop
 | |
|     } else if (top < 0) {
 | |
|       top = 0
 | |
|     }
 | |
| 
 | |
|     // Push values out
 | |
|     this._publish(left, top, level, animate)
 | |
|   }
 | |
| 
 | |
|   doTouchStart(touches, timeStamp) {
 | |
|     // Array-like check is enough here
 | |
|     if (touches.length == null) {
 | |
|       warn(`Invalid touch list: ${touches}`)
 | |
|     }
 | |
|     if (timeStamp instanceof Date) {
 | |
|       timeStamp = timeStamp.valueOf()
 | |
|     }
 | |
|     if (typeof timeStamp !== 'number') {
 | |
|       warn(`Invalid timestamp value: ${timeStamp}`)
 | |
|     }
 | |
| 
 | |
|     // Reset interruptedAnimation flag
 | |
|     this._interruptedAnimation = true
 | |
| 
 | |
|     // Stop deceleration
 | |
|     if (this._isDecelerating) {
 | |
|       Animate.stop(this._isDecelerating)
 | |
|       this._isDecelerating = false
 | |
|       this._interruptedAnimation = true
 | |
|     }
 | |
| 
 | |
|     // Stop animation
 | |
|     if (this._isAnimating) {
 | |
|       Animate.stop(this._isAnimating)
 | |
|       this._isAnimating = false
 | |
|       this._interruptedAnimation = true
 | |
|     }
 | |
| 
 | |
|     // Use center point when dealing with two fingers
 | |
|     const isSingleTouch = touches.length === 1
 | |
|     let currentTouchLeft, currentTouchTop
 | |
| 
 | |
|     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
 | |
|     this._initialTouchLeft = currentTouchLeft
 | |
|     this._initialTouchTop = currentTouchTop
 | |
| 
 | |
|     // Store current zoom level
 | |
|     this._zoomLevelStart = this._zoomLevel
 | |
| 
 | |
|     // Store initial touch positions
 | |
|     this._lastTouchLeft = currentTouchLeft
 | |
|     this._lastTouchTop = currentTouchTop
 | |
| 
 | |
|     // Store initial move time stamp
 | |
|     this._lastTouchMove = timeStamp
 | |
| 
 | |
|     // Reset initial scale
 | |
|     this._lastScale = 1
 | |
| 
 | |
|     // Reset locking flags
 | |
|     this._enableScrollX = !isSingleTouch && this.options.scrollingX
 | |
|     this._enableScrollY = !isSingleTouch && this.options.scrollingY
 | |
| 
 | |
|     // Reset tracking flag
 | |
|     this._isTracking = true
 | |
| 
 | |
|     // Reset deceleration complete flag
 | |
|     this._didDecelerationComplete = false
 | |
| 
 | |
|     // Dragging starts directly with two fingers, otherwise lazy with an offset
 | |
|     this._isDragging = !isSingleTouch
 | |
| 
 | |
|     // Some features are disabled in multi touch scenarios
 | |
|     this._isSingleTouch = isSingleTouch
 | |
| 
 | |
|     // Clearing data structure
 | |
|     this._positions = []
 | |
|   }
 | |
| 
 | |
|   doTouchMove(touches, timeStamp, scale) {
 | |
|     // Array-like check is enough here
 | |
|     if (touches.length == null) {
 | |
|       warn(`Invalid touch list: ${touches}`)
 | |
|     }
 | |
| 
 | |
|     if (timeStamp instanceof Date) {
 | |
|       timeStamp = timeStamp.valueOf()
 | |
|     }
 | |
| 
 | |
|     if (typeof timeStamp !== 'number') {
 | |
|       warn(`Invalid timestamp value: ${timeStamp}`)
 | |
|     }
 | |
| 
 | |
|     // Ignore event when tracking is not enabled (event might be outside of element)
 | |
|     if (!this._isTracking) {
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     let 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
 | |
|     }
 | |
| 
 | |
|     const positions = this._positions
 | |
| 
 | |
|     // Are we already is dragging mode?
 | |
|     if (this._isDragging) {
 | |
|       // Compute move distance
 | |
|       const moveX = currentTouchLeft - this._lastTouchLeft
 | |
|       const moveY = currentTouchTop - this._lastTouchTop
 | |
| 
 | |
|       // Read previous scroll position and zooming
 | |
|       let scrollLeft = this._scrollLeft
 | |
|       let scrollTop = this._scrollTop
 | |
|       let level = this._zoomLevel
 | |
| 
 | |
|       // Work with scaling
 | |
|       if (scale != null && this.options.zooming) {
 | |
|         const oldLevel = level
 | |
| 
 | |
|         // Recompute level based on previous scale and new scale
 | |
|         level = level / this.__lastScale * scale
 | |
| 
 | |
|         // Limit level according to configuration
 | |
|         level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom)
 | |
| 
 | |
|         // Only do further compution when change happened
 | |
|         if (oldLevel !== level) {
 | |
|           // Compute relative event position to container
 | |
|           var currentTouchLeftRel = currentTouchLeft - this._clientLeft
 | |
|           var currentTouchTopRel = currentTouchTop - this._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
 | |
|           this.__computeScrollMax(level)
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (this._enableScrollX) {
 | |
|         scrollLeft -= moveX * this.options.speedMultiplier
 | |
|         const maxScrollLeft = this._maxScrollLeft
 | |
| 
 | |
|         if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
 | |
|           // Slow down on the edges
 | |
|           if (this.options.bouncing) {
 | |
|             scrollLeft += moveX / 2 * this.options.speedMultiplier
 | |
|           } else if (scrollLeft > maxScrollLeft) {
 | |
|             scrollLeft = maxScrollLeft
 | |
|           } else {
 | |
|             scrollLeft = 0
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // Compute new vertical scroll position
 | |
|       if (this._enableScrollY) {
 | |
|         scrollTop -= moveY * this.options.speedMultiplier
 | |
|         const maxScrollTop = this._maxScrollTop
 | |
|         if (scrollTop > maxScrollTop || scrollTop < 0) {
 | |
|           // Slow down on the edges
 | |
|           if (this.options.bouncing) {
 | |
|             scrollTop += moveY / 2 * this.options.speedMultiplier
 | |
|             // Support pull-to-refresh (only when only y is scrollable)
 | |
|             if (!this._enableScrollX && this._refreshHeight != null) {
 | |
|               if (!this._refreshActive && scrollTop <= -this._refreshHeight) {
 | |
|                 this._refreshActive = true
 | |
|                 if (this._refreshActivate) {
 | |
|                   this._refreshActivate()
 | |
|                 }
 | |
|               } else if (this._refreshActive && scrollTop > -this._refreshHeight) {
 | |
|                 this._refreshActive = false
 | |
|                 if (this._refreshDeactivate) {
 | |
|                   this._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
 | |
|       this._publish(scrollLeft, scrollTop)
 | |
| 
 | |
|       // Otherwise figure out whether we are switching into dragging mode now.
 | |
|     } else {
 | |
|       const minimumTrackingForScroll = this.options.locking ? 3 : 0
 | |
|       const minimumTrackingForDrag = 5
 | |
| 
 | |
|       const distanceX = Math.abs(currentTouchLeft - this._initialTouchLeft)
 | |
|       const distanceY = Math.abs(currentTouchTop - this._initialTouchTop)
 | |
| 
 | |
|       this._enableScrollX = this.options.scrollingX && distanceX >= minimumTrackingForScroll
 | |
|       this._enableScrollY = this.options.scrollingY && distanceY >= minimumTrackingForScroll
 | |
| 
 | |
|       positions.push(this._scrollLeft, this._scrollTop, timeStamp)
 | |
| 
 | |
|       this._isDragging =
 | |
|         (this._enableScrollX || this._enableScrollY) &&
 | |
|         (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag)
 | |
|       if (this._isDragging) {
 | |
|         this._interruptedAnimation = false
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Update last touch positions and time stamp for next event
 | |
|     this._lastTouchLeft = currentTouchLeft
 | |
|     this._lastTouchTop = currentTouchTop
 | |
|     this._lastTouchMove = timeStamp
 | |
|   }
 | |
| 
 | |
|   doTouchEnd(timeStamp) {
 | |
|     if (timeStamp instanceof Date) {
 | |
|       timeStamp = timeStamp.valueOf()
 | |
|     }
 | |
| 
 | |
|     if (typeof timeStamp !== 'number') {
 | |
|       warn(`Invalid timestamp value: ${timeStamp}`)
 | |
|     }
 | |
|     // 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 itthis.
 | |
|     if (!this._isTracking) {
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     // Not touching anymore (when two finger hit the screen there are two touch end events)
 | |
|     this._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 (this._isDragging) {
 | |
|       // Reset dragging flag
 | |
|       this._isDragging = false
 | |
| 
 | |
|       // Start deceleration
 | |
|       // Verify that the last move detected was in some relevant time frame
 | |
|       if (this._isSingleTouch && this.options.animating && timeStamp - this._lastTouchMove <= 100) {
 | |
|         // Then figure out what the scroll position was about 100ms ago
 | |
|         const positions = this._positions
 | |
|         const endPos = positions.length - 1
 | |
|         let startPos = endPos
 | |
| 
 | |
|         // Move pointer to position measured 100ms ago
 | |
|         for (let i = endPos; i > 0 && positions[i] > this._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
 | |
|           const timeOffset = positions[endPos] - positions[startPos]
 | |
|           const movedLeft = this._scrollLeft - positions[startPos - 2]
 | |
|           const movedTop = this._scrollTop - positions[startPos - 1]
 | |
| 
 | |
|           // Based on 50ms compute the movement to apply for each render step
 | |
|           this._decelerationVelocityX = movedLeft / timeOffset * (1000 / 60)
 | |
|           this._decelerationVelocityY = movedTop / timeOffset * (1000 / 60)
 | |
| 
 | |
|           // How much velocity is required to start the deceleration
 | |
|           const minVelocityToStartDeceleration =
 | |
|             this.options.paging || this.options.snapping ? this.options.snappingVelocity : 1
 | |
| 
 | |
|           // Verify that we have enough velocity to start deceleration
 | |
|           if (
 | |
|             Math.abs(this._decelerationVelocityX) > minVelocityToStartDeceleration ||
 | |
|             Math.abs(this._decelerationVelocityY) > minVelocityToStartDeceleration
 | |
|           ) {
 | |
|             // Deactivate pull-to-refresh when decelerating
 | |
|             if (!this._refreshActive) {
 | |
|               this._startDeceleration(timeStamp)
 | |
|             }
 | |
|           } else {
 | |
|             this.options.scrollingComplete()
 | |
|           }
 | |
|         } else {
 | |
|           this.options.scrollingComplete()
 | |
|         }
 | |
|       } else if (timeStamp - this._lastTouchMove > 100) {
 | |
|         !this.options.snapping && this.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 (!this._isDecelerating) {
 | |
|       if (this._refreshActive && this._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
 | |
|         this._publish(this._scrollLeft, -this._refreshHeight, this._zoomLevel, true)
 | |
| 
 | |
|         if (this._refreshStart) {
 | |
|           this._refreshStart()
 | |
|         }
 | |
|       } else {
 | |
|         if (this._interruptedAnimation || this._isDragging) {
 | |
|           this.options.scrollingComplete()
 | |
|         }
 | |
| 
 | |
|         this.scrollTo(this._scrollLeft, this._scrollTop, true, this._zoomLevel)
 | |
|         // Directly signalize deactivation (nothing todo on refresh?)
 | |
|         if (this._refreshActive) {
 | |
|           this._refreshActive = false
 | |
|           if (this._refreshDeactivate) {
 | |
|             this._refreshDeactivate()
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Fully cleanup list
 | |
|     this._positions.length = 0
 | |
|   }
 | |
| 
 | |
|   _publish(left, top, zoom = 1, animate = false) {
 | |
|     // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
 | |
|     const wasAnimating = this._isAnimating
 | |
| 
 | |
|     if (wasAnimating) {
 | |
|       Animate.stop(wasAnimating)
 | |
|       this._isAnimating = false
 | |
|     }
 | |
| 
 | |
|     if (animate && this.options.animating) {
 | |
|       // Keep scheduled positions for scrollBy/zoomBy functionality
 | |
|       this._scheduledLeft = left
 | |
|       this._scheduledTop = top
 | |
|       this._scheduledZoom = zoom
 | |
| 
 | |
|       const oldLeft = this._scrollLeft
 | |
|       const oldTop = this._scrollTop
 | |
|       const oldZoom = this._zoomLevel
 | |
| 
 | |
|       const diffLeft = left - oldLeft
 | |
|       const diffTop = top - oldTop
 | |
|       const diffZoom = zoom - oldZoom
 | |
| 
 | |
|       const step = (percent, now, render) => {
 | |
|         if (render) {
 | |
|           this._scrollLeft = oldLeft + diffLeft * percent
 | |
|           this._scrollTop = oldTop + diffTop * percent
 | |
|           this._zoomLevel = oldZoom + diffZoom * percent
 | |
|           // Push values out
 | |
|           if (this._callback) {
 | |
|             this._callback(this._scrollLeft, this._scrollTop, this._zoomLevel)
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       const verify = id => {
 | |
|         return this._isAnimating === id
 | |
|       }
 | |
| 
 | |
|       const completed = (renderedFramesPerSecond, animationId, wasFinished) => {
 | |
|         if (animationId === this._isAnimating) {
 | |
|           this._isAnimating = false
 | |
|         }
 | |
| 
 | |
|         if (this._didDecelerationComplete || wasFinished) {
 | |
|           this.options.scrollingComplete()
 | |
|         }
 | |
| 
 | |
|         if (this.options.zooming) {
 | |
|           this._computeScrollMax()
 | |
|           if (this._zoomComplete) {
 | |
|             this._zoomComplete()
 | |
|             this._zoomComplete = null
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       const doAnimation = () => {
 | |
|         // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
 | |
|         this._isAnimating = Animate.start(
 | |
|           step,
 | |
|           verify,
 | |
|           completed,
 | |
|           this.options.animationDuration,
 | |
|           wasAnimating ? easeOutCubic : easeInOutCubic,
 | |
|         )
 | |
|       }
 | |
| 
 | |
|       if (this.options.inRequestAnimationFrame) {
 | |
|         Animate.requestAnimationFrame(() => {
 | |
|           doAnimation()
 | |
|         })
 | |
|       } else {
 | |
|         doAnimation()
 | |
|       }
 | |
|     } else {
 | |
|       this._scheduledLeft = this._scrollLeft = left
 | |
|       this._scheduledTop = this._scrollTop = top
 | |
|       this._scheduledZoom = this._zoomLevel = zoom
 | |
| 
 | |
|       // Push values out
 | |
|       if (this._callback) {
 | |
|         this._callback(left, top)
 | |
|       }
 | |
| 
 | |
|       // Fix max scroll ranges
 | |
|       if (this.options.zooming) {
 | |
|         this._computeScrollMax()
 | |
|         if (this._zoomComplete) {
 | |
|           this._zoomComplete()
 | |
|           this._zoomComplete = null
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _computeScrollMax(zoomLevel) {
 | |
|     if (zoomLevel == null) {
 | |
|       zoomLevel = this._zoomLevel
 | |
|     }
 | |
| 
 | |
|     this._maxScrollLeft = Math.max(this._contentWidth * zoomLevel - this._clientWidth, 0)
 | |
|     this._maxScrollTop = Math.max(this._contentHeight * zoomLevel - this._clientHeight, 0)
 | |
|   }
 | |
| 
 | |
|   _startDeceleration(timeStamp) {
 | |
|     if (this.options.paging) {
 | |
|       const scrollLeft = Math.max(Math.min(this._scrollLeft, this._maxScrollLeft), 0)
 | |
|       const scrollTop = Math.max(Math.min(this._scrollTop, this._maxScrollTop), 0)
 | |
|       const clientWidth = this._clientWidth
 | |
|       const clientHeight = this._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.
 | |
|       this._minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth
 | |
|       this._minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight
 | |
|       this._maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth
 | |
|       this._maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight
 | |
|     } else {
 | |
|       this._minDecelerationScrollLeft = 0
 | |
|       this._minDecelerationScrollTop = 0
 | |
|       this._maxDecelerationScrollLeft = this._maxScrollLeft
 | |
|       this._maxDecelerationScrollTop = this._maxScrollTop
 | |
|     }
 | |
| 
 | |
|     // Wrap class method
 | |
|     const step = (percent, now, render) => {
 | |
|       this._stepThroughDeceleration(render)
 | |
|     }
 | |
| 
 | |
|     // How much velocity is required to keep the deceleration running
 | |
|     const minVelocityToKeepDecelerating = this.options.snapping ? this.options.snappingVelocity : 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.
 | |
|     const verify = () => {
 | |
|       const shouldContinue =
 | |
|         Math.abs(this._decelerationVelocityX) >= minVelocityToKeepDecelerating ||
 | |
|         Math.abs(this._decelerationVelocityY) >= minVelocityToKeepDecelerating
 | |
|       if (!shouldContinue) {
 | |
|         this._didDecelerationComplete = true
 | |
|       }
 | |
|       return shouldContinue
 | |
|     }
 | |
| 
 | |
|     const completed = (renderedFramesPerSecond, animationId, wasFinished) => {
 | |
|       this._isDecelerating = false
 | |
|       // if (this._didDecelerationComplete) {
 | |
|       //   this.options.scrollingComplete()
 | |
|       // }
 | |
| 
 | |
|       // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
 | |
|       this.scrollTo(this._scrollLeft, this._scrollTop, this.options.snapping)
 | |
|     }
 | |
| 
 | |
|     // Start animation and switch on flag
 | |
|     this._isDecelerating = Animate.start(step, verify, completed)
 | |
|   }
 | |
| 
 | |
|   _stepThroughDeceleration(render) {
 | |
|     //
 | |
|     // COMPUTE NEXT SCROLL POSITION
 | |
|     //
 | |
| 
 | |
|     // Add deceleration to scroll position
 | |
|     let scrollLeft = this._scrollLeft + this._decelerationVelocityX
 | |
|     let scrollTop = this._scrollTop + this._decelerationVelocityY
 | |
| 
 | |
|     //
 | |
|     // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
 | |
|     //
 | |
| 
 | |
|     if (!this.options.bouncing) {
 | |
|       var scrollLeftFixed = Math.max(
 | |
|         Math.min(this._maxDecelerationScrollLeft, scrollLeft),
 | |
|         this._minDecelerationScrollLeft,
 | |
|       )
 | |
|       if (scrollLeftFixed !== scrollLeft) {
 | |
|         scrollLeft = scrollLeftFixed
 | |
|         this._decelerationVelocityX = 0
 | |
|       }
 | |
|       var scrollTopFixed = Math.max(Math.min(this._maxDecelerationScrollTop, scrollTop), this._minDecelerationScrollTop)
 | |
|       if (scrollTopFixed !== scrollTop) {
 | |
|         scrollTop = scrollTopFixed
 | |
|         this._decelerationVelocityY = 0
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     //
 | |
|     // UPDATE SCROLL POSITION
 | |
|     //
 | |
| 
 | |
|     if (render) {
 | |
|       this._publish(scrollLeft, scrollTop, this._zoomLevel)
 | |
|     } else {
 | |
|       this._scrollLeft = scrollLeft
 | |
|       this._scrollTop = scrollTop
 | |
|     }
 | |
| 
 | |
|     //
 | |
|     // SLOW DOWN
 | |
|     //
 | |
| 
 | |
|     // Slow down velocity on every iteration
 | |
|     if (!this.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
 | |
|       this._decelerationVelocityX *= frictionFactor
 | |
|       this._decelerationVelocityY *= frictionFactor
 | |
|     }
 | |
| 
 | |
|     //
 | |
|     // BOUNCING SUPPORT
 | |
|     //
 | |
| 
 | |
|     if (this.options.bouncing) {
 | |
|       var scrollOutsideX = 0
 | |
|       var scrollOutsideY = 0
 | |
| 
 | |
|       // This configures the amount of change applied to deceleration/acceleration when reaching boundaries
 | |
|       var penetrationDeceleration = this.options.penetrationDeceleration
 | |
|       var penetrationAcceleration = this.options.penetrationAcceleration
 | |
| 
 | |
|       // Check limits
 | |
|       if (scrollLeft < this._minDecelerationScrollLeft) {
 | |
|         scrollOutsideX = this._minDecelerationScrollLeft - scrollLeft
 | |
|       } else if (scrollLeft > this._maxDecelerationScrollLeft) {
 | |
|         scrollOutsideX = this._maxDecelerationScrollLeft - scrollLeft
 | |
|       }
 | |
| 
 | |
|       if (scrollTop < this._minDecelerationScrollTop) {
 | |
|         scrollOutsideY = this._minDecelerationScrollTop - scrollTop
 | |
|       } else if (scrollTop > this._maxDecelerationScrollTop) {
 | |
|         scrollOutsideY = this._maxDecelerationScrollTop - scrollTop
 | |
|       }
 | |
| 
 | |
|       // Slow down until slow enough, then flip back to snap position
 | |
|       if (scrollOutsideX !== 0) {
 | |
|         if (scrollOutsideX * this._decelerationVelocityX <= 0) {
 | |
|           this._decelerationVelocityX += scrollOutsideX * penetrationDeceleration
 | |
|         } else {
 | |
|           this._decelerationVelocityX = scrollOutsideX * penetrationAcceleration
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (scrollOutsideY !== 0) {
 | |
|         if (scrollOutsideY * this._decelerationVelocityY <= 0) {
 | |
|           this._decelerationVelocityY += scrollOutsideY * penetrationDeceleration
 | |
|         } else {
 | |
|           this._decelerationVelocityY = scrollOutsideY * penetrationAcceleration
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| extend(Scroller.prototype, members)
 |