forked from mirrors/gecko-dev
`scrollbutton-down` and `scrollbutton-up` toolbarbuttons are not labeled and it would prevent users of assistive technology from knowing their purpose and, for instance, speech-to-text software users won't be able to activate them by calling their name and screen reader users won't know what these controls are expected to do, when passing through them in a browse mode or in a list of buttons/controls. Differential Revision: https://phabricator.services.mozilla.com/D197705
873 lines
26 KiB
JavaScript
873 lines
26 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
// This is loaded into all XUL windows. Wrap in a block to prevent
|
|
// leaking to window scope.
|
|
{
|
|
class MozArrowScrollbox extends MozElements.BaseControl {
|
|
static get inheritedAttributes() {
|
|
return {
|
|
"#scrollbutton-up": "disabled=scrolledtostart",
|
|
".scrollbox-clip": "orient",
|
|
scrollbox: "orient,align,pack,dir,smoothscroll",
|
|
"#scrollbutton-down": "disabled=scrolledtoend",
|
|
};
|
|
}
|
|
|
|
get markup() {
|
|
return `
|
|
<html:link rel="stylesheet" href="chrome://global/skin/toolbarbutton.css"/>
|
|
<html:link rel="stylesheet" href="chrome://global/skin/arrowscrollbox.css"/>
|
|
<toolbarbutton id="scrollbutton-up" part="scrollbutton-up" keyNav="false" data-l10n-id="overflow-scroll-button-up"/>
|
|
<spacer part="overflow-start-indicator"/>
|
|
<box class="scrollbox-clip" part="scrollbox-clip" flex="1">
|
|
<scrollbox part="scrollbox" flex="1">
|
|
<html:slot/>
|
|
</scrollbox>
|
|
</box>
|
|
<spacer part="overflow-end-indicator"/>
|
|
<toolbarbutton id="scrollbutton-down" part="scrollbutton-down" keyNav="false" data-l10n-id="overflow-scroll-button-down"/>
|
|
`;
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({ mode: "open" });
|
|
this.shadowRoot.appendChild(this.fragment);
|
|
|
|
this.scrollbox = this.shadowRoot.querySelector("scrollbox");
|
|
this._scrollButtonUp = this.shadowRoot.getElementById("scrollbutton-up");
|
|
this._scrollButtonDown =
|
|
this.shadowRoot.getElementById("scrollbutton-down");
|
|
|
|
MozXULElement.insertFTLIfNeeded("toolkit/global/arrowscrollbox.ftl");
|
|
|
|
this._arrowScrollAnim = {
|
|
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)
|
|
);
|
|
},
|
|
};
|
|
|
|
this._scrollIndex = 0;
|
|
this._scrollIncrement = null;
|
|
this._ensureElementIsVisibleAnimationFrame = 0;
|
|
this._prevMouseScrolls = [null, null];
|
|
this._touchStart = -1;
|
|
this._scrollButtonUpdatePending = false;
|
|
this._isScrolling = false;
|
|
this._destination = 0;
|
|
this._direction = 0;
|
|
|
|
this.addEventListener("wheel", this.on_wheel);
|
|
this.addEventListener("touchstart", this.on_touchstart);
|
|
this.addEventListener("touchmove", this.on_touchmove);
|
|
this.addEventListener("touchend", this.on_touchend);
|
|
this.shadowRoot.addEventListener("click", this.on_click.bind(this));
|
|
this.shadowRoot.addEventListener(
|
|
"mousedown",
|
|
this.on_mousedown.bind(this)
|
|
);
|
|
this.shadowRoot.addEventListener(
|
|
"mouseover",
|
|
this.on_mouseover.bind(this)
|
|
);
|
|
this.shadowRoot.addEventListener("mouseup", this.on_mouseup.bind(this));
|
|
this.shadowRoot.addEventListener("mouseout", this.on_mouseout.bind(this));
|
|
|
|
// These events don't get retargeted outside of the shadow root, but
|
|
// some callers like tests wait for these events. So run handlers
|
|
// and then retarget events from the scrollbox to the host.
|
|
this.scrollbox.addEventListener(
|
|
"underflow",
|
|
event => {
|
|
this.on_underflow(event);
|
|
this.dispatchEvent(new Event("underflow"));
|
|
},
|
|
true
|
|
);
|
|
this.scrollbox.addEventListener(
|
|
"overflow",
|
|
event => {
|
|
this.on_overflow(event);
|
|
this.dispatchEvent(new Event("overflow"));
|
|
},
|
|
true
|
|
);
|
|
this.scrollbox.addEventListener("scroll", event => {
|
|
this.on_scroll(event);
|
|
this.dispatchEvent(new Event("scroll"));
|
|
});
|
|
|
|
this.scrollbox.addEventListener("scrollend", event => {
|
|
this.on_scrollend(event);
|
|
this.dispatchEvent(new Event("scrollend"));
|
|
});
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.hasConnected) {
|
|
return;
|
|
}
|
|
this.hasConnected = true;
|
|
|
|
document.l10n.connectRoot(this.shadowRoot);
|
|
|
|
if (!this.hasAttribute("smoothscroll")) {
|
|
this.smoothScroll = Services.prefs.getBoolPref(
|
|
"toolkit.scrollbox.smoothScroll",
|
|
true
|
|
);
|
|
}
|
|
|
|
this.removeAttribute("overflowing");
|
|
this.initializeAttributeInheritance();
|
|
this._updateScrollButtonsDisabledState();
|
|
}
|
|
|
|
get fragment() {
|
|
if (!this.constructor.hasOwnProperty("_fragment")) {
|
|
this.constructor._fragment = MozXULElement.parseXULToFragment(
|
|
this.markup
|
|
);
|
|
}
|
|
return document.importNode(this.constructor._fragment, true);
|
|
}
|
|
|
|
get _clickToScroll() {
|
|
return this.hasAttribute("clicktoscroll");
|
|
}
|
|
|
|
get _scrollDelay() {
|
|
if (this._clickToScroll) {
|
|
return Services.prefs.getIntPref(
|
|
"toolkit.scrollbox.clickToScroll.scrollDelay",
|
|
150
|
|
);
|
|
}
|
|
|
|
// Use the same REPEAT_DELAY as "nsRepeatService.h".
|
|
return /Mac/.test(navigator.platform) ? 25 : 50;
|
|
}
|
|
|
|
get scrollIncrement() {
|
|
if (this._scrollIncrement === null) {
|
|
this._scrollIncrement = Services.prefs.getIntPref(
|
|
"toolkit.scrollbox.scrollIncrement",
|
|
20
|
|
);
|
|
}
|
|
return this._scrollIncrement;
|
|
}
|
|
|
|
set smoothScroll(val) {
|
|
this.setAttribute("smoothscroll", !!val);
|
|
}
|
|
|
|
get smoothScroll() {
|
|
return this.getAttribute("smoothscroll") == "true";
|
|
}
|
|
|
|
get scrollClientRect() {
|
|
return this.scrollbox.getBoundingClientRect();
|
|
}
|
|
|
|
get scrollClientSize() {
|
|
return this.getAttribute("orient") == "vertical"
|
|
? this.scrollbox.clientHeight
|
|
: this.scrollbox.clientWidth;
|
|
}
|
|
|
|
get scrollSize() {
|
|
return this.getAttribute("orient") == "vertical"
|
|
? this.scrollbox.scrollHeight
|
|
: this.scrollbox.scrollWidth;
|
|
}
|
|
|
|
get lineScrollAmount() {
|
|
// 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 average width of the elements.
|
|
var elements = this._getScrollableElements();
|
|
return elements.length && this.scrollSize / elements.length;
|
|
}
|
|
|
|
get scrollPosition() {
|
|
return this.getAttribute("orient") == "vertical"
|
|
? this.scrollbox.scrollTop
|
|
: this.scrollbox.scrollLeft;
|
|
}
|
|
|
|
get startEndProps() {
|
|
if (!this._startEndProps) {
|
|
this._startEndProps =
|
|
this.getAttribute("orient") == "vertical"
|
|
? ["top", "bottom"]
|
|
: ["left", "right"];
|
|
}
|
|
return this._startEndProps;
|
|
}
|
|
|
|
get isRTLScrollbox() {
|
|
if (!this._isRTLScrollbox) {
|
|
this._isRTLScrollbox =
|
|
this.getAttribute("orient") != "vertical" &&
|
|
document.defaultView.getComputedStyle(this.scrollbox).direction ==
|
|
"rtl";
|
|
}
|
|
return this._isRTLScrollbox;
|
|
}
|
|
|
|
_onButtonClick(event) {
|
|
if (this._clickToScroll) {
|
|
this._distanceScroll(event);
|
|
}
|
|
}
|
|
|
|
_onButtonMouseDown(event, index) {
|
|
if (this._clickToScroll && event.button == 0) {
|
|
this._startScroll(index);
|
|
}
|
|
}
|
|
|
|
_onButtonMouseUp(event) {
|
|
if (this._clickToScroll && event.button == 0) {
|
|
this._stopScroll();
|
|
}
|
|
}
|
|
|
|
_onButtonMouseOver(index) {
|
|
if (this._clickToScroll) {
|
|
this._continueScroll(index);
|
|
} else {
|
|
this._startScroll(index);
|
|
}
|
|
}
|
|
|
|
_onButtonMouseOut() {
|
|
if (this._clickToScroll) {
|
|
this._pauseScroll();
|
|
} else {
|
|
this._stopScroll();
|
|
}
|
|
}
|
|
|
|
_boundsWithoutFlushing(element) {
|
|
if (!("_DOMWindowUtils" in this)) {
|
|
this._DOMWindowUtils = window.windowUtils;
|
|
}
|
|
|
|
return this._DOMWindowUtils
|
|
? this._DOMWindowUtils.getBoundsWithoutFlushing(element)
|
|
: element.getBoundingClientRect();
|
|
}
|
|
|
|
_canScrollToElement(element) {
|
|
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);
|
|
}
|
|
|
|
ensureElementIsVisible(element, aInstant) {
|
|
if (!this._canScrollToElement(element)) {
|
|
return;
|
|
}
|
|
|
|
if (this._ensureElementIsVisibleAnimationFrame) {
|
|
window.cancelAnimationFrame(this._ensureElementIsVisibleAnimationFrame);
|
|
}
|
|
this._ensureElementIsVisibleAnimationFrame = window.requestAnimationFrame(
|
|
() => {
|
|
element.scrollIntoView({
|
|
block: "nearest",
|
|
behavior: aInstant ? "instant" : "auto",
|
|
});
|
|
this._ensureElementIsVisibleAnimationFrame = 0;
|
|
}
|
|
);
|
|
}
|
|
|
|
scrollByIndex(index, aInstant) {
|
|
if (index == 0) {
|
|
return;
|
|
}
|
|
|
|
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.previousElementSibling;
|
|
index++;
|
|
}
|
|
while (index > 0 && nextElement) {
|
|
if (this._canScrollToElement(nextElement)) {
|
|
targetElement = nextElement;
|
|
}
|
|
nextElement = nextElement.nextElementSibling;
|
|
index--;
|
|
}
|
|
if (!targetElement) {
|
|
return;
|
|
}
|
|
|
|
this.ensureElementIsVisible(targetElement, aInstant);
|
|
}
|
|
|
|
_getScrollableElements() {
|
|
let nodes = this.children;
|
|
if (nodes.length == 1) {
|
|
let node = nodes[0];
|
|
if (
|
|
node.localName == "slot" &&
|
|
node.namespaceURI == "http://www.w3.org/1999/xhtml"
|
|
) {
|
|
nodes = node.getRootNode().host.children;
|
|
}
|
|
}
|
|
return Array.prototype.filter.call(nodes, this._canScrollToElement, this);
|
|
}
|
|
|
|
_elementFromPoint(aX, aPhysicalScrollDir) {
|
|
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];
|
|
}
|
|
|
|
_startScroll(index) {
|
|
if (this.isRTLScrollbox) {
|
|
index *= -1;
|
|
}
|
|
|
|
if (this._clickToScroll) {
|
|
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();
|
|
}
|
|
|
|
let callback;
|
|
if (this._clickToScroll) {
|
|
callback = () => {
|
|
if (!document && this._scrollTimer) {
|
|
this._scrollTimer.cancel();
|
|
}
|
|
this.scrollByIndex(this._scrollIndex);
|
|
};
|
|
} else {
|
|
callback = () => this.scrollByPixels(this.scrollIncrement * index);
|
|
}
|
|
|
|
this._scrollTimer.initWithCallback(
|
|
callback,
|
|
this._scrollDelay,
|
|
Ci.nsITimer.TYPE_REPEATING_SLACK
|
|
);
|
|
|
|
callback();
|
|
}
|
|
|
|
_stopScroll() {
|
|
if (this._scrollTimer) {
|
|
this._scrollTimer.cancel();
|
|
}
|
|
|
|
if (this._clickToScroll) {
|
|
this._mousedown = false;
|
|
if (!this._scrollIndex || !this.smoothScroll) {
|
|
return;
|
|
}
|
|
|
|
this.scrollByIndex(this._scrollIndex);
|
|
this._scrollIndex = 0;
|
|
|
|
this._arrowScrollAnim.stop();
|
|
}
|
|
}
|
|
|
|
_pauseScroll() {
|
|
if (this._mousedown) {
|
|
this._stopScroll();
|
|
this._mousedown = true;
|
|
document.addEventListener("mouseup", this);
|
|
document.addEventListener("blur", this, true);
|
|
}
|
|
}
|
|
|
|
_continueScroll(index) {
|
|
if (this._mousedown) {
|
|
this._startScroll(index);
|
|
}
|
|
}
|
|
|
|
_distanceScroll(aEvent) {
|
|
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.nextElementSibling
|
|
: targetElement.previousElementSibling;
|
|
}
|
|
}
|
|
|
|
if (!targetElement) {
|
|
// scroll to the first resp. last element
|
|
let elements = this._getScrollableElements();
|
|
targetElement = scrollBack
|
|
? elements[0]
|
|
: elements[elements.length - 1];
|
|
}
|
|
|
|
this.ensureElementIsVisible(targetElement);
|
|
}
|
|
|
|
handleEvent(aEvent) {
|
|
if (
|
|
aEvent.type == "mouseup" ||
|
|
(aEvent.type == "blur" && aEvent.target == document)
|
|
) {
|
|
this._mousedown = false;
|
|
document.removeEventListener("mouseup", this);
|
|
document.removeEventListener("blur", this, true);
|
|
}
|
|
}
|
|
|
|
scrollByPixels(aPixels, aInstant) {
|
|
let scrollOptions = { behavior: aInstant ? "instant" : "auto" };
|
|
scrollOptions[this.startEndProps[0]] = aPixels;
|
|
this.scrollbox.scrollBy(scrollOptions);
|
|
}
|
|
|
|
_updateScrollButtonsDisabledState() {
|
|
if (!this.hasAttribute("overflowing")) {
|
|
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(() => {
|
|
if (!this.isConnected) {
|
|
// We've been destroyed in the meantime.
|
|
return;
|
|
}
|
|
|
|
this._scrollButtonUpdatePending = false;
|
|
|
|
let scrolledToStart = false;
|
|
let scrolledToEnd = false;
|
|
|
|
if (!this.hasAttribute("overflowing")) {
|
|
scrolledToStart = true;
|
|
scrolledToEnd = true;
|
|
} else {
|
|
let isAtEdge = (element, start) => {
|
|
let edge = start ? this.startEndProps[0] : this.startEndProps[1];
|
|
let scrollEdge = this._boundsWithoutFlushing(this.scrollbox)[
|
|
edge
|
|
];
|
|
let elementEdge = this._boundsWithoutFlushing(element)[edge];
|
|
// This is enough slop (>2/3) so that no subpixel value should
|
|
// get us confused about whether we reached the end.
|
|
const EPSILON = 0.7;
|
|
if (start) {
|
|
return scrollEdge <= elementEdge + EPSILON;
|
|
}
|
|
return elementEdge <= scrollEdge + EPSILON;
|
|
};
|
|
|
|
let elements = this._getScrollableElements();
|
|
let [startElement, endElement] = [
|
|
elements[0],
|
|
elements[elements.length - 1],
|
|
];
|
|
if (this.isRTLScrollbox) {
|
|
[startElement, endElement] = [endElement, startElement];
|
|
}
|
|
scrolledToStart =
|
|
startElement && isAtEdge(startElement, /* start = */ true);
|
|
scrolledToEnd =
|
|
endElement && isAtEdge(endElement, /* start = */ false);
|
|
if (this.isRTLScrollbox) {
|
|
[scrolledToStart, scrolledToEnd] = [
|
|
scrolledToEnd,
|
|
scrolledToStart,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (scrolledToEnd) {
|
|
this.setAttribute("scrolledtoend", "true");
|
|
} else {
|
|
this.removeAttribute("scrolledtoend");
|
|
}
|
|
|
|
if (scrolledToStart) {
|
|
this.setAttribute("scrolledtostart", "true");
|
|
} else {
|
|
this.removeAttribute("scrolledtostart");
|
|
}
|
|
}, 0);
|
|
});
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
// Release timer to avoid reference cycles.
|
|
if (this._scrollTimer) {
|
|
this._scrollTimer.cancel();
|
|
this._scrollTimer = null;
|
|
}
|
|
document.l10n.disconnectRoot(this.shadowRoot);
|
|
}
|
|
|
|
on_wheel(event) {
|
|
// Don't consume the event if we can't scroll.
|
|
if (!this.hasAttribute("overflowing")) {
|
|
return;
|
|
}
|
|
|
|
const { deltaMode } = event;
|
|
let doScroll = false;
|
|
let instant;
|
|
let scrollAmount = 0;
|
|
if (this.getAttribute("orient") == "vertical") {
|
|
doScroll = true;
|
|
scrollAmount = event.deltaY;
|
|
} 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;
|
|
scrollAmount = scrollByDelta;
|
|
if (deltaMode == event.DOM_DELTA_PIXEL) {
|
|
instant = true;
|
|
}
|
|
}
|
|
|
|
if (this._prevMouseScrolls.length > 1) {
|
|
this._prevMouseScrolls.shift();
|
|
}
|
|
this._prevMouseScrolls.push(isVertical);
|
|
}
|
|
|
|
if (doScroll) {
|
|
let direction = scrollAmount < 0 ? -1 : 1;
|
|
|
|
if (deltaMode == event.DOM_DELTA_PAGE) {
|
|
scrollAmount *= this.scrollClientSize;
|
|
} else if (deltaMode == event.DOM_DELTA_LINE) {
|
|
// Try to not scroll by more than one page when using line scrolling,
|
|
// so that all elements are scrollable.
|
|
let lineAmount = this.lineScrollAmount;
|
|
let clientSize = this.scrollClientSize;
|
|
if (Math.abs(scrollAmount * lineAmount) > clientSize) {
|
|
// NOTE: This still tries to scroll a non-fractional amount of
|
|
// items per line scrolled.
|
|
scrollAmount =
|
|
Math.max(1, Math.floor(clientSize / lineAmount)) * direction;
|
|
}
|
|
scrollAmount *= lineAmount;
|
|
} else {
|
|
// DOM_DELTA_PIXEL, leave scrollAmount untouched.
|
|
}
|
|
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();
|
|
}
|
|
|
|
on_touchstart(event) {
|
|
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.getAttribute("orient") == "vertical"
|
|
? event.touches[0].screenY
|
|
: event.touches[0].screenX;
|
|
}
|
|
}
|
|
|
|
on_touchmove(event) {
|
|
if (event.touches.length == 1 && this._touchStart >= 0) {
|
|
var touchPoint =
|
|
this.getAttribute("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();
|
|
}
|
|
}
|
|
|
|
on_touchend() {
|
|
this._touchStart = -1;
|
|
}
|
|
|
|
on_underflow(event) {
|
|
// Ignore underflow events:
|
|
// - from nested scrollable elements
|
|
// - corresponding to an overflow event that we ignored
|
|
if (event.target != this.scrollbox || !this.hasAttribute("overflowing")) {
|
|
return;
|
|
}
|
|
|
|
// Ignore events that doesn't match our orientation.
|
|
// Scrollport event orientation:
|
|
// 0: vertical
|
|
// 1: horizontal
|
|
// 2: both
|
|
if (this.getAttribute("orient") == "vertical") {
|
|
if (event.detail == 1) {
|
|
return;
|
|
}
|
|
} else if (event.detail == 0) {
|
|
// horizontal scrollbox
|
|
return;
|
|
}
|
|
|
|
this.removeAttribute("overflowing");
|
|
this._updateScrollButtonsDisabledState();
|
|
}
|
|
|
|
on_overflow(event) {
|
|
// Ignore overflow events:
|
|
// - from nested scrollable elements
|
|
if (event.target != this.scrollbox) {
|
|
return;
|
|
}
|
|
|
|
// Ignore events that doesn't match our orientation.
|
|
// Scrollport event orientation:
|
|
// 0: vertical
|
|
// 1: horizontal
|
|
// 2: both
|
|
if (this.getAttribute("orient") == "vertical") {
|
|
if (event.detail == 1) {
|
|
return;
|
|
}
|
|
} else if (event.detail == 0) {
|
|
// horizontal scrollbox
|
|
return;
|
|
}
|
|
|
|
this.setAttribute("overflowing", "true");
|
|
this._updateScrollButtonsDisabledState();
|
|
}
|
|
|
|
on_scroll() {
|
|
this._isScrolling = true;
|
|
this._updateScrollButtonsDisabledState();
|
|
}
|
|
|
|
on_scrollend() {
|
|
this._isScrolling = false;
|
|
this._destination = 0;
|
|
this._direction = 0;
|
|
}
|
|
|
|
on_click(event) {
|
|
if (
|
|
event.originalTarget != this._scrollButtonUp &&
|
|
event.originalTarget != this._scrollButtonDown
|
|
) {
|
|
return;
|
|
}
|
|
this._onButtonClick(event);
|
|
}
|
|
|
|
on_mousedown(event) {
|
|
if (event.originalTarget == this._scrollButtonUp) {
|
|
this._onButtonMouseDown(event, -1);
|
|
}
|
|
if (event.originalTarget == this._scrollButtonDown) {
|
|
this._onButtonMouseDown(event, 1);
|
|
}
|
|
}
|
|
|
|
on_mouseup(event) {
|
|
if (
|
|
event.originalTarget != this._scrollButtonUp &&
|
|
event.originalTarget != this._scrollButtonDown
|
|
) {
|
|
return;
|
|
}
|
|
this._onButtonMouseUp(event);
|
|
}
|
|
|
|
on_mouseover(event) {
|
|
if (event.originalTarget == this._scrollButtonUp) {
|
|
this._onButtonMouseOver(-1);
|
|
}
|
|
if (event.originalTarget == this._scrollButtonDown) {
|
|
this._onButtonMouseOver(1);
|
|
}
|
|
}
|
|
|
|
on_mouseout(event) {
|
|
if (
|
|
event.originalTarget != this._scrollButtonUp &&
|
|
event.originalTarget != this._scrollButtonDown
|
|
) {
|
|
return;
|
|
}
|
|
this._onButtonMouseOut();
|
|
}
|
|
}
|
|
|
|
customElements.define("arrowscrollbox", MozArrowScrollbox);
|
|
}
|