1<?xml version="1.0"?>
2<!-- This Source Code Form is subject to the terms of the Mozilla Public
3   - License, v. 2.0. If a copy of the MPL was not distributed with this
4   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
5
6
7<bindings id="arrowscrollboxBindings"
8   xmlns="http://www.mozilla.org/xbl"
9   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
10   xmlns:xbl="http://www.mozilla.org/xbl">
11
12  <binding id="scrollbox-base" extends="chrome://global/content/bindings/general.xml#basecontrol">
13    <resources>
14      <stylesheet src="chrome://global/skin/scrollbox.css"/>
15    </resources>
16  </binding>
17
18  <binding id="scrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
19    <content>
20      <xul:box class="box-inherit scrollbox-innerbox" xbl:inherits="orient,align,pack,dir" flex="1">
21        <children/>
22      </xul:box>
23    </content>
24  </binding>
25
26  <binding id="arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
27    <content>
28      <xul:autorepeatbutton class="autorepeatbutton-up"
29                            anonid="scrollbutton-up"
30                            xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
31                            oncommand="_autorepeatbuttonScroll(event);"/>
32      <xul:spacer class="arrowscrollbox-overflow-start-indicator"
33                  xbl:inherits="collapsed=scrolledtostart"/>
34      <xul:scrollbox class="arrowscrollbox-scrollbox"
35                     anonid="scrollbox"
36                     flex="1"
37                     xbl:inherits="orient,align,pack,dir">
38        <children/>
39      </xul:scrollbox>
40      <xul:spacer class="arrowscrollbox-overflow-end-indicator"
41                  xbl:inherits="collapsed=scrolledtoend"/>
42      <xul:autorepeatbutton class="autorepeatbutton-down"
43                            anonid="scrollbutton-down"
44                            xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
45                            oncommand="_autorepeatbuttonScroll(event);"/>
46    </content>
47
48    <implementation>
49      <constructor><![CDATA[
50        this.setAttribute("notoverflowing", "true");
51        this._updateScrollButtonsDisabledState();
52      ]]></constructor>
53
54      <destructor><![CDATA[
55        this._stopSmoothScroll();
56      ]]></destructor>
57
58      <field name="_scrollbox">
59        document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
60      </field>
61      <field name="_scrollButtonUp">
62        document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-up");
63      </field>
64      <field name="_scrollButtonDown">
65        document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-down");
66      </field>
67
68      <field name="__prefBranch">null</field>
69      <property name="_prefBranch" readonly="true">
70        <getter><![CDATA[
71          if (this.__prefBranch === null) {
72            this.__prefBranch = Components.classes['@mozilla.org/preferences-service;1']
73                                          .getService(Components.interfaces.nsIPrefBranch);
74          }
75          return this.__prefBranch;
76        ]]></getter>
77      </property>
78
79      <field name="_scrollIncrement">null</field>
80      <property name="scrollIncrement" readonly="true">
81        <getter><![CDATA[
82          if (this._scrollIncrement === null) {
83            try {
84              this._scrollIncrement = this._prefBranch
85                                          .getIntPref("toolkit.scrollbox.scrollIncrement");
86            }
87            catch (ex) {
88              this._scrollIncrement = 20;
89            }
90          }
91          return this._scrollIncrement;
92        ]]></getter>
93      </property>
94
95      <field name="_smoothScroll">null</field>
96      <property name="smoothScroll">
97        <getter><![CDATA[
98          if (this._smoothScroll === null) {
99            if (this.hasAttribute("smoothscroll")) {
100              this._smoothScroll = (this.getAttribute("smoothscroll") == "true");
101            } else {
102              try {
103                this._smoothScroll = this._prefBranch
104                                         .getBoolPref("toolkit.scrollbox.smoothScroll");
105              }
106              catch (ex) {
107                this._smoothScroll = true;
108              }
109            }
110          }
111          return this._smoothScroll;
112        ]]></getter>
113        <setter><![CDATA[
114          this._smoothScroll = val;
115          return val;
116        ]]></setter>
117      </property>
118
119      <field name="_scrollBoxObject">null</field>
120      <property name="scrollBoxObject" readonly="true">
121        <getter><![CDATA[
122          if (!this._scrollBoxObject) {
123            this._scrollBoxObject = this._scrollbox.boxObject;
124          }
125          return this._scrollBoxObject;
126        ]]></getter>
127      </property>
128
129      <property name="scrollClientRect" readonly="true">
130        <getter><![CDATA[
131          return this._scrollbox.getBoundingClientRect();
132        ]]></getter>
133      </property>
134
135      <property name="scrollClientSize" readonly="true">
136        <getter><![CDATA[
137          return this.orient == "vertical" ?
138                 this._scrollbox.clientHeight :
139                 this._scrollbox.clientWidth;
140        ]]></getter>
141      </property>
142
143      <property name="scrollSize" readonly="true">
144        <getter><![CDATA[
145          return this.orient == "vertical" ?
146                 this._scrollbox.scrollHeight :
147                 this._scrollbox.scrollWidth;
148        ]]></getter>
149      </property>
150      <property name="scrollPaddingRect" readonly="true">
151        <getter><![CDATA[
152          // This assumes that this._scrollbox doesn't have any border.
153          var outerRect = this.scrollClientRect;
154          var innerRect = {};
155          innerRect.left = outerRect.left - this._scrollbox.scrollLeft;
156          innerRect.top = outerRect.top - this._scrollbox.scrollTop;
157          innerRect.right = innerRect.left + this._scrollbox.scrollWidth;
158          innerRect.bottom = innerRect.top + this._scrollbox.scrollHeight;
159          return innerRect;
160        ]]></getter>
161      </property>
162      <property name="scrollboxPaddingStart" readonly="true">
163        <getter><![CDATA[
164          var ltr = (window.getComputedStyle(this, null).direction == "ltr");
165          var paddingStartName = ltr ? "padding-left" : "padding-right";
166          var scrollboxStyle = window.getComputedStyle(this._scrollbox, null);
167          return parseFloat(scrollboxStyle.getPropertyValue(paddingStartName));
168        ]]></getter>
169      </property>
170      <property name="scrollPosition">
171        <getter><![CDATA[
172          return this.orient == "vertical" ?
173                 this._scrollbox.scrollTop :
174                 this._scrollbox.scrollLeft;
175        ]]></getter>
176        <setter><![CDATA[
177          if (this.orient == "vertical")
178            this._scrollbox.scrollTop = val;
179          else
180            this._scrollbox.scrollLeft = val;
181          return val;
182        ]]></setter>
183      </property>
184
185      <property name="_startEndProps" readonly="true">
186        <getter><![CDATA[
187          return this.orient == "vertical" ?
188                 ["top", "bottom"] : ["left", "right"];
189        ]]></getter>
190      </property>
191
192      <field name="_isRTLScrollbox"><![CDATA[
193        this.orient != "vertical" &&
194        document.defaultView.getComputedStyle(this._scrollbox, "").direction == "rtl";
195      ]]></field>
196
197      <field name="_scrollTarget">null</field>
198
199      <method name="_canScrollToElement">
200        <parameter name="element"/>
201        <body><![CDATA[
202          return window.getComputedStyle(element).display != "none";
203        ]]></body>
204      </method>
205
206      <method name="ensureElementIsVisible">
207        <parameter name="element"/>
208        <parameter name="aSmoothScroll"/>
209        <body><![CDATA[
210          if (!this._canScrollToElement(element))
211            return;
212
213          var vertical = this.orient == "vertical";
214          var rect = this.scrollClientRect;
215          var containerStart = vertical ? rect.top : rect.left;
216          var containerEnd = vertical ? rect.bottom : rect.right;
217          rect = element.getBoundingClientRect();
218          var elementStart = vertical ? rect.top : rect.left;
219          var elementEnd = vertical ? rect.bottom : rect.right;
220
221          var scrollPaddingRect = this.scrollPaddingRect;
222          let style = window.getComputedStyle(this._scrollbox, null);
223          var scrollContentRect = {
224            left: scrollPaddingRect.left + parseFloat(style.paddingLeft),
225            top: scrollPaddingRect.top + parseFloat(style.paddingTop),
226            right: scrollPaddingRect.right - parseFloat(style.paddingRight),
227            bottom: scrollPaddingRect.bottom - parseFloat(style.paddingBottom)
228          };
229
230          // Provide an entry point for derived bindings to adjust these values.
231          if (this._adjustElementStartAndEnd) {
232            [elementStart, elementEnd] =
233              this._adjustElementStartAndEnd(element, elementStart, elementEnd);
234          }
235
236          if (elementStart <= (vertical ? scrollContentRect.top : scrollContentRect.left)) {
237            elementStart = vertical ? scrollPaddingRect.top : scrollPaddingRect.left;
238          }
239          if (elementEnd >= (vertical ? scrollContentRect.bottom : scrollContentRect.right)) {
240            elementEnd = vertical ? scrollPaddingRect.bottom : scrollPaddingRect.right;
241          }
242
243          var amountToScroll;
244
245          if (elementStart < containerStart) {
246            amountToScroll = elementStart - containerStart;
247          } else if (containerEnd < elementEnd) {
248            amountToScroll = elementEnd - containerEnd;
249          } else if (this._isScrolling) {
250            // decelerate if a currently-visible element is selected during the scroll
251            const STOP_DISTANCE = 15;
252            if (this._isScrolling == -1 && elementStart - STOP_DISTANCE < containerStart)
253              amountToScroll = elementStart - containerStart;
254            else if (this._isScrolling == 1 && containerEnd - STOP_DISTANCE < elementEnd)
255              amountToScroll = elementEnd - containerEnd;
256            else
257              amountToScroll = this._isScrolling * STOP_DISTANCE;
258          } else {
259            return;
260          }
261
262          this._stopSmoothScroll();
263
264          if (aSmoothScroll != false && this.smoothScroll) {
265            this._smoothScrollByPixels(amountToScroll, element);
266          } else {
267            this.scrollByPixels(amountToScroll);
268          }
269        ]]></body>
270      </method>
271
272      <method name="_smoothScrollByPixels">
273        <parameter name="amountToScroll"/>
274        <parameter name="element"/><!-- optional -->
275        <body><![CDATA[
276          this._stopSmoothScroll();
277          if (amountToScroll == 0)
278            return;
279
280          this._scrollTarget = element;
281          // Positive amountToScroll makes us scroll right (elements fly left), negative scrolls left.
282          this._isScrolling = amountToScroll < 0 ? -1 : 1;
283
284          this._scrollAnim.start(amountToScroll);
285        ]]></body>
286      </method>
287
288      <field name="_scrollAnim"><![CDATA[({
289        scrollbox: this,
290        requestHandle: 0, /* 0 indicates there is no pending request */
291        start: function scrollAnim_start(distance) {
292          this.distance = distance;
293          this.startPos = this.scrollbox.scrollPosition;
294          this.duration = Math.min(1000, Math.round(50 * Math.sqrt(Math.abs(distance))));
295          this.startTime = window.performance.now();
296
297          if (!this.requestHandle)
298            this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
299        },
300        stop: function scrollAnim_stop() {
301          window.cancelAnimationFrame(this.requestHandle);
302          this.requestHandle = 0;
303        },
304        sample: function scrollAnim_handleEvent(timeStamp) {
305          const timePassed = timeStamp - this.startTime;
306          const pos = timePassed >= this.duration ? 1 :
307                      1 - Math.pow(1 - timePassed / this.duration, 4);
308
309          this.scrollbox.scrollPosition = this.startPos + (this.distance * pos);
310
311          if (pos == 1)
312            this.scrollbox._stopSmoothScroll();
313          else
314            this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
315        }
316      })]]></field>
317
318      <method name="scrollByIndex">
319        <parameter name="index"/>
320        <parameter name="aSmoothScroll"/>
321        <body><![CDATA[
322          if (index == 0)
323            return;
324
325          // Each scrollByIndex call is expected to scroll the given number of
326          // items. If a previous call is still in progress because of smooth
327          // scrolling, we need to complete it before starting a new one.
328          if (this._scrollTarget) {
329            let elements = this._getScrollableElements();
330            if (this._scrollTarget != elements[0] &&
331                this._scrollTarget != elements[elements.length - 1])
332              this.ensureElementIsVisible(this._scrollTarget, false);
333          }
334
335          var rect = this.scrollClientRect;
336          var [start, end] = this._startEndProps;
337          var x = index > 0 ? rect[end] + 1 : rect[start] - 1;
338          var nextElement = this._elementFromPoint(x, index);
339          if (!nextElement)
340            return;
341
342          var targetElement;
343          if (this._isRTLScrollbox)
344            index *= -1;
345          while (index < 0 && nextElement) {
346            if (this._canScrollToElement(nextElement))
347              targetElement = nextElement;
348            nextElement = nextElement.previousSibling;
349            index++;
350          }
351          while (index > 0 && nextElement) {
352            if (this._canScrollToElement(nextElement))
353              targetElement = nextElement;
354            nextElement = nextElement.nextSibling;
355            index--;
356          }
357          if (!targetElement)
358            return;
359
360          this.ensureElementIsVisible(targetElement, aSmoothScroll);
361        ]]></body>
362      </method>
363
364      <method name="_getScrollableElements">
365        <body><![CDATA[
366          var nodes = this.childNodes;
367          if (nodes.length == 1 &&
368              nodes[0].localName == "children" &&
369              nodes[0].namespaceURI == "http://www.mozilla.org/xbl") {
370            nodes = document.getBindingParent(this).childNodes;
371          }
372
373          return Array.filter(nodes, this._canScrollToElement, this);
374        ]]></body>
375      </method>
376
377      <method name="_elementFromPoint">
378        <parameter name="aX"/>
379        <parameter name="aPhysicalScrollDir"/>
380        <body><![CDATA[
381          var elements = this._getScrollableElements();
382          if (!elements.length)
383            return null;
384
385          if (this._isRTLScrollbox)
386            elements.reverse();
387
388          var [start, end] = this._startEndProps;
389          var low = 0;
390          var high = elements.length - 1;
391
392          if (aX < elements[low].getBoundingClientRect()[start] ||
393              aX > elements[high].getBoundingClientRect()[end])
394            return null;
395
396          var mid, rect;
397          while (low <= high) {
398            mid = Math.floor((low + high) / 2);
399            rect = elements[mid].getBoundingClientRect();
400            if (rect[start] > aX)
401              high = mid - 1; 
402            else if (rect[end] < aX)
403              low = mid + 1;
404            else
405              return elements[mid];
406          }
407
408          // There's no element at the requested coordinate, but the algorithm
409          // from above yields an element next to it, in a random direction.
410          // The desired scrolling direction leads to the correct element.
411
412          if (!aPhysicalScrollDir)
413            return null;
414
415          if (aPhysicalScrollDir < 0 && rect[start] > aX)
416            mid = Math.max(mid - 1, 0);
417          else if (aPhysicalScrollDir > 0 && rect[end] < aX)
418            mid = Math.min(mid + 1, elements.length - 1);
419
420          return elements[mid];
421        ]]></body>
422      </method>
423
424      <method name="_autorepeatbuttonScroll">
425        <parameter name="event"/>
426        <body><![CDATA[
427          var dir = event.originalTarget == this._scrollButtonUp ? -1 : 1;
428          if (this._isRTLScrollbox)
429            dir *= -1;
430
431          this.scrollByPixels(this.scrollIncrement * dir);
432
433          event.stopPropagation();
434        ]]></body>
435      </method>
436
437      <method name="scrollByPixels">
438        <parameter name="px"/>
439        <body><![CDATA[
440          this.scrollPosition += px;
441        ]]></body>
442      </method>
443
444      <!-- 0: idle
445           1: scrolling right
446          -1: scrolling left -->
447      <field name="_isScrolling">0</field>
448      <field name="_prevMouseScrolls">[null, null]</field>
449
450      <method name="_stopSmoothScroll">
451        <body><![CDATA[
452          if (this._isScrolling) {
453            this._scrollAnim.stop();
454            this._isScrolling = 0;
455            this._scrollTarget = null;
456          }
457        ]]></body>
458      </method>
459
460      <method name="_updateScrollButtonsDisabledState">
461        <body><![CDATA[
462          var scrolledToStart = false;
463          var scrolledToEnd = false;
464
465          if (this.hasAttribute("notoverflowing")) {
466            scrolledToStart = true;
467            scrolledToEnd = true;
468          }
469          else if (this.scrollPosition == 0) {
470            // In the RTL case, this means the _last_ element in the
471            // scrollbox is visible
472            if (this._isRTLScrollbox) 
473              scrolledToEnd = true;
474            else
475              scrolledToStart = true;
476          }
477          else if (this.scrollClientSize + this.scrollPosition == this.scrollSize) {
478            // In the RTL case, this means the _first_ element in the
479            // scrollbox is visible
480            if (this._isRTLScrollbox)
481              scrolledToStart = true;
482            else
483              scrolledToEnd = true;
484          }
485
486          if (scrolledToEnd)
487            this.setAttribute("scrolledtoend", "true");
488          else
489            this.removeAttribute("scrolledtoend");
490
491          if (scrolledToStart)
492            this.setAttribute("scrolledtostart", "true");
493          else
494            this.removeAttribute("scrolledtostart");
495        ]]></body>
496      </method>
497    </implementation>
498
499    <handlers>
500      <handler event="DOMMouseScroll"><![CDATA[
501        if (this.orient == "vertical") {
502          // prevent horizontal scrolling from scrolling a vertical scrollbox
503          if (event.axis == event.HORIZONTAL_AXIS)
504            return;
505          this.scrollByIndex(event.detail);
506        }
507        // We allow vertical scrolling to scroll a horizontal scrollbox
508        // because many users have a vertical scroll wheel but no
509        // horizontal support.
510        // Because of this, we need to avoid scrolling chaos on trackpads
511        // and mouse wheels that support simultaneous scrolling in both axes.
512        // We do this by scrolling only when the last two scroll events were
513        // on the same axis as the current scroll event.
514        else {
515          let isVertical = event.axis == event.VERTICAL_AXIS;
516
517          if (this._prevMouseScrolls.every(prev => prev == isVertical))
518            this.scrollByIndex(isVertical && this._isRTLScrollbox ? -event.detail :
519                                                                    event.detail);
520
521          if (this._prevMouseScrolls.length > 1)
522            this._prevMouseScrolls.shift();
523          this._prevMouseScrolls.push(isVertical);
524        }
525
526        event.stopPropagation();
527        event.preventDefault();
528      ]]></handler>
529
530      <handler event="MozMousePixelScroll"><![CDATA[
531        event.stopPropagation();
532        event.preventDefault();
533      ]]></handler>
534
535      <handler event="underflow" phase="capturing"><![CDATA[
536        // filter underflow events which were dispatched on nested scrollboxes
537        if (event.target != this)
538          return;
539
540        // Ignore events that doesn't match our orientation.
541        // Scrollport event orientation:
542        //   0: vertical
543        //   1: horizontal
544        //   2: both
545        if (this.orient == "vertical") {
546          if (event.detail == 1)
547            return;
548        }
549        else {    // horizontal scrollbox
550          if (event.detail == 0)
551            return;
552        }
553
554        this.setAttribute("notoverflowing", "true");
555
556        try {
557          // See bug 341047 and comments in overflow handler as to why 
558          // try..catch is needed here
559          this._updateScrollButtonsDisabledState();
560
561          let childNodes = this._getScrollableElements();
562          if (childNodes && childNodes.length)
563            this.ensureElementIsVisible(childNodes[0], false);
564        }
565        catch(e) {
566          this.removeAttribute("notoverflowing");
567        }
568      ]]></handler>
569
570      <handler event="overflow" phase="capturing"><![CDATA[
571        // filter underflow events which were dispatched on nested scrollboxes
572        if (event.target != this)
573          return;
574
575        // Ignore events that doesn't match our orientation.
576        // Scrollport event orientation:
577        //   0: vertical
578        //   1: horizontal
579        //   2: both
580        if (this.orient == "vertical") {
581          if (event.detail == 1)
582            return;
583        }
584        else {    // horizontal scrollbox
585          if (event.detail == 0)
586            return;
587        }
588
589        this.removeAttribute("notoverflowing");
590
591        try {
592          // See bug 341047, the overflow event is dispatched when the 
593          // scrollbox already is mostly destroyed. This causes some code in
594          // _updateScrollButtonsDisabledState() to throw an error. It also
595          // means that the notoverflowing attribute was removed erroneously,
596          // as the whole overflow event should not be happening in that case.
597          this._updateScrollButtonsDisabledState();
598        } 
599        catch(e) {
600          this.setAttribute("notoverflowing", "true");
601        }
602      ]]></handler>
603
604      <handler event="scroll" action="this._updateScrollButtonsDisabledState()"/>
605    </handlers>
606  </binding>
607
608  <binding id="autorepeatbutton" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
609    <content repeat="hover">
610      <xul:image class="autorepeatbutton-icon"/>
611    </content>
612  </binding>
613
614  <binding id="arrowscrollbox-clicktoscroll" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox">
615    <content>
616      <xul:toolbarbutton class="scrollbutton-up"
617                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
618                         anonid="scrollbutton-up"
619                         onclick="_distanceScroll(event);"
620                         onmousedown="if (event.button == 0) _startScroll(-1);"
621                         onmouseup="if (event.button == 0) _stopScroll();"
622                         onmouseover="_continueScroll(-1);"
623                         onmouseout="_pauseScroll();"/>
624      <xul:spacer class="arrowscrollbox-overflow-start-indicator"
625                  xbl:inherits="collapsed=scrolledtostart"/>
626      <xul:scrollbox class="arrowscrollbox-scrollbox"
627                     anonid="scrollbox"
628                     flex="1"
629                     xbl:inherits="orient,align,pack,dir">
630        <children/>
631      </xul:scrollbox>
632      <xul:spacer class="arrowscrollbox-overflow-end-indicator"
633                  xbl:inherits="collapsed=scrolledtoend"/>
634      <xul:toolbarbutton class="scrollbutton-down"
635                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
636                         anonid="scrollbutton-down"
637                         onclick="_distanceScroll(event);"
638                         onmousedown="if (event.button == 0) _startScroll(1);"
639                         onmouseup="if (event.button == 0) _stopScroll();"
640                         onmouseover="_continueScroll(1);"
641                         onmouseout="_pauseScroll();"/>
642    </content>
643    <implementation implements="nsITimerCallback, nsIDOMEventListener">
644      <constructor><![CDATA[
645        try {
646          this._scrollDelay = this._prefBranch
647                                  .getIntPref("toolkit.scrollbox.clickToScroll.scrollDelay");
648        }
649        catch (ex) {
650        }
651      ]]></constructor>
652
653      <destructor><![CDATA[
654        // Release timer to avoid reference cycles.
655        if (this._scrollTimer) {
656          this._scrollTimer.cancel();
657          this._scrollTimer = null;
658        }
659      ]]></destructor>
660
661      <field name="_scrollIndex">0</field>
662      <field name="_scrollDelay">150</field>
663
664      <method name="notify">
665        <parameter name="aTimer"/>
666        <body>
667        <![CDATA[
668          if (!document)
669            aTimer.cancel();
670
671          this.scrollByIndex(this._scrollIndex);
672        ]]>
673        </body>
674      </method>
675
676      <field name="_arrowScrollAnim"><![CDATA[({
677        scrollbox: this,
678        requestHandle: 0, /* 0 indicates there is no pending request */
679        start: function arrowSmoothScroll_start() {
680          this.lastFrameTime = window.performance.now();
681          if (!this.requestHandle)
682            this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
683        },
684        stop: function arrowSmoothScroll_stop() {
685          window.cancelAnimationFrame(this.requestHandle);
686          this.requestHandle = 0;
687        },
688        sample: function arrowSmoothScroll_handleEvent(timeStamp) {
689          const scrollIndex = this.scrollbox._scrollIndex;
690          const timePassed = timeStamp - this.lastFrameTime;
691          this.lastFrameTime = timeStamp;
692          
693          const scrollDelta = 0.5 * timePassed * scrollIndex;
694          this.scrollbox.scrollPosition += scrollDelta;
695
696          this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
697        }
698      })]]></field>
699
700      <method name="_startScroll">
701        <parameter name="index"/>
702        <body><![CDATA[
703          if (this._isRTLScrollbox)
704            index *= -1;
705          this._scrollIndex = index;
706          this._mousedown = true;
707          if (this.smoothScroll) {
708            this._arrowScrollAnim.start();
709            return;
710          }
711
712          if (!this._scrollTimer)
713            this._scrollTimer =
714              Components.classes["@mozilla.org/timer;1"]
715                        .createInstance(Components.interfaces.nsITimer);
716          else
717            this._scrollTimer.cancel();
718
719          this._scrollTimer.initWithCallback(this, this._scrollDelay,
720                                             this._scrollTimer.TYPE_REPEATING_SLACK);
721          this.notify(this._scrollTimer);
722        ]]>
723        </body>
724      </method>
725
726      <method name="_stopScroll">
727        <body><![CDATA[
728          if (this._scrollTimer)
729            this._scrollTimer.cancel();
730          this._mousedown = false;
731          if (!this._scrollIndex || !this.smoothScroll)
732            return;
733
734          this.scrollByIndex(this._scrollIndex);
735          this._scrollIndex = 0;
736          this._arrowScrollAnim.stop();
737        ]]></body>
738      </method>
739
740      <method name="_pauseScroll">
741        <body><![CDATA[
742          if (this._mousedown) {
743            this._stopScroll();
744            this._mousedown = true;
745            document.addEventListener("mouseup", this, false);
746            document.addEventListener("blur", this, true);
747          }
748        ]]></body>
749      </method>
750
751      <method name="_continueScroll">
752        <parameter name="index"/>
753        <body><![CDATA[
754          if (this._mousedown)
755            this._startScroll(index);
756        ]]></body>
757      </method>
758
759      <method name="handleEvent">
760        <parameter name="aEvent"/>
761        <body><![CDATA[
762          if (aEvent.type == "mouseup" ||
763              aEvent.type == "blur" && aEvent.target == document) {
764            this._mousedown = false;
765            document.removeEventListener("mouseup", this, false);
766            document.removeEventListener("blur", this, true);
767          }
768        ]]></body>
769      </method>
770
771      <method name="_distanceScroll">
772        <parameter name="aEvent"/>
773        <body><![CDATA[
774          if (aEvent.detail < 2 || aEvent.detail > 3)
775            return;
776
777          var scrollBack = (aEvent.originalTarget == this._scrollButtonUp);
778          var scrollLeftOrUp = this._isRTLScrollbox ? !scrollBack : scrollBack;
779          var targetElement;
780
781          if (aEvent.detail == 2) {
782            // scroll by the size of the scrollbox
783            let [start, end] = this._startEndProps;
784            let x;
785            if (scrollLeftOrUp)
786              x = this.scrollClientRect[start] - this.scrollClientSize;
787            else
788              x = this.scrollClientRect[end] + this.scrollClientSize;
789            targetElement = this._elementFromPoint(x, scrollLeftOrUp ? -1 : 1);
790
791            // the next partly-hidden element will become fully visible,
792            // so don't scroll too far
793            if (targetElement)
794              targetElement = scrollBack ?
795                              targetElement.nextSibling :
796                              targetElement.previousSibling;
797          }
798
799          if (!targetElement) {
800            // scroll to the first resp. last element
801            let elements = this._getScrollableElements();
802            targetElement = scrollBack ?
803                            elements[0] :
804                            elements[elements.length - 1];
805          }
806
807          this.ensureElementIsVisible(targetElement);
808        ]]></body>
809      </method>
810
811    </implementation>
812  </binding>
813</bindings>
814