text: /* ************************************************************************
  
     qooxdoo - the new era of web development
  
     http://qooxdoo.org
  
     Copyright:
       2004-2011 1&1 Internet AG, Germany, http://www.1und1.de
  
     License:
       MIT: https://opensource.org/licenses/MIT
       See the LICENSE file in the project's top-level directory for details.
  
     Authors:
       * Tino Butz (tbtz)
  
  ************************************************************************ */
  
  /**
   * Container, which allows, depending on the set variant <code>qx.mobile.nativescroll</code>,
   * vertical and horizontal scrolling if the contents is larger than the container.
   *
   * Note that this class can only have one child widget. This container has a
   * fixed layout, which cannot be changed.
   *
   * *Example*
   *
   * Here is a little example of how to use the widget.
   *
   * <pre class='javascript'>
   *   // create the scroll widget
   *   var scroll = new qx.ui.mobile.container.Scroll();
   *
   *   // add a children
   *   scroll.add(new qx.ui.mobile.basic.Label("Name: "));
   *
   *   this.getRoot().add(scroll);
   * </pre>
   *
   * This example creates a scroll container and adds a label to it.
   */
  qx.Class.define("qx.ui.mobile.container.Scroll", {
    extend: qx.ui.mobile.container.Composite,
  
    /**
     * @param scrollProperties {Object} A map with scroll properties which are passed to the scrolling container (may contain iScroll properties).
     */
    construct(scrollProperties) {
      super();
  
      if (scrollProperties) {
        this._scrollProperties = scrollProperties;
      }
  
      this.addListener("appear", this._updateWaypoints, this);
  
      this._waypointsX = [];
      this._waypointsY = [];
  
      this._currentX = 0;
      this._currentY = 0;
    },
  
    events: {
      /** Fired when the scroll container reaches its end position (including momentum/inertia). */
      scrollEnd: "qx.event.type.Event",
  
      /** Fired when the user scrolls to the end of scroll area. */
      pageEnd: "qx.event.type.Event",
  
      /** Fired when a vertical or horizontal waypoint is triggered. Data:
       * <code> {"offset": 0,
       *        "input": "10%",
       *        "index": 0,
       *        "element" : 0}</code>
       */
      waypoint: "qx.event.type.Data",
  
      /**
       * Fired when a momentum starts on an iOS device.
       */
      momentumStart: "qx.event.type.Event",
  
      /**
       * Fired when a momentum ends on an iOS device.
       */
      momentumEnd: "qx.event.type.Data"
    },
  
    /*
    *****************************************************************************
       PROPERTIES
    *****************************************************************************
    */
    properties: {
      // overridden
      defaultCssClass: {
        refine: true,
        init: "scroll"
      },
  
      /**
       * Delegation object which can have one or more functions defined by the
       * {@link qx.ui.mobile.container.IScrollDelegate} interface.
       *
       * @internal
       */
      delegate: {
        init: null,
        nullable: true
      }
    },
  
    /*
    *****************************************************************************
       MEMBERS
    *****************************************************************************
    */
  
    members: {
      _scrollProperties: null,
      _activeWaypointX: null,
      _activeWaypointY: null,
      _waypointsX: null,
      _waypointsY: null,
      _calculatedWaypointsX: null,
      _calculatedWaypointsY: null,
      _currentX: null,
      _currentY: null,
  
      /**
       * Sets the current x position.
       * @param value {Number} the current horizontal position.
       */
      _setCurrentX(value) {
        var old = this._currentX;
        this._currentX = value;
        this._fireWaypoint(value, old, "x");
      },
  
      /**
       * Sets the current y position.
       * @param value {Number} the current vertical position.
       */
      _setCurrentY(value) {
        var old = this._currentY;
        this._currentY = value;
        this._fireWaypoint(value, old, "y");
      },
  
      /**
       * Sets the horizontal trigger points, where a <code>waypoint</code> event will be fired.
       * @param waypoints {Array} description
       */
      setWaypointsX(waypoints) {
        this._waypointsX = waypoints;
      },
  
      /**
       * Sets the vertical trigger points, where a <code>waypoint</code> event will be fired.
       * @param waypoints {Array} an array with waypoint descriptions. Allowed are percentage description as string, or pixel trigger points defined as numbers. <code>["20%",200]</code>
       */
      setWaypointsY(waypoints) {
        this._waypointsY = waypoints;
      },
  
      /**
       * Returns the scroll height.
       * @return {Number} the scroll height.
       */
      getScrollHeight() {
        return this._getScrollHeight();
      },
  
      /**
       * Returns the scroll width.
       * @return {Number} the scroll width.
       */
      getScrollWidth() {
        return this._getScrollWidth();
      },
  
      /**
       * Re-calculates the internal waypoint offsets.
       */
      _updateWaypoints() {
        this._calculatedWaypointsX = [];
        this._calculatedWaypointsY = [];
        this._calcWaypoints(
          this._waypointsX,
          this._calculatedWaypointsX,
          this.getScrollWidth(),
          "x"
        );
  
        this._calcWaypoints(
          this._waypointsY,
          this._calculatedWaypointsY,
          this.getScrollHeight()
        );
      },
  
      /**
       * Validates and checks the waypoint offsets.
       * @param waypoints {Array} an array with waypoint descriptions.
       * @param results {Array} the array where calculated waypoints will be added.
       * @param scrollSize {Number} the vertical or horizontal scroll size.
       * @param axis {String?} "x" or "y".
       */
      _calcWaypoints(waypoints, results, scrollSize, axis) {
        axis = axis || "y";
  
        var offset = 0;
        for (var i = 0; i < waypoints.length; i++) {
          var waypoint = waypoints[i];
          if (qx.lang.Type.isString(waypoint)) {
            if (waypoint.endsWith("%")) {
              offset = parseInt(waypoint, 10) * (scrollSize / 100);
              results.push({
                offset: offset,
                input: waypoint,
                index: i,
                element: null,
                axis: axis
              });
            } else {
              // Dynamically created waypoints, based upon a selector.
              var element = this.getContentElement();
              var waypointElements = qx.bom.Selector.query(waypoint, element);
              for (var j = 0; j < waypointElements.length; j++) {
                var position = qx.bom.element.Location.getRelative(
                  waypointElements[j],
                  element
                );
  
                if (axis === "y") {
                  offset = position.top + this.getContentElement().scrollTop;
                } else if (axis === "x") {
                  offset = position.left + this.getContentElement().scrollLeft;
                }
                results.push({
                  offset: position.top + this._currentY,
                  input: waypoint,
                  index: i,
                  element: j,
                  axis: axis
                });
              }
            }
          } else if (qx.lang.Type.isNumber(waypoint)) {
            results.push({
              offset: waypoint,
              input: waypoint,
              index: i,
              element: null,
              axis: axis
            });
          }
        }
  
        results.sort(function (a, b) {
          return a.offset - b.offset;
        });
      },
  
      /**
       * Fires a waypoints event when scroll position changes.
       * @param value {Number} old scroll position.
       * @param old {Number} old scroll position.
       * @param axis {String} "x" or "y".
       */
      _fireWaypoint(value, old, axis) {
        var waypoints = this._calculatedWaypointsY;
        if (axis === "x") {
          waypoints = this._calculatedWaypointsX;
        }
  
        if (waypoints === null) {
          return;
        }
  
        var nextWaypoint = null;
        for (var i = 0; i < waypoints.length; i++) {
          var waypoint = waypoints[i];
          if (waypoint.offset !== null) {
            if (
              (value > -1 && value >= waypoint.offset) ||
              (value < 0 && waypoint.offset < 0 && value <= waypoint.offset)
            ) {
              nextWaypoint = waypoint;
            } else {
              break;
            }
          }
        }
  
        if (nextWaypoint === null) {
          if (axis === "x") {
            this._activeWaypointX = null;
          } else {
            this._activeWaypointY = null;
          }
          return;
        }
  
        var direction = null;
        if (old <= value) {
          direction = "down";
          if (axis == "x") {
            direction = "left";
          }
        } else {
          direction = "up";
          if (axis == "x") {
            direction = "right";
          }
        }
  
        var activeWaypoint = this._activeWaypointY;
        if (axis === "x") {
          activeWaypoint = this._activeWaypointX;
        }
  
        if (
          activeWaypoint === null ||
          activeWaypoint.index !== nextWaypoint.index ||
          activeWaypoint.element !== nextWaypoint.element
        ) {
          activeWaypoint = nextWaypoint;
          this._activeWaypointY = activeWaypoint;
          if (axis === "x") {
            this._activeWaypointX = activeWaypoint;
          }
          this.fireDataEvent("waypoint", {
            axis: axis,
            index: nextWaypoint.index,
            element: nextWaypoint.element,
            direction: direction
          });
        }
      },
  
      // overridden
      _createContainerElement() {
        var element = super._createContainerElement();
        var scrollElement = this._createScrollElement();
        if (scrollElement) {
          return scrollElement;
        }
  
        return element;
      },
  
      // overridden
      _getContentElement() {
        var contentElement = super._getContentElement();
  
        var scrollContentElement = this._getScrollContentElement();
  
        return scrollContentElement || contentElement;
      },
  
      /**
       * Calls the refresh function the used scrolling method. Needed to recalculate the
       * scrolling container.
       */
      refresh() {
        this._refresh();
        this._updateWaypoints();
      },
  
      /**
       * Scrolls the wrapper contents to the x/y coordinates in a given time.
       *
       * @param x {Integer} X coordinate to scroll to.
       * @param y {Integer} Y coordinate to scroll to.
       * @param time {Integer} Time slice in which scrolling should
       *              be done.
       */
      scrollTo(x, y, time) {
        this._scrollTo(x, y, time);
      },
  
      /**
       * Returns the current scroll position
       * @return {Array} an array with <code>[scrollLeft,scrollTop]</code>.
       */
      getPosition() {
        return this._getPosition();
      },
  
      /**
       * Detects whether this scroll container is scrollable or not.
       * @return {Boolean} <code>true</code> or <code>false</code>
       */
      isScrollable() {
        return this._isScrollable();
      },
  
      /**
       * Detects whether this scroll container is scrollable or not.
       * @return {Boolean} <code>true</code> or <code>false</code>
       */
      _isScrollable() {
        return this._isScrollableX() || this._isScrollableY();
      },
  
      /**
       * Detects whether this scroll container is scrollable on x axis or not.
       * @return {Boolean} <code>true</code> or <code>false</code>
       */
      _isScrollableX() {
        if (this.getLayoutParent() === null) {
          return false;
        }
  
        var parentWidth = this.getContainerElement().clientWidth;
        var contentWidth = this.getContentElement().scrollWidth;
  
        var scrollContentElement = this._getScrollContentElement();
        if (scrollContentElement) {
          contentWidth = qx.bom.element.Dimension.getWidth(scrollContentElement);
        }
  
        return parentWidth < contentWidth;
      },
  
      /**
       * Detects whether this scroll container is scrollable on y axis or not.
       * @return {Boolean} <code>true</code> or <code>false</code>
       */
      _isScrollableY() {
        if (this.getLayoutParent() === null) {
          return false;
        }
  
        var parentHeight = this.getContainerElement().clientHeight;
        var contentHeight = this.getContentElement().scrollHeight;
  
        var scrollContentElement = this._getScrollContentElement();
        if (scrollContentElement) {
          contentHeight =
            qx.bom.element.Dimension.getHeight(scrollContentElement);
        }
  
        return parentHeight < contentHeight;
      },
  
      /**
       * Scrolls the wrapper contents to the widgets coordinates in a given
       * period.
       *
       * @param target {Element} the element to which the scroll container should scroll to.
       * @param time {Integer?0} Time slice in which scrolling should
       *              be done (in seconds).
       *
       */
      scrollToElement(target, time) {
        this._scrollToElement(target, time);
      },
  
      /**
       * Scrolls the wrapper contents to the widgets coordinates in a given
       * period.
       *
       * @param element {String} the element to which the scroll container should scroll to.
       * @param time {Integer?0} Time slice in which scrolling should be done (in seconds).
       *
       */
      _scrollToElement(element, time) {
        if (this._getContentElement() && this._isScrollable()) {
          if (typeof time === "undefined") {
            time = 0;
          }
  
          var location = qx.bom.element.Location.getRelative(
            this._getContentElement(),
            element,
            "scroll",
            "scroll"
          );
  
          var offset = this._getScrollOffset();
  
          this._scrollTo(
            -location.left - offset[0],
            -location.top - offset[1],
            time
          );
        }
      },
  
      /**
       *
       * Determines the scroll offset for the <code>_scrollToElement</code> method.
       * If a delegate is available, the method calls
       * <code>qx.ui.mobile.container.IScrollDelegate.getScrollOffset()</code> for offset calculation.
       *
       * @return {Array} an array with x,y offset.
       */
      _getScrollOffset() {
        var delegate = this.getDelegate();
        if (delegate != null && delegate.getScrollOffset) {
          return delegate.getScrollOffset.bind(this)();
        } else {
          return [0, 0];
        }
      },
  
      /**
       * Scrolls the wrapper contents to the widgets coordinates in a given
       * period.
       *
       * @param widget {qx.ui.mobile.core.Widget} the widget, the scroll container should scroll to.
       * @param time {Integer} Time slice in which scrolling should
       *              be done.
       */
      scrollToWidget(widget, time) {
        if (widget) {
          this._scrollToElement(widget.getContentElement(), time);
        }
      }
    },
  
    defer(statics) {
      if (qx.core.Environment.get("qx.mobile.nativescroll") == false) {
        qx.Class.include(statics, qx.ui.mobile.container.MIScroll);
      } else {
        qx.Class.include(statics, qx.ui.mobile.container.MNativeScroll);
      }
    },
  
    destruct() {
      this.removeListener("appear", this._updateWaypoints, this);
      this._waypointsX = this._waypointsY = null;
    }
  });
  