toolkit/content/widgets/scrollbox.xml
author Dão Gottwald <dao@mozilla.com>
Fri, 09 Jun 2017 15:00:38 +0200
changeset 363248 6efd3b837691c005a140c2325e7ce19e26869edd
parent 361637 cd807be171322fd50de8709de13f240121259933
child 363249 944c5cc643a642b399ae6dc7ec29ba2a0e42b562
permissions -rw-r--r--
Bug 1371604 - Convert arrowscrollbox.scrollboxPaddingStart and arrowscrollbox._startEndProps from XBL properties to fields. r=mconley MozReview-Commit-ID: 4QHvbZ5bvbK

<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->


<bindings id="arrowscrollboxBindings"
   xmlns="http://www.mozilla.org/xbl"
   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="scrollbox-base" extends="chrome://global/content/bindings/general.xml#basecontrol">
    <resources>
      <stylesheet src="chrome://global/skin/scrollbox.css"/>
    </resources>
  </binding>

  <binding id="scrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
    <content>
      <xul:box class="box-inherit scrollbox-innerbox" xbl:inherits="orient,align,pack,dir" flex="1">
        <children/>
      </xul:box>
    </content>

    <implementation>
      <method name="scrollByIndex">
        <parameter name="index"/>
        <body>
          this.boxObject.scrollByIndex(index);
        </body>
      </method>
    </implementation>
  </binding>

  <binding id="arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
    <content>
      <xul:autorepeatbutton class="autorepeatbutton-up"
                            anonid="scrollbutton-up"
                            xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
                            oncommand="_autorepeatbuttonScroll(event);"/>
      <xul:spacer class="arrowscrollbox-overflow-start-indicator"
                  xbl:inherits="collapsed=scrolledtostart"/>
      <xul:scrollbox class="arrowscrollbox-scrollbox"
                     anonid="scrollbox"
                     flex="1"
                     xbl:inherits="orient,align,pack,dir">
        <children/>
      </xul:scrollbox>
      <xul:spacer class="arrowscrollbox-overflow-end-indicator"
                  xbl:inherits="collapsed=scrolledtoend"/>
      <xul:autorepeatbutton class="autorepeatbutton-down"
                            anonid="scrollbutton-down"
                            xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
                            oncommand="_autorepeatbuttonScroll(event);"/>
    </content>

    <implementation>
      <constructor><![CDATA[
        this.setAttribute("notoverflowing", "true");
        this._updateScrollButtonsDisabledState();
      ]]></constructor>

      <destructor><![CDATA[
        this._stopSmoothScroll();
      ]]></destructor>

      <field name="_scrollbox">
        document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
      </field>
      <field name="_scrollButtonUp">
        document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-up");
      </field>
      <field name="_scrollButtonDown">
        document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-down");
      </field>

      <field name="__prefBranch">null</field>
      <property name="_prefBranch" readonly="true">
        <getter><![CDATA[
          if (this.__prefBranch === null) {
            this.__prefBranch = Components.classes["@mozilla.org/preferences-service;1"]
                                          .getService(Components.interfaces.nsIPrefBranch);
          }
          return this.__prefBranch;
        ]]></getter>
      </property>

      <field name="_scrollIncrement">null</field>
      <property name="scrollIncrement" readonly="true">
        <getter><![CDATA[
          if (this._scrollIncrement === null) {
            this._scrollIncrement = this._prefBranch
                                        .getIntPref("toolkit.scrollbox.scrollIncrement", 20);
          }
          return this._scrollIncrement;
        ]]></getter>
      </property>

      <field name="_smoothScroll">null</field>
      <property name="smoothScroll">
        <getter><![CDATA[
          if (this._smoothScroll === null) {
            if (this.hasAttribute("smoothscroll")) {
              this._smoothScroll = (this.getAttribute("smoothscroll") == "true");
            } else {
              this._smoothScroll = this._prefBranch
                                       .getBoolPref("toolkit.scrollbox.smoothScroll", true);
            }
          }
          return this._smoothScroll;
        ]]></getter>
        <setter><![CDATA[
          this._smoothScroll = val;
          return val;
        ]]></setter>
      </property>

      <field name="_scrollBoxObject">null</field>
      <property name="scrollBoxObject" readonly="true">
        <getter><![CDATA[
          if (!this._scrollBoxObject) {
            this._scrollBoxObject = this._scrollbox.boxObject;
          }
          return this._scrollBoxObject;
        ]]></getter>
      </property>

      <property name="scrollClientRect" readonly="true">
        <getter><![CDATA[
          return this._scrollbox.getBoundingClientRect();
        ]]></getter>
      </property>

      <property name="scrollClientSize" readonly="true">
        <getter><![CDATA[
          return this.orient == "vertical" ?
                 this._scrollbox.clientHeight :
                 this._scrollbox.clientWidth;
        ]]></getter>
      </property>

      <property name="scrollSize" readonly="true">
        <getter><![CDATA[
          return this.orient == "vertical" ?
                 this._scrollbox.scrollHeight :
                 this._scrollbox.scrollWidth;
        ]]></getter>
      </property>

      <property name="lineScrollAmount" readonly="true">
        <getter><![CDATA[
          // line scroll amout should be the width (at horizontal scrollbox) or
          // the height (at vertical scrollbox) of the scrolled elements.
          // However, the elements may have different width or height.  So,
          // for consistent speed, let's use avalage with of the elements.
          var elements = this._getScrollableElements();
          if (!elements.length) {
            // Returning 0 shouldn't be problem because if there is no
            // scrollable elements, it's impossible to scroll anyway.
            return 0;
          }

          if (this._isRTLScrollbox)
            elements.reverse();

          var [start, end] = this._startEndProps;
          var low = 0;
          var high = elements.length - 1;
          // XXX If the total width is 0, do we need something more?
          var totalWidth =
            elements[high].getBoundingClientRect()[end] - elements[low].getBoundingClientRect()[start];
          return totalWidth / elements.length;
        ]]></getter>
      </property>

      <property name="scrollPaddingRect" readonly="true">
        <getter><![CDATA[
          // This assumes that this._scrollbox doesn't have any border.
          var outerRect = this.scrollClientRect;
          var innerRect = {};
          innerRect.left = outerRect.left - this._scrollbox.scrollLeft;
          innerRect.top = outerRect.top - this._scrollbox.scrollTop;
          innerRect.right = innerRect.left + this._scrollbox.scrollWidth;
          innerRect.bottom = innerRect.top + this._scrollbox.scrollHeight;
          return innerRect;
        ]]></getter>
      </property>
      <field name="scrollboxPaddingStart"><![CDATA[
        parseFloat(window.getComputedStyle(this._scrollbox)[
          this._isRTLScrollbox ? "paddingRight" : "paddingLeft"
        ]);
      ]]></field>
      <property name="scrollPosition">
        <getter><![CDATA[
          return this.orient == "vertical" ?
                 this._scrollbox.scrollTop :
                 this._scrollbox.scrollLeft;
        ]]></getter>
        <setter><![CDATA[
          if (this.orient == "vertical")
            this._scrollbox.scrollTop = val;
          else
            this._scrollbox.scrollLeft = val;
          return val;
        ]]></setter>
      </property>

      <field name="_startEndProps"><![CDATA[
        this.orient == "vertical" ? ["top", "bottom"] : ["left", "right"];
      ]]></field>

      <field name="_isRTLScrollbox"><![CDATA[
        this.orient != "vertical" &&
        document.defaultView.getComputedStyle(this._scrollbox).direction == "rtl";
      ]]></field>

      <field name="_scrollTarget">null</field>

      <method name="_canScrollToElement">
        <parameter name="element"/>
        <body><![CDATA[
          return window.getComputedStyle(element).display != "none";
        ]]></body>
      </method>

      <method name="ensureElementIsVisible">
        <parameter name="element"/>
        <parameter name="aSmoothScroll"/>
        <body><![CDATA[
          if (!this._canScrollToElement(element))
            return;

          var vertical = this.orient == "vertical";
          var rect = this.scrollClientRect;
          var containerStart = vertical ? rect.top : rect.left;
          var containerEnd = vertical ? rect.bottom : rect.right;
          rect = element.getBoundingClientRect();
          var elementStart = vertical ? rect.top : rect.left;
          var elementEnd = vertical ? rect.bottom : rect.right;

          var scrollPaddingRect = this.scrollPaddingRect;
          let style = window.getComputedStyle(this._scrollbox);
          var scrollContentRect = {
            left: scrollPaddingRect.left + parseFloat(style.paddingLeft),
            top: scrollPaddingRect.top + parseFloat(style.paddingTop),
            right: scrollPaddingRect.right - parseFloat(style.paddingRight),
            bottom: scrollPaddingRect.bottom - parseFloat(style.paddingBottom)
          };

          // Provide an entry point for derived bindings to adjust these values.
          if (this._adjustElementStartAndEnd) {
            [elementStart, elementEnd] =
              this._adjustElementStartAndEnd(element, elementStart, elementEnd);
          }

          if (elementStart <= (vertical ? scrollContentRect.top : scrollContentRect.left)) {
            elementStart = vertical ? scrollPaddingRect.top : scrollPaddingRect.left;
          }
          if (elementEnd >= (vertical ? scrollContentRect.bottom : scrollContentRect.right)) {
            elementEnd = vertical ? scrollPaddingRect.bottom : scrollPaddingRect.right;
          }

          var amountToScroll;

          if (elementStart < containerStart) {
            amountToScroll = elementStart - containerStart;
          } else if (containerEnd < elementEnd) {
            amountToScroll = elementEnd - containerEnd;
          } else if (this._isScrolling) {
            // decelerate if a currently-visible element is selected during the scroll
            const STOP_DISTANCE = 15;
            if (this._isScrolling == -1 && elementStart - STOP_DISTANCE < containerStart)
              amountToScroll = elementStart - containerStart;
            else if (this._isScrolling == 1 && containerEnd - STOP_DISTANCE < elementEnd)
              amountToScroll = elementEnd - containerEnd;
            else
              amountToScroll = this._isScrolling * STOP_DISTANCE;
          } else {
            return;
          }

          this._stopSmoothScroll();

          if (aSmoothScroll != false && this.smoothScroll) {
            this._smoothScrollByPixels(amountToScroll, element);
          } else {
            this.scrollByPixels(amountToScroll);
          }
        ]]></body>
      </method>

      <method name="_smoothScrollByPixels">
        <parameter name="amountToScroll"/>
        <parameter name="element"/><!-- optional -->
        <body><![CDATA[
          if (amountToScroll == 0)
            return;

          // Shouldn't forget pending scroll amount if the scroll direction
          // isn't changed because this may be called high frequency with very
          // small pixel values.
          var scrollDirection = 0;
          if (amountToScroll) {
            // Positive amountToScroll makes us scroll right (elements fly left),
            // negative scrolls left.
            scrollDirection = amountToScroll < 0 ? -1 : 1;
          }

          // However, if the scroll direction is changed, let's cancel the
          // pending scroll because user must want to scroll from current
          // position.
          if (this._isScrolling && this._isScrolling != scrollDirection)
            this._stopSmoothScroll();

          this._scrollTarget = element;
          this._isScrolling = scrollDirection;

          this._scrollAnim.start(amountToScroll, !this._scrollTarget);
        ]]></body>
      </method>

      <field name="_scrollAnim"><![CDATA[({
        scrollbox: this,
        distance: 0.0,
        requestHandle: 0, /* 0 indicates there is no pending request */

        // Be aware, |distance| may be dounble. I.e., the absolute value of it can
        // be less than 1.  Set |isContinuousScroll| to true when the scroll may be
        // a part of continous scroll, for example, it's caused by turning mosue wheel.
        start: function scrollAnim_start(distance, isContinuousScroll) {
          // When it's a continous scroll and the scroll was started, this needs to
          // respect preceding scroll requests.  For example, 1.5px scroll occurs 2 times,
          // 3px should be scrolled.  So, fractional values shouldn't be discarded.
          if (isContinuousScroll && this.distance) {
            // |this.startPos| is integer due to cache of |.scrollPosition|.  Therefore,
            // we need to manage actual destination with |this.destination|.
            var oldDestination = this.destination;
            this.destination = this._clampPosition(this.destination + distance);

            // If scroll position has already reached the ends, we need to do nothing.
            if (oldDestination == this.destination)
              return;

            // If the integer part of the destination isn't changed, we need to do
            // nothing now, wait next event.
            if (Math.trunc(this.destination) == Math.trunc(this.destination - distance))
              return;

            // Let's restart animation from current position to the new destination.
            if (this.requestHandle) {
              this.stop();
              this.startPos = this.scrollbox.scrollPosition;
              // The call of |.stop()| causes clearing |this.distance| but let's recover it
              // for keeping continuous scroll.
              this.distance = this.destination - this.startPos;
            }
          } else {
            this.startPos = this.scrollbox.scrollPosition;
            this.destination = this._clampPosition(this.startPos + distance);
            this.distance = this.destination - this.startPos;

            // If absolute value of |this.distance| is less than 1px and this call is
            // start of a continous scroll, should wait to scroll until accumulated
            // scroll amount becomes 1px or greater.
            if (isContinuousScroll && Math.abs(this.distance) < 1)
              return;
          }
          this.duration = Math.min(1000, Math.round(50 * Math.sqrt(Math.abs(distance))));
          this.startTime = window.performance.now();

          if (!this.requestHandle)
            this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
        },

        stop: function scrollAnim_stop() {
          window.cancelAnimationFrame(this.requestHandle);
          this.requestHandle = 0;
          // Reset continouos scroll transaction at stopping the scroll animation.
          this.distance = 0;
        },

        sample: function scrollAnim_handleEvent(timeStamp) {
          // Note that timeStamp sometimes older than start time.  If we use
          // native value below, it causes scrolling revese direction.
          // So, if the timeStamp is older, let's treat it as same as the start time.
          const timePassed = Math.max(0, timeStamp - this.startTime);
          const pos = timePassed >= this.duration ? 1 :
                      1 - Math.pow(1 - timePassed / this.duration, 4);

          this.scrollbox.scrollPosition = this.startPos + (this.distance * pos);

          if (pos == 1)
            this.scrollbox._stopSmoothScroll();
          else
            this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
        },

        _clampPosition: function scrollAnim_clampPosition(aScrollPosition) {
          if (aScrollPosition < 0) {
            return 0;
          }
          var maxPos = this.scrollbox.scrollSize - this.scrollbox.scrollClientSize;
          if (aScrollPosition > maxPos) {
            return maxPos;
          }
          return aScrollPosition;
        }
      })]]></field>

      <method name="scrollByIndex">
        <parameter name="index"/>
        <parameter name="aSmoothScroll"/>
        <body><![CDATA[
          if (index == 0)
            return;

          // Each scrollByIndex call is expected to scroll the given number of
          // items. If a previous call is still in progress because of smooth
          // scrolling, we need to complete it before starting a new one.
          if (this._scrollTarget) {
            let elements = this._getScrollableElements();
            if (this._scrollTarget != elements[0] &&
                this._scrollTarget != elements[elements.length - 1])
              this.ensureElementIsVisible(this._scrollTarget, false);
          }

          var rect = this.scrollClientRect;
          var [start, end] = this._startEndProps;
          var x = index > 0 ? rect[end] + 1 : rect[start] - 1;
          var nextElement = this._elementFromPoint(x, index);
          if (!nextElement)
            return;

          var targetElement;
          if (this._isRTLScrollbox)
            index *= -1;
          while (index < 0 && nextElement) {
            if (this._canScrollToElement(nextElement))
              targetElement = nextElement;
            nextElement = nextElement.previousSibling;
            index++;
          }
          while (index > 0 && nextElement) {
            if (this._canScrollToElement(nextElement))
              targetElement = nextElement;
            nextElement = nextElement.nextSibling;
            index--;
          }
          if (!targetElement)
            return;

          this.ensureElementIsVisible(targetElement, aSmoothScroll);
        ]]></body>
      </method>

      <method name="scrollByPage">
        <parameter name="pageDelta"/>
        <parameter name="aSmoothScroll"/>
        <body><![CDATA[
          if (pageDelta == 0)
            return;

          // If a previous call is still in progress because of smooth
          // scrolling, we need to complete it before starting a new one.
          if (this._scrollTarget) {
            let elements = this._getScrollableElements();
            if (this._scrollTarget != elements[0] &&
                this._scrollTarget != elements[elements.length - 1])
              this.ensureElementIsVisible(this._scrollTarget, false);
          }

          var [start, end] = this._startEndProps;
          var rect = this.scrollClientRect;
          var containerEdge = pageDelta > 0 ? rect[end] + 1 : rect[start] - 1;
          var pixelDelta = pageDelta * (rect[end] - rect[start]);
          var destinationPosition = containerEdge + pixelDelta;
          var nextElement = this._elementFromPoint(containerEdge, pageDelta);
          if (!nextElement)
            return;

          // We need to iterate over our elements in the direction of pageDelta.
          // pageDelta is the physical direction, so in a horizontal scroll box,
          // positive values scroll to the right no matter if the scrollbox is
          // LTR or RTL. But RTL changes how we need to advance the iteration
          // (whether to get the next or the previous sibling of the current
          // element).
          var logicalAdvanceDir = pageDelta * (this._isRTLScrollbox ? -1 : 1);
          var advance = logicalAdvanceDir > 0 ? (e => e.nextSibling) : (e => e.previousSibling);

          var extendsPastTarget = (pageDelta > 0)
            ? (e => e.getBoundingClientRect()[end] > destinationPosition)
            : (e => e.getBoundingClientRect()[start] < destinationPosition);

          // We want to scroll to the last element we encounter before we find
          // an element which extends past destinationPosition.
          var targetElement;
          do {
            if (this._canScrollToElement(nextElement))
              targetElement = nextElement;
            nextElement = advance(nextElement);
          } while (nextElement && !extendsPastTarget(nextElement));

          if (!targetElement)
            return;

          this.ensureElementIsVisible(targetElement, aSmoothScroll);
        ]]></body>
      </method>

      <method name="_getScrollableElements">
        <body><![CDATA[
          var nodes = this.childNodes;
          if (nodes.length == 1 &&
              nodes[0].localName == "children" &&
              nodes[0].namespaceURI == "http://www.mozilla.org/xbl") {
            nodes = document.getBindingParent(this).childNodes;
          }

          return Array.filter(nodes, this._canScrollToElement, this);
        ]]></body>
      </method>

      <method name="_elementFromPoint">
        <parameter name="aX"/>
        <parameter name="aPhysicalScrollDir"/>
        <body><![CDATA[
          var elements = this._getScrollableElements();
          if (!elements.length)
            return null;

          if (this._isRTLScrollbox)
            elements.reverse();

          var [start, end] = this._startEndProps;
          var low = 0;
          var high = elements.length - 1;

          if (aX < elements[low].getBoundingClientRect()[start] ||
              aX > elements[high].getBoundingClientRect()[end])
            return null;

          var mid, rect;
          while (low <= high) {
            mid = Math.floor((low + high) / 2);
            rect = elements[mid].getBoundingClientRect();
            if (rect[start] > aX)
              high = mid - 1;
            else if (rect[end] < aX)
              low = mid + 1;
            else
              return elements[mid];
          }

          // There's no element at the requested coordinate, but the algorithm
          // from above yields an element next to it, in a random direction.
          // The desired scrolling direction leads to the correct element.

          if (!aPhysicalScrollDir)
            return null;

          if (aPhysicalScrollDir < 0 && rect[start] > aX)
            mid = Math.max(mid - 1, 0);
          else if (aPhysicalScrollDir > 0 && rect[end] < aX)
            mid = Math.min(mid + 1, elements.length - 1);

          return elements[mid];
        ]]></body>
      </method>

      <method name="_autorepeatbuttonScroll">
        <parameter name="event"/>
        <body><![CDATA[
          var dir = event.originalTarget == this._scrollButtonUp ? -1 : 1;
          if (this._isRTLScrollbox)
            dir *= -1;

          this.scrollByPixels(this.scrollIncrement * dir);

          event.stopPropagation();
        ]]></body>
      </method>

      <method name="scrollByPixels">
        <parameter name="px"/>
        <body><![CDATA[
          this.scrollPosition += px;
        ]]></body>
      </method>

      <!-- 0: idle
           1: scrolling right
          -1: scrolling left -->
      <field name="_isScrolling">0</field>
      <field name="_prevMouseScrolls">[null, null]</field>

      <field name="_touchStart">-1</field>

      <method name="_stopSmoothScroll">
        <body><![CDATA[
          if (this._isScrolling) {
            this._scrollAnim.stop();
            this._isScrolling = 0;
            this._scrollTarget = null;
          }
        ]]></body>
      </method>

      <method name="_updateScrollButtonsDisabledState">
        <parameter name="aScrolling"/>
        <body><![CDATA[
          let scrolledToStart;
          let scrolledToEnd;

          // Avoid flushing layout when not overflowing or when scrolling.
          if (this.hasAttribute("notoverflowing")) {
            scrolledToStart = true;
            scrolledToEnd = true;
          } else if (aScrolling) {
            scrolledToStart = false;
            scrolledToEnd = false;
          } else if (this.scrollPosition == 0) {
            // In the RTL case, this means the _last_ element in the
            // scrollbox is visible
            scrolledToEnd = this._isRTLScrollbox;
            scrolledToStart = !this._isRTLScrollbox;
          } else if (this.scrollClientSize + this.scrollPosition == this.scrollSize) {
            // In the RTL case, this means the _first_ element in the
            // scrollbox is visible
            scrolledToStart = this._isRTLScrollbox;
            scrolledToEnd = !this._isRTLScrollbox;
          }

          if (scrolledToEnd)
            this.setAttribute("scrolledtoend", "true");
          else
            this.removeAttribute("scrolledtoend");

          if (scrolledToStart)
            this.setAttribute("scrolledtostart", "true");
          else
            this.removeAttribute("scrolledtostart");
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="wheel"><![CDATA[
        let doScroll = false;
        let useSmoothScroll = event.deltaMode != event.DOM_DELTA_PIXEL && this.smoothScroll;
        let scrollAmount = 0;
        if (this.orient == "vertical") {
          doScroll = true;
          if (event.deltaMode == event.DOM_DELTA_PIXEL)
            scrollAmount = event.deltaY;
          else if (event.deltaMode == event.DOM_DELTA_PAGE)
            scrollAmount = event.deltaY * this.scrollClientSize;
          else
            scrollAmount = event.deltaY * this.lineScrollAmount;
        } else {
          // We allow vertical scrolling to scroll a horizontal scrollbox
          // because many users have a vertical scroll wheel but no
          // horizontal support.
          // Because of this, we need to avoid scrolling chaos on trackpads
          // and mouse wheels that support simultaneous scrolling in both axes.
          // We do this by scrolling only when the last two scroll events were
          // on the same axis as the current scroll event.
          // For diagonal scroll events we only respect the dominant axis.
          let isVertical = Math.abs(event.deltaY) > Math.abs(event.deltaX);
          let delta = isVertical ? event.deltaY : event.deltaX;
          let scrollByDelta = isVertical && this._isRTLScrollbox ? -delta : delta;

          if (this._prevMouseScrolls.every(prev => prev == isVertical)) {
            doScroll = true;
            if (event.deltaMode == event.DOM_DELTA_PIXEL)
              scrollAmount = scrollByDelta;
            else if (event.deltaMode == event.DOM_DELTA_PAGE)
              scrollAmount = scrollByDelta * this.scrollClientSize;
            else
              scrollAmount = scrollByDelta * this.lineScrollAmount;
          }

          if (this._prevMouseScrolls.length > 1)
            this._prevMouseScrolls.shift();
          this._prevMouseScrolls.push(isVertical);
        }

        if (doScroll) {
          if (useSmoothScroll)
            this._smoothScrollByPixels(scrollAmount);
          else
            this.scrollByPixels(scrollAmount);
        }

        event.stopPropagation();
        event.preventDefault();
      ]]></handler>

      <handler event="touchstart"><![CDATA[
        if (event.touches.length > 1) {
          // Multiple touch points detected, abort. In particular this aborts
          // the panning gesture when the user puts a second finger down after
          // already panning with one finger. Aborting at this point prevents
          // the pan gesture from being resumed until all fingers are lifted
          // (as opposed to when the user is back down to one finger).
          this._touchStart = -1;
        } else {
          this._touchStart = (this.orient == "vertical"
                ? event.touches[0].screenY
                : event.touches[0].screenX);
        }
      ]]></handler>

      <handler event="touchmove"><![CDATA[
        if (event.touches.length == 1 &&
            this._touchStart >= 0) {
          var touchPoint = (this.orient == "vertical"
                ? event.touches[0].screenY
                : event.touches[0].screenX);
          var delta = this._touchStart - touchPoint;
          if (Math.abs(delta) > 0) {
            this.scrollByPixels(delta);
            this._touchStart = touchPoint;
          }
          event.preventDefault();
        }
      ]]></handler>

      <handler event="touchend"><![CDATA[
        this._touchStart = -1;
      ]]></handler>

      <handler event="underflow" phase="capturing"><![CDATA[
        // filter underflow events which were dispatched on nested scrollboxes
        if (event.target != this)
          return;

        // Ignore events that doesn't match our orientation.
        // Scrollport event orientation:
        //   0: vertical
        //   1: horizontal
        //   2: both
        if (this.orient == "vertical") {
          if (event.detail == 1)
            return;
        } else if (event.detail == 0) {
          // horizontal scrollbox
          return;
        }

        this.setAttribute("notoverflowing", "true");

        try {
          // See bug 341047 and comments in overflow handler as to why
          // try..catch is needed here
          this._updateScrollButtonsDisabledState();
        } catch (e) {
          this.removeAttribute("notoverflowing");
        }
      ]]></handler>

      <handler event="overflow" phase="capturing"><![CDATA[
        // filter underflow events which were dispatched on nested scrollboxes
        if (event.target != this)
          return;

        // Ignore events that doesn't match our orientation.
        // Scrollport event orientation:
        //   0: vertical
        //   1: horizontal
        //   2: both
        if (this.orient == "vertical") {
          if (event.detail == 1)
            return;
        } else if (event.detail == 0) {
          // horizontal scrollbox
          return;
        }

        this.removeAttribute("notoverflowing");

        try {
          // See bug 341047, the overflow event is dispatched when the
          // scrollbox already is mostly destroyed. This causes some code in
          // _updateScrollButtonsDisabledState() to throw an error. It also
          // means that the notoverflowing attribute was removed erroneously,
          // as the whole overflow event should not be happening in that case.
          this._updateScrollButtonsDisabledState();
        } catch (e) {
          this.setAttribute("notoverflowing", "true");
        }
      ]]></handler>

      <handler event="scroll"><![CDATA[
        if (!this._delayedUpdateScrollButtonsTimer) {
          // This is the beginning of a scrolling animation. We need to update
          // scroll buttons now in case we were scrolled to the start or to the
          // end before we started scrolling.
          this._updateScrollButtonsDisabledState(true);
        } else {
          // We're in the middle of the scrolling animation. We'll restart the
          // delayed update request so that we only update the scroll buttons
          // a second time once we're done scrolling.
          window.clearTimeout(this._delayedUpdateScrollButtonsTimer);
        }

        // Try to detect the end of the scrolling animation to update the
        // scroll buttons again. To avoid false positives, this timeout needs
        // to be big enough to account for intermediate frames that don't move
        // the scroll position in case we're scrolling slowly.
        this._delayedUpdateScrollButtonsTimer = setTimeout(() => {
          // Scrolling animation has finished.
          this._delayedUpdateScrollButtonsTimer = 0;
          this._updateScrollButtonsDisabledState();
        }, 200);
      ]]></handler>
    </handlers>
  </binding>

  <binding id="autorepeatbutton" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
    <content repeat="hover">
      <xul:image class="autorepeatbutton-icon"/>
    </content>
  </binding>

  <binding id="arrowscrollbox-clicktoscroll" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox">
    <content>
      <xul:toolbarbutton class="scrollbutton-up"
                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
                         anonid="scrollbutton-up"
                         onclick="_distanceScroll(event);"
                         onmousedown="if (event.button == 0) _startScroll(-1);"
                         onmouseup="if (event.button == 0) _stopScroll();"
                         onmouseover="_continueScroll(-1);"
                         onmouseout="_pauseScroll();"/>
      <xul:spacer class="arrowscrollbox-overflow-start-indicator"
                  xbl:inherits="collapsed=scrolledtostart"/>
      <xul:scrollbox class="arrowscrollbox-scrollbox"
                     anonid="scrollbox"
                     flex="1"
                     xbl:inherits="orient,align,pack,dir">
        <children/>
      </xul:scrollbox>
      <xul:spacer class="arrowscrollbox-overflow-end-indicator"
                  xbl:inherits="collapsed=scrolledtoend"/>
      <xul:toolbarbutton class="scrollbutton-down"
                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
                         anonid="scrollbutton-down"
                         onclick="_distanceScroll(event);"
                         onmousedown="if (event.button == 0) _startScroll(1);"
                         onmouseup="if (event.button == 0) _stopScroll();"
                         onmouseover="_continueScroll(1);"
                         onmouseout="_pauseScroll();"/>
    </content>
    <implementation implements="nsITimerCallback, nsIDOMEventListener">
      <constructor><![CDATA[
        this._scrollDelay =
          this._prefBranch.getIntPref("toolkit.scrollbox.clickToScroll.scrollDelay",
                                      this._scrollDelay);
      ]]></constructor>

      <destructor><![CDATA[
        // Release timer to avoid reference cycles.
        if (this._scrollTimer) {
          this._scrollTimer.cancel();
          this._scrollTimer = null;
        }
      ]]></destructor>

      <field name="_scrollIndex">0</field>
      <field name="_scrollDelay">150</field>

      <method name="notify">
        <parameter name="aTimer"/>
        <body>
        <![CDATA[
          if (!document)
            aTimer.cancel();

          this.scrollByIndex(this._scrollIndex);
        ]]>
        </body>
      </method>

      <field name="_arrowScrollAnim"><![CDATA[({
        scrollbox: this,
        requestHandle: 0, /* 0 indicates there is no pending request */
        start: function arrowSmoothScroll_start() {
          this.lastFrameTime = window.performance.now();
          if (!this.requestHandle)
            this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
        },
        stop: function arrowSmoothScroll_stop() {
          window.cancelAnimationFrame(this.requestHandle);
          this.requestHandle = 0;
        },
        sample: function arrowSmoothScroll_handleEvent(timeStamp) {
          const scrollIndex = this.scrollbox._scrollIndex;
          const timePassed = timeStamp - this.lastFrameTime;
          this.lastFrameTime = timeStamp;

          const scrollDelta = 0.5 * timePassed * scrollIndex;
          this.scrollbox.scrollPosition += scrollDelta;

          this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
        }
      })]]></field>

      <method name="_startScroll">
        <parameter name="index"/>
        <body><![CDATA[
          if (this._isRTLScrollbox)
            index *= -1;
          this._scrollIndex = index;
          this._mousedown = true;
          if (this.smoothScroll) {
            this._arrowScrollAnim.start();
            return;
          }

          if (!this._scrollTimer)
            this._scrollTimer =
              Components.classes["@mozilla.org/timer;1"]
                        .createInstance(Components.interfaces.nsITimer);
          else
            this._scrollTimer.cancel();

          this._scrollTimer.initWithCallback(this, this._scrollDelay,
                                             this._scrollTimer.TYPE_REPEATING_SLACK);
          this.notify(this._scrollTimer);
        ]]>
        </body>
      </method>

      <method name="_stopScroll">
        <body><![CDATA[
          if (this._scrollTimer)
            this._scrollTimer.cancel();
          this._mousedown = false;
          if (!this._scrollIndex || !this.smoothScroll)
            return;

          this.scrollByIndex(this._scrollIndex);
          this._scrollIndex = 0;
          this._arrowScrollAnim.stop();
        ]]></body>
      </method>

      <method name="_pauseScroll">
        <body><![CDATA[
          if (this._mousedown) {
            this._stopScroll();
            this._mousedown = true;
            document.addEventListener("mouseup", this);
            document.addEventListener("blur", this, true);
          }
        ]]></body>
      </method>

      <method name="_continueScroll">
        <parameter name="index"/>
        <body><![CDATA[
          if (this._mousedown)
            this._startScroll(index);
        ]]></body>
      </method>

      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.type == "mouseup" ||
              aEvent.type == "blur" && aEvent.target == document) {
            this._mousedown = false;
            document.removeEventListener("mouseup", this);
            document.removeEventListener("blur", this, true);
          }
        ]]></body>
      </method>

      <method name="_distanceScroll">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.detail < 2 || aEvent.detail > 3)
            return;

          var scrollBack = (aEvent.originalTarget == this._scrollButtonUp);
          var scrollLeftOrUp = this._isRTLScrollbox ? !scrollBack : scrollBack;
          var targetElement;

          if (aEvent.detail == 2) {
            // scroll by the size of the scrollbox
            let [start, end] = this._startEndProps;
            let x;
            if (scrollLeftOrUp)
              x = this.scrollClientRect[start] - this.scrollClientSize;
            else
              x = this.scrollClientRect[end] + this.scrollClientSize;
            targetElement = this._elementFromPoint(x, scrollLeftOrUp ? -1 : 1);

            // the next partly-hidden element will become fully visible,
            // so don't scroll too far
            if (targetElement)
              targetElement = scrollBack ?
                              targetElement.nextSibling :
                              targetElement.previousSibling;
          }

          if (!targetElement) {
            // scroll to the first resp. last element
            let elements = this._getScrollableElements();
            targetElement = scrollBack ?
                            elements[0] :
                            elements[elements.length - 1];
          }

          this.ensureElementIsVisible(targetElement);
        ]]></body>
      </method>

    </implementation>
  </binding>
</bindings>