text: /*
   * Scroller
   * http://github.com/zynga/scroller
   *
   * modified by wangyi7099
   *
   * Copyright 2011, Zynga Inc.
   * Licensed under the MIT License.
   * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
   *
   * Based on the work of: Unify Project (unify-project.org)
   * http://unify-project.org
   * Copyright 2011, Deutsche Telekom AG
   * License: MIT + Apache (V2)
   */
  import { easingPattern, createEasingFunction } from '../easingPattern';
  import { core } from './animate';
  import { NOOP } from 'shared/constants';
  
  var animatingMethod = null;
  
  var noAnimatingMethod = null;
  
  export default function Scroller(callback, options) {
    this.__callback = callback;
  
    this.options = {
      /** Enable scrolling on x-axis */
      scrollingX: true,
  
      /** Enable scrolling on y-axis */
      scrollingY: true,
  
      /** Enable animations for deceleration, snap back, zooming and scrolling */
      animating: true,
  
      /** duration for animations triggered by scrollTo/zoomTo */
      animationDuration: 250,
  
      /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
      bouncing: {
        top: 100,
        bottom: 100,
        left: 100,
        right: 100
      },
  
      /** 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,
  
      animatingEasing: 'easeOutCubic',
  
      noAnimatingEasing: 'easeInOutCubic',
  
      /** 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];
    }
  
    animatingMethod = createEasingFunction(
      this.options.animatingEasing,
      easingPattern
    );
    noAnimatingMethod = createEasingFunction(
      this.options.noAnimatingEasing,
      easingPattern
    );
  }
  
  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,
    /** {Integer} Height to assign to refresh area */
    __loadHeight: 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,
  
    __refreshBeforeDeactivate: 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,
  
    __loadActive: null,
  
    __loadActivate: null,
  
    __loadBeforeDeactivate: null,
  
    __loadDeactivate: null,
  
    __loadStart: 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,
  
    /**
     * current page
     */
    __currentPageX: null,
  
    __currentPageY: null,
  
    /**
     * total page
     */
    __totalXPage: null,
  
    __totalYPage: null,
    /*
  	---------------------------------------------------------------------------
  		INTERNAL FIELDS :: LAST POSITIONS
  	---------------------------------------------------------------------------
  	*/
    /** whether the scroller is disabled or not */
    __disable: false,
    /** {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,
      animate,
      noScroll = false
    ) {
      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();
  
      if (!noScroll) {
        // Refresh scroll position
        self.scrollTo(self.__scrollLeft, self.__scrollTop, animate);
      }
    },
  
    /**
     * 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,
        beforeDeactivateCallback,
        beforeDeactiveEnd
      }
    ) {
      var self = this;
  
      self.__refreshHeight = height;
      self.__refreshActivate = activateCallback;
      self.__refreshBeforeDeactivate = beforeDeactivateCallback;
      self.__refreshBeforeDeactiveEnd = beforeDeactiveEnd;
      self.__refreshDeactivate = deactivateCallback;
      self.__refreshStart = startCallback;
    },
    activatePushToLoad: function(
      height,
      {
        activateCallback,
        deactivateCallback,
        startCallback,
        beforeDeactivateCallback,
        beforeDeactiveEnd
      }
    ) {
      var self = this;
  
      self.__loadHeight = height;
      self.__loadActivate = activateCallback;
      self.__loadBeforeDeactivate = beforeDeactivateCallback;
      self.__loadBeforeDeactiveEnd = beforeDeactiveEnd;
      self.__loadDeactivate = deactivateCallback;
      self.__loadStart = startCallback;
    },
  
    /**
     * Starts pull-to-refresh manually.
     */
    triggerRefreshOrLoad: function(type = 'refresh') {
      var wasDecelerating = this.__isDecelerating;
      if (wasDecelerating) {
        core.effect.Animate.stop(wasDecelerating);
        this.__isDecelerating = false;
      }
      // 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
      if (type == 'refresh') {
        if (this.__refreshActive || this.__refreshBeforeDeactiveStarted) return;
  
        this.__publish(
          this.__scrollLeft,
          -this.__refreshHeight,
          this.__zoomLevel,
          true
        );
        if (this.__refreshStart) {
          this.__refreshStart();
          this.__refreshActive = true;
        }
      } else if (type == 'load') {
        if (this.__loadActive || this.__loadBeforeDeactiveStarted) return;
  
        this.__publish(
          this.__scrollLeft,
          this.__maxScrollTop + this.__loadHeight,
          this.__zoomLevel,
          true
        );
        if (this.__loadStart) {
          this.__loadStart();
          this.__loadActive = true;
        }
      }
    },
  
    /**
     * Signalizes that pull-to-refresh is finished.
     */
    finishRefreshOrLoad: function() {
      var self = this;
  
      if (self.__refreshActive) {
        self.__refreshActive = false;
        let endRefreshActive = function() {
          if (self.__refreshBeforeDeactiveEnd) {
            self.__refreshBeforeDeactiveEnd();
          }
          self.__refreshBeforeDeactiveStarted = true;
          self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
        };
  
        if (self.__refreshBeforeDeactivate) {
          self.__refreshBeforeDeactivate(endRefreshActive);
        } else {
          endRefreshActive();
        }
      }
  
      if (self.__loadActive) {
        self.__loadActive = false;
        let endLoadActive = function() {
          if (self.__loadBeforeDeactiveEnd) {
            self.__loadBeforeDeactiveEnd();
          }
          self.__loadBeforeDeactiveStarted = true;
          self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
        };
  
        if (self.__loadBeforeDeactivate) {
          self.__loadBeforeDeactivate(endLoadActive);
        } else {
          endLoadActive();
        }
      }
    },
  
    /**
     * 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, force) {
      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 && !force) {
        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 && !force) {
        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;
        }
      }
  
      if (!force) {
        // 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);
    },
    getCurrentPage() {
      this.__computePage();
      return {
        x: this.__currentPageX,
        y: this.__currentPageY
      };
    },
  
    goToPage({ x, y }, animate) {
      if (isNaN(x)) {
        x = 1;
      }
      if (isNaN(y)) {
        y = 1;
      }
      this.scrollTo(
        (x - 1) * this.__clientWidth,
        (y - 1) * this.__clientHeight,
        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  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);
          }
        }
  
        var bouncing = self.options.bouncing;
  
        if (self.__enableScrollX) {
          scrollLeft -= moveX * this.options.speedMultiplier;
          var maxScrollLeft = self.__maxScrollLeft;
  
          if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
            scrollLeft += (moveX / 2) * this.options.speedMultiplier;
  
            // fix scrollLeft
            scrollLeft = Math.min(
              Math.max(-bouncing.left, scrollLeft),
              maxScrollLeft + bouncing.right
            );
          }
        }
  
        // Compute new vertical scroll position
        if (self.__enableScrollY) {
          scrollTop -= moveY * this.options.speedMultiplier;
          var maxScrollTop = self.__maxScrollTop;
  
          if (scrollTop > maxScrollTop || scrollTop < 0) {
            scrollTop += (moveY / 2) * this.options.speedMultiplier;
  
            // fix scrollTop
            scrollTop = Math.min(
              Math.max(-bouncing.top, scrollTop),
              maxScrollTop + bouncing.bottom
            );
  
            // Trigger pull to refresh or push to load
            if (
              !self.__enableScrollX &&
              (self.__refreshHeight != null || self.__loadHeight != 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();
                }
              }
              // handle for push-load
              else if (
                !self.__loadActive &&
                scrollTop >= self.__maxScrollTop + self.__loadHeight &&
                self.__loadHeight > 0
              ) {
                self.__loadActive = true;
                if (self.__loadActivate) {
                  self.__loadActivate();
                }
              } else if (
                self.__loadActive &&
                scrollTop < self.__maxScrollTop + self.__loadHeight
              ) {
                self.__loadActive = false;
                if (self.__loadDeactivate) {
                  self.__loadDeactivate();
                }
              }
            }
          }
        }
  
        // 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) {
      if (timeStamp instanceof Date) {
        timeStamp = timeStamp.valueOf();
      }
      if (typeof timeStamp !== 'number') {
        throw new Error('Invalid timestamp value: ' + timeStamp);
      }
  
      var self = this;
  
      // Ignore event when tracking is not enabled (no touchstart event on element)
      // This is required as this listener ('touchmove') sits on the document and not on the element itself.
      if (!self.__isTracking) {
        return;
      }
  
      // Not touching anymore (when two finger hit the screen there are two touch end events)
      self.__isTracking = false;
  
      // Be sure to reset the dragging flag now. Here we also detect whether
      // the finger has moved fast enough to switch into a deceleration animation.
      if (self.__isDragging) {
        // Reset dragging flag
        self.__isDragging = false;
  
        // Start deceleration
        // Verify that the last move detected was in some relevant time frame
        if (
          self.__isSingleTouch &&
          self.options.animating &&
          timeStamp - self.__lastTouchMove <= 100
        ) {
          // Then figure out what the scroll position was about 100ms ago
          var positions = self.__positions;
          var endPos = positions.length - 1;
          var startPos = endPos;
  
          // Move pointer to position measured 100ms ago
          for (
            var i = endPos;
            i > 0 && positions[i] > self.__lastTouchMove - 100;
            i -= 3
          ) {
            startPos = i;
          }
  
          // If start and stop position is identical in a 100ms timeframe,
          // we cannot compute any useful deceleration.
          if (startPos !== endPos) {
            // Compute relative movement between these two points
            var timeOffset = positions[endPos] - positions[startPos];
            var movedLeft = self.__scrollLeft - positions[startPos - 2];
            var movedTop = self.__scrollTop - positions[startPos - 1];
  
            // Based on 50ms compute the movement to apply for each render step
            self.__decelerationVelocityX = (movedLeft / timeOffset) * (1000 / 60);
            self.__decelerationVelocityY = (movedTop / timeOffset) * (1000 / 60);
  
            // How much velocity is required to start the deceleration
            var minVelocityToStartDeceleration =
              self.options.paging || self.options.snapping ? 4 : 1;
  
            // Verify that we have enough velocity to start deceleration
            if (
              Math.abs(self.__decelerationVelocityX) >
                minVelocityToStartDeceleration ||
              Math.abs(self.__decelerationVelocityY) >
                minVelocityToStartDeceleration
            ) {
              // Deactivate pull-to-refresh when decelerating
              if (!self.__refreshActive && !self.__loadActive) {
                self.__startDeceleration(timeStamp);
              }
            } else {
              self.__scrollComplete();
            }
          } else {
            self.__scrollComplete();
          }
        } else if (timeStamp - self.__lastTouchMove > 100) {
          self.__scrollComplete();
        }
      }
  
      // 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.__loadActive && self.__loadStart) {
          // 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.__maxScrollTop + self.__loadHeight,
            self.__zoomLevel,
            true
          );
  
          if (self.__loadStart) {
            self.__loadStart();
          }
        } else {
          if (self.__interruptedAnimation || self.__isDragging) {
            self.__scrollComplete();
          }
          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();
            }
          } else if (self.__loadActive) {
            self.__loadActive = false;
            if (self.__loadDeactivate) {
              self.__loadDeactivate();
            }
          }
        }
      }
  
      // Fully cleanup list
      self.__positions.length = 0;
    },
  
    /** Handle for scroll/publish */
    onScroll: NOOP,
  
    stop: function() {
      var self = this;
  
      self.__disable = true;
    },
    start: function() {
      var self = this;
  
      self.__disable = true;
    },
    /*
  	---------------------------------------------------------------------------
  		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) {
      var self = this;
      if (self.__disable) {
        return;
      }
      if (isNaN(left)) {
        left = this.__scrollLeft;
      }
      if (isNaN(top)) {
        top = this.__scrollTop;
      }
      // 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
              );
              self.onScroll();
            }
          }
        };
  
        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.__scrollComplete();
          }
  
          if (self.options.zooming) {
            self.__computeScrollMax();
            if (self.__zoomComplete) {
              self.__zoomComplete();
              self.__zoomComplete = null;
            }
          }
  
          if (self.__refreshBeforeDeactiveStarted) {
            self.__refreshBeforeDeactiveStarted = false;
            if (self.__refreshDeactivate) self.__refreshDeactivate();
          }
  
          if (self.__loadBeforeDeactiveStarted) {
            self.__loadBeforeDeactiveStarted = false;
            if (self.__loadDeactivate) self.__loadDeactivate();
          }
        };
  
        // 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 ? animatingMethod : noAnimatingMethod
        );
      } else {
        self.__scheduledLeft = self.__scrollLeft = left;
        self.__scheduledTop = self.__scrollTop = top;
        self.__scheduledZoom = self.__zoomLevel = zoom;
  
        // Push values out
        if (self.__callback) {
          self.__callback(left, top, zoom);
          self.onScroll();
        }
  
        // Fix max scroll ranges
        if (self.options.zooming) {
          self.__computeScrollMax();
          if (self.__zoomComplete) {
            self.__zoomComplete();
            self.__zoomComplete = null;
          }
        }
  
        if (self.__refreshBeforeDeactiveStarted) {
          self.__refreshBeforeDeactiveStarted = false;
          if (self.__refreshDeactivate) self.__refreshDeactivate();
        }
  
        if (self.__loadBeforeDeactiveStarted) {
          self.__loadBeforeDeactiveStarted = false;
          if (self.__loadDeactivate) self.__loadDeactivate();
        }
      }
    },
  
    /**
     * Recomputes scroll minimum values based on client dimensions and content dimensions.
     */
    __computeScrollMax: function(zoomLevel) {
      var self = this;
  
      if (zoomLevel == null) {
        zoomLevel = self.__zoomLevel;
      }
  
      self.__maxScrollLeft = Math.max(
        self.__contentWidth * zoomLevel - self.__clientWidth,
        0
      );
      self.__maxScrollTop = Math.max(
        self.__contentHeight * zoomLevel - self.__clientHeight,
        0
      );
    },
    /** compute current page total page */
    __computePage: function() {
      var self = this;
      var clientWidth = self.__clientWidth;
      var clientHeight = self.__clientHeight;
      var left = self.__scrollLeft;
      var top = self.__scrollTop;
      self.__totalXPage = Math.ceil(self.__contentWidth / clientWidth);
      self.__currentPageX = Math.ceil(left / clientWidth + 1);
      self.__totalYPage = Math.ceil(self.__contentHeight / clientHeight);
      self.__currentPageY = Math.ceil(top / clientHeight + 1);
    },
    /** complete scroll*/
    __scrollComplete: function() {
      var self = this;
      self.options.scrollingComplete();
    },
    /*
  	---------------------------------------------------------------------------
  		ANIMATION (DECELERATION) SUPPORT
  	---------------------------------------------------------------------------
  	*/
  
    /**
     * Called when a touch sequence end and the speed of the finger was high enough
     * to switch into deceleration mode.
     */
    __startDeceleration: function() {
      var self = this;
  
      if (self.options.paging) {
        var scrollLeft = Math.max(
          Math.min(self.__scrollLeft, self.__maxScrollLeft),
          0
        );
        var scrollTop = Math.max(
          Math.min(self.__scrollTop, self.__maxScrollTop),
          0
        );
        var clientWidth = self.__clientWidth;
        var clientHeight = self.__clientHeight;
  
        // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
        // Each page should have exactly the size of the client area.
        self.__minDecelerationScrollLeft =
          Math.floor(scrollLeft / clientWidth) * clientWidth;
        self.__minDecelerationScrollTop =
          Math.floor(scrollTop / clientHeight) * clientHeight;
        self.__maxDecelerationScrollLeft =
          Math.ceil(scrollLeft / clientWidth) * clientWidth;
        self.__maxDecelerationScrollTop =
          Math.ceil(scrollTop / clientHeight) * clientHeight;
      } else {
        self.__minDecelerationScrollLeft = 0;
        self.__minDecelerationScrollTop = 0;
        self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
        self.__maxDecelerationScrollTop = self.__maxScrollTop;
      }
  
      // Wrap class method
      var step = function(percent, now, render) {
        self.__stepThroughDeceleration(render);
      };
  
      // How much velocity is required to keep the deceleration running
      var minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.001;
  
      // Detect whether it's still worth to continue animating steps
      // If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
      var verify = function() {
        var shouldContinue =
          Math.abs(self.__decelerationVelocityX) >=
            minVelocityToKeepDecelerating ||
          Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating;
        if (!shouldContinue) {
          self.__didDecelerationComplete = true;
        }
        return shouldContinue;
      };
  
      var completed = function() {
        if (!self.__isDecelerating) {
          return;
        }
        self.__isDecelerating = false;
        if (self.__didDecelerationComplete) {
          self.__scrollComplete();
        }
  
        // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
        self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
      };
  
      // Start animation and switch on flag
      self.__isDecelerating = core.effect.Animate.start(step, verify, completed);
    },
  
    /**
     * Called on every step of the animation
     *
     * @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only!
     */
    __stepThroughDeceleration: function(render) {
      var self = this;
      var bouncing = self.options.bouncing;
      var minLeft = self.__minDecelerationScrollLeft;
      var maxLeft = self.__maxDecelerationScrollLeft;
      var minTop = self.__minDecelerationScrollTop;
      var maxTop = self.__maxDecelerationScrollTop;
      // Add deceleration to scroll position
      var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;
      var scrollTop = self.__scrollTop + self.__decelerationVelocityY;
  
      var bounceX = scrollLeft < minLeft || scrollLeft > maxLeft;
      var bounceY = scrollTop < minTop || scrollTop > maxTop;
  
      // fix scrollLeft and scrollTop
      var fixedScrollLeft = Math.min(
        Math.max(minLeft - bouncing.left, scrollLeft),
        maxLeft + bouncing.right
      );
  
      var fixedScrollTop = Math.min(
        Math.max(minTop - bouncing.top, scrollTop),
        maxTop + bouncing.bottom
      );
  
      //
      // UPDATE SCROLL POSITION
      //
      if (render) {
        self.__publish(fixedScrollLeft, fixedScrollTop, self.__zoomLevel);
      } else {
        self.__scrollLeft = scrollLeft;
        self.__scrollTop = scrollTop;
      }
  
      //
      // SLOW DOWN
      //
  
      // Slow down velocity on every iteration
      if (!self.options.paging) {
        // This is the factor applied to every iteration of the animation
        // to slow down the process. This should emulate natural behavior where
        // objects slow down when the initiator of the movement is removed
        var frictionFactor = 0.95;
  
        self.__decelerationVelocityX *= frictionFactor;
        self.__decelerationVelocityY *= frictionFactor;
      }
  
      //
      // BOUNCING SUPPORT
      //
  
      var scrollOutsideX = 0;
      var scrollOutsideY = 0;
  
      // This configures the amount of change applied to deceleration/acceleration when reaching boundaries
      var penetrationDeceleration = self.options.penetrationDeceleration;
      var penetrationAcceleration = self.options.penetrationAcceleration;
  
      if (bounceX) {
        // Check limits
        if (scrollLeft < self.__minDecelerationScrollLeft) {
          scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
        } else if (scrollLeft > self.__maxDecelerationScrollLeft) {
          scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
        }
      }
  
      if (bounceY) {
        if (scrollTop < self.__minDecelerationScrollTop) {
          scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
        } else if (scrollTop > self.__maxDecelerationScrollTop) {
          scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
        }
      }
  
      if (scrollOutsideX !== 0) {
        if (scrollOutsideX * self.__decelerationVelocityX <= 0) {
          self.__decelerationVelocityX +=
            scrollOutsideX * penetrationDeceleration;
          if (
            scrollOutsideX < 0 &&
            -scrollOutsideX >= bouncing.right &&
            self.__decelerationVelocityX > 0
          ) {
            self.__decelerationVelocityX = -bouncing.right;
          }
          if (
            scrollOutsideX > 0 &&
            scrollOutsideX >= bouncing.left &&
            self.__decelerationVelocityX < 0
          ) {
            self.__decelerationVelocityX = bouncing.left;
          }
        } else {
          self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
        }
      }
  
      if (scrollOutsideY !== 0) {
        if (scrollOutsideY * self.__decelerationVelocityY <= 0) {
          self.__decelerationVelocityY +=
            scrollOutsideY * penetrationDeceleration;
          if (
            scrollOutsideY < 0 &&
            -scrollOutsideY >= bouncing.bottom &&
            self.__decelerationVelocityY > 0
          ) {
            self.__decelerationVelocityY = -bouncing.bottom;
          }
          if (
            scrollOutsideY > 0 &&
            scrollOutsideY >= bouncing.top &&
            self.__decelerationVelocityY < 0
          ) {
            self.__decelerationVelocityY = bouncing.top;
          }
        } else {
          self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
        }
      }
    }
  };
  
  // Copy over members to prototype
  for (var key in members) {
    Scroller.prototype[key] = members[key];
  }
  