diff options
Diffstat (limited to 'toolkit/content/widgets/scrollbox.xml')
-rw-r--r-- | toolkit/content/widgets/scrollbox.xml | 908 |
1 files changed, 908 insertions, 0 deletions
diff --git a/toolkit/content/widgets/scrollbox.xml b/toolkit/content/widgets/scrollbox.xml new file mode 100644 index 000000000..ff57a5911 --- /dev/null +++ b/toolkit/content/widgets/scrollbox.xml @@ -0,0 +1,908 @@ +<?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> + </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) { + try { + this._scrollIncrement = this._prefBranch + .getIntPref("toolkit.scrollbox.scrollIncrement"); + } + catch (ex) { + this._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 { + try { + this._smoothScroll = this._prefBranch + .getBoolPref("toolkit.scrollbox.smoothScroll"); + } + catch (ex) { + this._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="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> + <property name="scrollboxPaddingStart" readonly="true"> + <getter><![CDATA[ + var ltr = (window.getComputedStyle(this, null).direction == "ltr"); + var paddingStartName = ltr ? "padding-left" : "padding-right"; + var scrollboxStyle = window.getComputedStyle(this._scrollbox, null); + return parseFloat(scrollboxStyle.getPropertyValue(paddingStartName)); + ]]></getter> + </property> + <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> + + <property name="_startEndProps" readonly="true"> + <getter><![CDATA[ + return this.orient == "vertical" ? + ["top", "bottom"] : ["left", "right"]; + ]]></getter> + </property> + + <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, null); + 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[ + this._stopSmoothScroll(); + if (amountToScroll == 0) + return; + + this._scrollTarget = element; + // Positive amountToScroll makes us scroll right (elements fly left), negative scrolls left. + this._isScrolling = amountToScroll < 0 ? -1 : 1; + + this._scrollAnim.start(amountToScroll); + ]]></body> + </method> + + <field name="_scrollAnim"><![CDATA[({ + scrollbox: this, + requestHandle: 0, /* 0 indicates there is no pending request */ + start: function scrollAnim_start(distance) { + this.distance = distance; + this.startPos = this.scrollbox.scrollPosition; + 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; + }, + sample: function scrollAnim_handleEvent(timeStamp) { + const timePassed = 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)); + } + })]]></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"> + <body><![CDATA[ + var scrolledToStart = false; + var scrolledToEnd = false; + + if (this.hasAttribute("notoverflowing")) { + scrolledToStart = true; + scrolledToEnd = true; + } + else if (this.scrollPosition == 0) { + // In the RTL case, this means the _last_ element in the + // scrollbox is visible + if (this._isRTLScrollbox) + scrolledToEnd = true; + else + scrolledToStart = true; + } + else if (this.scrollClientSize + this.scrollPosition == this.scrollSize) { + // In the RTL case, this means the _first_ element in the + // scrollbox is visible + if (this._isRTLScrollbox) + scrolledToStart = true; + else + scrolledToEnd = true; + } + + 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[ + if (this.orient == "vertical") { + if (event.deltaMode == event.DOM_DELTA_PIXEL) + this.scrollByPixels(event.deltaY); + else if (event.deltaMode == event.DOM_DELTA_PAGE) + this.scrollByPage(event.deltaY); + else + this.scrollByIndex(event.deltaY); + } + // 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. + else { + 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)) { + if (event.deltaMode == event.DOM_DELTA_PIXEL) + this.scrollByPixels(scrollByDelta); + else if (event.deltaMode == event.DOM_DELTA_PAGE) + this.scrollByPage(scrollByDelta); + else + this.scrollByIndex(scrollByDelta); + } + + if (this._prevMouseScrolls.length > 1) + this._prevMouseScrolls.shift(); + this._prevMouseScrolls.push(isVertical); + } + + 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(); + + let childNodes = this._getScrollableElements(); + if (childNodes && childNodes.length) + this.ensureElementIsVisible(childNodes[0], false); + } + 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" action="this._updateScrollButtonsDisabledState()"/> + </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[ + try { + this._scrollDelay = this._prefBranch + .getIntPref("toolkit.scrollbox.clickToScroll.scrollDelay"); + } + catch (ex) { + } + ]]></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, false); + 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, false); + 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> |