toolkit/content/widgets/scrollbox.xml
author Bogdan Tara <btara@mozilla.com>
Thu, 12 Jul 2018 01:50:53 +0300
changeset 481348 d610dd7bfbcdc0ec00ce393dff833f2d8e1b7986
parent 481347 b099e7e0b26494e0b2cdc549f75ff19291627968
child 481513 3af5036936c12d911a0a3de1ef86f692e7878486
permissions -rw-r--r--
Backed out changeset b099e7e0b264 (bug 1454358) for build bustages on Element.h CLOSED TREE

<?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" extends="chrome://global/content/bindings/general.xml#basecontrol">
    <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/general.xml#basecontrol">
    <resources>
      <stylesheet src="chrome://global/skin/scrollbox.css"/>
    </resources>

    <content>
      <xul:toolbarbutton class="scrollbutton-up"
                         anonid="scrollbutton-up"
                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
                         onmouseover="_startScroll(-1);"
                         onmouseout="_stopScroll();"/>
      <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,smoothscroll">
        <children/>
      </xul:scrollbox>
      <xul:spacer class="arrowscrollbox-overflow-end-indicator"
                  xbl:inherits="collapsed=scrolledtoend"/>
      <xul:toolbarbutton class="scrollbutton-down"
                         anonid="scrollbutton-down"
                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
                         onmouseover="_startScroll(1);"
                         onmouseout="_stopScroll();"/>
    </content>

    <implementation>
      <constructor><![CDATA[
        if (!this.hasAttribute("smoothscroll")) {
          this.smoothScroll = this._prefBranch
                                  .getBoolPref("toolkit.scrollbox.smoothScroll", true);
        }

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

      <destructor><![CDATA[
        // Release timer to avoid reference cycles.
        if (this._scrollTimer) {
          this._scrollTimer.cancel();
          this._scrollTimer = null;
        }
      ]]></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 = Cc["@mozilla.org/preferences-service;1"]
                                  .getService(Ci.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>

      <property name="smoothScroll">
        <getter><![CDATA[
          return this.getAttribute("smoothscroll") == "true";
        ]]></getter>
        <setter><![CDATA[
          this.setAttribute("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();
          return elements.length && (this.scrollSize / elements.length);
        ]]></getter>
      </property>

      <property name="scrollPosition" readonly="true">
        <getter><![CDATA[
          return this.orient == "vertical" ?
                 this._scrollbox.scrollTop :
                 this._scrollbox.scrollLeft;
        ]]></getter>
      </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="_boundsWithoutFlushing">
        <parameter name="element"/>
        <body><![CDATA[
          if (!("_DOMWindowUtils" in this)) {
            try {
              this._DOMWindowUtils =
                window.QueryInterface(Ci.nsIInterfaceRequestor)
                      .getInterface(Ci.nsIDOMWindowUtils);
            } catch (e) {
              // Can't access nsIDOMWindowUtils if we're unprivileged.
              this._DOMWindowUtils = null;
            }
          }

          return this._DOMWindowUtils ?
                 this._DOMWindowUtils.getBoundsWithoutFlushing(element) :
                 element.getBoundingClientRect();
        ]]></body>
      </method>

      <method name="_canScrollToElement">
        <parameter name="element"/>
        <body><![CDATA[
          if (element.hidden) {
            return false;
          }

          // See if the element is hidden via CSS without the hidden attribute.
          // If we get only zeros for the client rect, this means the element
          // is hidden. As a performance optimization, we don't flush layout
          // here which means that on the fly changes aren't fully supported.
          let rect = this._boundsWithoutFlushing(element);
          return !!(rect.top || rect.left || rect.width || rect.height);
        ]]></body>
      </method>

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

          element.scrollIntoView({ behavior: aInstant ? "instant" : "auto" });
        ]]></body>
      </method>

      <method name="scrollByIndex">
        <parameter name="index"/>
        <parameter name="aInstant"/>
        <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, true);
          }

          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, aInstant);
        ]]></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="_startScroll">
        <parameter name="index"/>
        <body><![CDATA[
          if (this._isRTLScrollbox) {
            index *= -1;
          }

          if (!this._scrollTimer) {
            this._scrollTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
          } else {
            this._scrollTimer.cancel();
          }

          let callback = () => this.scrollByPixels(this.scrollIncrement * index);

          // Use the same REPEAT_DELAY as "nsRepeatService.h".
          let scrollDelay = /Mac/.test(navigator.platform) ? 25 : 50;

          this._scrollTimer.initWithCallback(callback, scrollDelay,
                                             Ci.nsITimer.TYPE_REPEATING_SLACK);
          callback();
        ]]>
        </body>
      </method>

      <method name="_stopScroll">
        <body><![CDATA[
          if (this._scrollTimer)
            this._scrollTimer.cancel();
        ]]></body>
      </method>

      <method name="scrollByPixels">
        <parameter name="aPixels"/>
        <parameter name="aInstant"/>
        <body><![CDATA[
          let scrollOptions = { behavior: aInstant ? "instant" : "auto" };
          scrollOptions[this._startEndProps[0]] = aPixels;
          this._scrollbox.scrollBy(scrollOptions);
        ]]></body>
      </method>

      <field name="_prevMouseScrolls">[null, null]</field>

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

      <field name="_scrollButtonUpdatePending">false</field>
      <method name="_updateScrollButtonsDisabledState">
        <body><![CDATA[
          if (this.hasAttribute("notoverflowing")) {
            this.setAttribute("scrolledtoend", "true");
            this.setAttribute("scrolledtostart", "true");
            return;
          }

          if (this._scrollButtonUpdatePending) {
            return;
          }
          this._scrollButtonUpdatePending = true;

          // Wait until after the next paint to get current layout data from
          // getBoundsWithoutFlushing.
          window.requestAnimationFrame(() => {
            setTimeout(() => {
              this._scrollButtonUpdatePending = false;

              let scrolledToStart = false;
              let scrolledToEnd = false;

              if (this.hasAttribute("notoverflowing")) {
                scrolledToStart = true;
                scrolledToEnd = true;
              } else {
                let [leftOrTop, rightOrBottom] = this._startEndProps;
                let leftOrTopEdge = ele => Math.round(this._boundsWithoutFlushing(ele)[leftOrTop]);
                let rightOrBottomEdge = ele => Math.round(this._boundsWithoutFlushing(ele)[rightOrBottom]);

                let elements = this._getScrollableElements();
                let [leftOrTopElement, rightOrBottomElement] = [elements[0], elements[elements.length - 1]];
                if (this._isRTLScrollbox) {
                  [leftOrTopElement, rightOrBottomElement] = [rightOrBottomElement, leftOrTopElement];
                }

                if (leftOrTopElement &&
                    leftOrTopEdge(leftOrTopElement) >= leftOrTopEdge(this._scrollbox)) {
                  scrolledToStart = !this._isRTLScrollbox;
                  scrolledToEnd = this._isRTLScrollbox;
                } else if (rightOrBottomElement &&
                           rightOrBottomEdge(rightOrBottomElement) <= rightOrBottomEdge(this._scrollbox)) {
                  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");
              }
            }, 0);
          });
        ]]></body>
      </method>

      <field name="_isScrolling">false</field>
      <field name="_destination">0</field>
      <field name="_direction">0</field>
    </implementation>

    <handlers>
      <handler event="wheel"><![CDATA[
        let doScroll = false;
        let instant;
        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;
              instant = true;
            } 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) {
          let direction = scrollAmount < 0 ? -1 : 1;
          let startPos = this.scrollPosition;

          if (!this._isScrolling || this._direction != direction) {
            this._destination = startPos + scrollAmount;
            this._direction = direction;
          } else {
            // We were already in the process of scrolling in this direction
            this._destination = this._destination + scrollAmount;
            scrollAmount = this._destination - startPos;
          }
          this.scrollByPixels(scrollAmount, instant);
        }

        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, true);
            this._touchStart = touchPoint;
          }
          event.preventDefault();
        }
      ]]></handler>

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

      <handler event="underflow" phase="capturing"><![CDATA[
        // Ignore underflow events:
        // - from nested scrollable elements
        // - corresponding to an overflow event that we ignored
        if (event.target != this ||
            this.hasAttribute("notoverflowing")) {
          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");
        this._updateScrollButtonsDisabledState();
      ]]></handler>

      <handler event="overflow" phase="capturing"><![CDATA[
        // Ignore overflow events:
        // - from nested scrollable elements
        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");
        this._updateScrollButtonsDisabledState();
      ]]></handler>

      <handler event="scroll"><![CDATA[
        this._isScrolling = true;
        this._updateScrollButtonsDisabledState();
      ]]></handler>

      <handler event="scrollend"><![CDATA[
        this._isScrolling = false;
        this._destination = 0;
        this._direction = 0;
      ]]></handler>
    </handlers>
  </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,smoothscroll">
        <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">
      <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.scrollByPixels(scrollDelta, true);
          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 =
              Cc["@mozilla.org/timer;1"]
                .createInstance(Ci.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>