зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1514926 - Convert the arrowscrollbox binding to a Custom Element r=dao
This uses Shadow DOM slotting instead of XBL, and migrates styles from using XBL anonymous traversal to using CSS Shadow Parts. This also removes the basecontrol binding, since this was the last binding to extend it. Differential Revision: https://phabricator.services.mozilla.com/D43651 --HG-- rename : toolkit/content/widgets/scrollbox.xml => toolkit/content/widgets/arrowscrollbox.js extra : moz-landing-system : lando
This commit is contained in:
Родитель
aa6db0d7ab
Коммит
98a08a3ce4
|
@ -53,7 +53,7 @@
|
|||
this._scrollButtonWidth = 0;
|
||||
this._lastNumPinned = 0;
|
||||
this._pinnedTabsLayoutCache = null;
|
||||
this._animateElement = this.arrowScrollbox._scrollButtonDown;
|
||||
this._animateElement = this.arrowScrollbox;
|
||||
this._tabClipWidth = Services.prefs.getIntPref(
|
||||
"browser.tabs.tabClipWidth"
|
||||
);
|
||||
|
@ -518,12 +518,11 @@
|
|||
// return to avoid drawing the drop indicator
|
||||
var pixelsToScroll = 0;
|
||||
if (this.getAttribute("overflow") == "true") {
|
||||
var targetAnonid = event.originalTarget.getAttribute("anonid");
|
||||
switch (targetAnonid) {
|
||||
case "scrollbutton-up":
|
||||
switch (event.originalTarget) {
|
||||
case arrowScrollbox._scrollButtonUp:
|
||||
pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
|
||||
break;
|
||||
case "scrollbutton-down":
|
||||
case arrowScrollbox._scrollButtonDown:
|
||||
pixelsToScroll = arrowScrollbox.scrollIncrement;
|
||||
break;
|
||||
}
|
||||
|
@ -956,7 +955,7 @@
|
|||
|
||||
_initializeArrowScrollbox() {
|
||||
let arrowScrollbox = this.arrowScrollbox;
|
||||
arrowScrollbox.addEventListener(
|
||||
arrowScrollbox.shadowRoot.addEventListener(
|
||||
"underflow",
|
||||
event => {
|
||||
// Ignore underflow events:
|
||||
|
@ -986,7 +985,7 @@
|
|||
true
|
||||
);
|
||||
|
||||
arrowScrollbox.addEventListener("overflow", event => {
|
||||
arrowScrollbox.shadowRoot.addEventListener("overflow", event => {
|
||||
// Ignore overflow events:
|
||||
// - from nested scrollable elements
|
||||
// - for vertical orientation
|
||||
|
@ -1002,7 +1001,7 @@
|
|||
this._handleTabSelect(true);
|
||||
});
|
||||
|
||||
// Override scrollbox.xml method, since our scrollbox's children are
|
||||
// Override arrowscrollbox.js method, since our scrollbox's children are
|
||||
// inherited from the scrollbox binding parent (this).
|
||||
arrowScrollbox._getScrollableElements = () => {
|
||||
return this.allTabs.filter(arrowScrollbox._canScrollToElement);
|
||||
|
|
|
@ -479,11 +479,10 @@
|
|||
}
|
||||
|
||||
// Autoscroll the popup strip if we drag over the scroll buttons.
|
||||
let anonid = event.originalTarget.getAttribute("anonid");
|
||||
let scrollDir = 0;
|
||||
if (anonid == "scrollbutton-up") {
|
||||
if (event.originalTarget == this.scrollBox._scrollButtonUp) {
|
||||
scrollDir = -1;
|
||||
} else if (anonid == "scrollbutton-down") {
|
||||
} else if (event.originalTarget == this.scrollBox._scrollButtonDown) {
|
||||
scrollDir = 1;
|
||||
}
|
||||
if (scrollDir != 0) {
|
||||
|
|
|
@ -380,13 +380,6 @@ notification[value="translation"] menulist > .menulist-dropmarker {
|
|||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Tab bar scroll arrows */
|
||||
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-up > .toolbarbutton-icon,
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-down > .toolbarbutton-icon {
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
/* All tabs menupopup */
|
||||
|
||||
.alltabs-item[selected="true"] {
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-scrollbox {
|
||||
.tabbrowser-arrowscrollbox::part(scrollbox) {
|
||||
/* Needed to prevent tabstrip from growing as wide as the sum of the tabs'
|
||||
page-title widths (when we'd rather have it be as wide as the window and
|
||||
compress the tabs to their minimum size): */
|
||||
|
@ -434,8 +434,8 @@
|
|||
}
|
||||
|
||||
/* Tab Overflow */
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:not([collapsed]),
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:not([collapsed]) {
|
||||
.tabbrowser-arrowscrollbox:not([scrolledtostart])::part(arrowscrollbox-overflow-start-indicator),
|
||||
.tabbrowser-arrowscrollbox:not([scrolledtoend])::part(arrowscrollbox-overflow-end-indicator) {
|
||||
width: 18px;
|
||||
background-image: url(chrome://browser/skin/tabbrowser/tab-overflow-indicator.png);
|
||||
background-size: 17px 100%;
|
||||
|
@ -450,28 +450,28 @@
|
|||
z-index: 3; /* the selected tab's z-index + 1 */
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:-moz-locale-dir(rtl),
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:-moz-locale-dir(ltr) {
|
||||
.tabbrowser-arrowscrollbox:-moz-locale-dir(rtl)::part(arrowscrollbox-overflow-start-indicator),
|
||||
.tabbrowser-arrowscrollbox:-moz-locale-dir(ltr)::part(arrowscrollbox-overflow-end-indicator) {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:not([collapsed]) {
|
||||
.tabbrowser-arrowscrollbox:not([scrolledtostart])::part(arrowscrollbox-overflow-start-indicator) {
|
||||
margin-inline-start: -1px;
|
||||
margin-inline-end: -17px;
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:not([collapsed]) {
|
||||
.tabbrowser-arrowscrollbox:not([scrolledtoend])::part(arrowscrollbox-overflow-end-indicator) {
|
||||
margin-inline-start: -17px;
|
||||
margin-inline-end: -1px;
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator[collapsed],
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator[collapsed] {
|
||||
.tabbrowser-arrowscrollbox[scrolledtostart]::part(arrowscrollbox-overflow-start-indicator),
|
||||
.tabbrowser-arrowscrollbox[scrolledtoend]::part(arrowscrollbox-overflow-end-indicator) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator,
|
||||
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator {
|
||||
.tabbrowser-arrowscrollbox::part(arrowscrollbox-overflow-start-indicator),
|
||||
.tabbrowser-arrowscrollbox::part(arrowscrollbox-overflow-end-indicator) {
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
|
@ -686,8 +686,8 @@
|
|||
|
||||
/* Tab bar scroll arrows */
|
||||
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-up,
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-down {
|
||||
.tabbrowser-arrowscrollbox::part(scrollbutton-up),
|
||||
.tabbrowser-arrowscrollbox::part(scrollbutton-down) {
|
||||
list-style-image: url(chrome://browser/skin/arrow-left.svg) !important;
|
||||
-moz-context-properties: fill, fill-opacity;
|
||||
fill: var(--lwt-toolbarbutton-icon-fill, currentColor);
|
||||
|
@ -695,8 +695,8 @@
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-up:-moz-locale-dir(rtl),
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-down:-moz-locale-dir(ltr) {
|
||||
.tabbrowser-arrowscrollbox:-moz-locale-dir(rtl)::part(scrollbutton-up),
|
||||
.tabbrowser-arrowscrollbox:-moz-locale-dir(ltr)::part(scrollbutton-down) {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
|
|
|
@ -55,8 +55,8 @@ toolbar[brighttext] {
|
|||
|
||||
/* ::::: primary toolbar buttons ::::: */
|
||||
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-up[disabled=true],
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-down[disabled=true],
|
||||
.tabbrowser-arrowscrollbox[scrolledtostart=true]::part(scrollbutton-up),
|
||||
.tabbrowser-arrowscrollbox[scrolledtoend=true]::part(scrollbutton-down),
|
||||
:root:not([customizing]) .toolbarbutton-1[disabled=true],
|
||||
/* specialcase the overflow and the hamburger button so they show up disabled in customize mode. */
|
||||
#nav-bar-overflow-button[disabled=true],
|
||||
|
@ -74,22 +74,22 @@ toolbar[brighttext] {
|
|||
}
|
||||
|
||||
#TabsToolbar .toolbarbutton-1,
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-up,
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-down {
|
||||
.tabbrowser-arrowscrollbox::part(scrollbutton-up),
|
||||
.tabbrowser-arrowscrollbox::part(scrollbutton-down) {
|
||||
margin: 0 0 var(--tabs-navbar-shadow-size) !important;
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-up,
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-down {
|
||||
.tabbrowser-arrowscrollbox::part(scrollbutton-up),
|
||||
.tabbrowser-arrowscrollbox::part(scrollbutton-down) {
|
||||
-moz-appearance: none;
|
||||
padding: 0 var(--toolbarbutton-inner-padding) !important;
|
||||
}
|
||||
|
||||
#navigator-toolbox:not(:hover) .tabbrowser-arrowscrollbox > .scrollbutton-down:not([highlight]) {
|
||||
#navigator-toolbox:not(:hover) .tabbrowser-arrowscrollbox:not([highlight])::part(scrollbutton-down) {
|
||||
transition: 1s background-color ease-out;
|
||||
}
|
||||
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-down[highlight] {
|
||||
.tabbrowser-arrowscrollbox[highlight]::part(scrollbutton-down) {
|
||||
background-color: Highlight;
|
||||
}
|
||||
|
||||
|
@ -175,8 +175,8 @@ toolbar[brighttext] .toolbaritem-combined-buttons > separator {
|
|||
}
|
||||
|
||||
#PersonalToolbar .toolbarbutton-1:not([disabled=true]):not([checked]):not([open]):not(:active):hover,
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled=true]):hover,
|
||||
.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled=true]):hover,
|
||||
.tabbrowser-arrowscrollbox:not([scrolledtostart=true])::part(scrollbutton-up):hover,
|
||||
.tabbrowser-arrowscrollbox:not([scrolledtoend=true])::part(scrollbutton-down):hover,
|
||||
.findbar-button:not(:-moz-any([checked="true"],[disabled="true"])):hover,
|
||||
toolbarbutton.bookmark-item:not(.subviewbutton):hover:not([disabled="true"]):not([open]),
|
||||
toolbar .toolbarbutton-1:not([disabled=true]):not([checked]):not([open]):not(:active):hover > .toolbarbutton-icon,
|
||||
|
|
|
@ -753,6 +753,7 @@
|
|||
document.documentURI == "chrome://extensions/content/dummy.xul";
|
||||
if (!isDummyDocument) {
|
||||
for (let script of [
|
||||
"chrome://global/content/elements/arrowscrollbox.js",
|
||||
"chrome://global/content/elements/dialog.js",
|
||||
"chrome://global/content/elements/general.js",
|
||||
"chrome://global/content/elements/button.js",
|
||||
|
|
|
@ -65,9 +65,7 @@ toolkit.jar:
|
|||
content/global/bindings/datekeeper.js (widgets/datekeeper.js)
|
||||
content/global/bindings/datepicker.js (widgets/datepicker.js)
|
||||
content/global/bindings/datetimebox.css (widgets/datetimebox.css)
|
||||
content/global/bindings/general.xml (widgets/general.xml)
|
||||
content/global/bindings/popup.xml (widgets/popup.xml)
|
||||
content/global/bindings/scrollbox.xml (widgets/scrollbox.xml)
|
||||
content/global/bindings/spinner.js (widgets/spinner.js)
|
||||
content/global/bindings/textbox.xml (widgets/textbox.xml)
|
||||
content/global/bindings/timekeeper.js (widgets/timekeeper.js)
|
||||
|
@ -93,6 +91,7 @@ toolkit.jar:
|
|||
content/global/elements/marquee.js (widgets/marquee.js)
|
||||
content/global/elements/menulist.js (widgets/menulist.js)
|
||||
content/global/elements/popupnotification.js (widgets/popupnotification.js)
|
||||
content/global/elements/arrowscrollbox.js (widgets/arrowscrollbox.js)
|
||||
content/global/elements/search-textbox.js (widgets/search-textbox.js)
|
||||
content/global/elements/stringbundle.js (widgets/stringbundle.js)
|
||||
content/global/elements/tabbox.js (widgets/tabbox.js)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
|
||||
<!--
|
||||
XUL Widget Test for basic properties - this test checks that the basic
|
||||
properties defined in general.xml and inherited by a number of elements
|
||||
properties defined in general.js and inherited by a number of elements
|
||||
work properly.
|
||||
-->
|
||||
<window title="Basic Properties Test"
|
||||
|
|
|
@ -0,0 +1,866 @@
|
|||
/* 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.
|
||||
{
|
||||
const { Services } = ChromeUtils.import(
|
||||
"resource://gre/modules/Services.jsm"
|
||||
);
|
||||
|
||||
class MozArrowScrollbox extends MozElements.BaseControl {
|
||||
static get inheritedAttributes() {
|
||||
return {
|
||||
".scrollbutton-up": "orient,disabled=scrolledtostart",
|
||||
"[part=scrollbox]": "orient,align,pack,dir,smoothscroll",
|
||||
".scrollbutton-down": "orient,disabled=scrolledtoend",
|
||||
};
|
||||
}
|
||||
|
||||
get markup() {
|
||||
return `
|
||||
<html:link rel="stylesheet" href="chrome://global/skin/global.css"/>
|
||||
<toolbarbutton class="scrollbutton-up" part="scrollbutton-up"/>
|
||||
<spacer part="arrowscrollbox-overflow-start-indicator"/>
|
||||
<scrollbox part="scrollbox" flex="1">
|
||||
<html:slot/>
|
||||
</scrollbox>
|
||||
<spacer part="arrowscrollbox-overflow-end-indicator"/>
|
||||
<toolbarbutton class="scrollbutton-down" part="scrollbutton-down"/>
|
||||
`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot.appendChild(this.fragment);
|
||||
|
||||
this.scrollbox = this.shadowRoot.querySelector("[part=scrollbox]");
|
||||
this._scrollButtonUp = this.shadowRoot.querySelector(".scrollbutton-up");
|
||||
this._scrollButtonDown = this.shadowRoot.querySelector(
|
||||
".scrollbutton-down"
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
this.setAttribute("notoverflowing", "true");
|
||||
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);
|
||||
return val;
|
||||
}
|
||||
|
||||
get smoothScroll() {
|
||||
if (!this.hasAttribute("smoothscroll")) {
|
||||
this.smoothScroll = Services.prefs.getBoolPref(
|
||||
"toolkit.scrollbox.smoothScroll",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return this.getAttribute("smoothscroll") == "true";
|
||||
}
|
||||
|
||||
get scrollClientRect() {
|
||||
return this.scrollbox.getBoundingClientRect();
|
||||
}
|
||||
|
||||
get scrollClientSize() {
|
||||
return this.orient == "vertical"
|
||||
? this.scrollbox.clientHeight
|
||||
: this.scrollbox.clientWidth;
|
||||
}
|
||||
|
||||
get scrollSize() {
|
||||
return this.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 avalage with of the elements.
|
||||
var elements = this._getScrollableElements();
|
||||
return elements.length && this.scrollSize / elements.length;
|
||||
}
|
||||
|
||||
get scrollPosition() {
|
||||
return this.orient == "vertical"
|
||||
? this.scrollbox.scrollTop
|
||||
: this.scrollbox.scrollLeft;
|
||||
}
|
||||
|
||||
get startEndProps() {
|
||||
if (!this._startEndProps) {
|
||||
this._startEndProps =
|
||||
this.orient == "vertical" ? ["top", "bottom"] : ["left", "right"];
|
||||
}
|
||||
return this._startEndProps;
|
||||
}
|
||||
|
||||
get isRTLScrollbox() {
|
||||
if (!this._isRTLScrollbox) {
|
||||
this._isRTLScrollbox =
|
||||
this.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 == "children" &&
|
||||
node.namespaceURI == "http://www.mozilla.org/xbl"
|
||||
) {
|
||||
nodes = document.getBindingParent(this).children;
|
||||
} else 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("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(() => {
|
||||
if (!this.isConnected) {
|
||||
// We've been destroyed in the meantime.
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// Release timer to avoid reference cycles.
|
||||
if (this._scrollTimer) {
|
||||
this._scrollTimer.cancel();
|
||||
this._scrollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
on_wheel(event) {
|
||||
// Don't consume the event if we can't scroll.
|
||||
if (this.hasAttribute("notoverflowing")) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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.orient == "vertical"
|
||||
? event.touches[0].screenY
|
||||
: event.touches[0].screenX;
|
||||
}
|
||||
}
|
||||
|
||||
on_touchmove(event) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
on_touchend(event) {
|
||||
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("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();
|
||||
}
|
||||
|
||||
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.orient == "vertical") {
|
||||
if (event.detail == 1) {
|
||||
return;
|
||||
}
|
||||
} else if (event.detail == 0) {
|
||||
// horizontal scrollbox
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeAttribute("notoverflowing");
|
||||
this._updateScrollButtonsDisabledState();
|
||||
}
|
||||
|
||||
on_scroll(event) {
|
||||
this._isScrolling = true;
|
||||
this._updateScrollButtonsDisabledState();
|
||||
}
|
||||
|
||||
on_scrollend(event) {
|
||||
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);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<?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="generalBindings"
|
||||
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="basecontrol">
|
||||
<implementation implements="nsIDOMXULControlElement">
|
||||
<!-- public implementation -->
|
||||
<property name="disabled" onset="if (val) this.setAttribute('disabled', 'true');
|
||||
else this.removeAttribute('disabled');
|
||||
return val;"
|
||||
onget="return this.getAttribute('disabled') == 'true';"/>
|
||||
<property name="tabIndex" onget="return parseInt(this.getAttribute('tabindex')) || 0"
|
||||
onset="if (val) this.setAttribute('tabindex', val);
|
||||
else this.removeAttribute('tabindex'); return val;"/>
|
||||
</implementation>
|
||||
</binding>
|
||||
|
||||
</bindings>
|
|
@ -73,7 +73,7 @@
|
|||
if (!super.shadowRoot.firstElementChild) {
|
||||
super.shadowRoot.appendChild(this.fragment);
|
||||
|
||||
// Retarget events from shadow DOM scrolbox to the popup itself.
|
||||
// Retarget events from shadow DOM arrowscrollbox to the host.
|
||||
this.scrollBox.addEventListener("scroll", ev =>
|
||||
this.dispatchEvent(new Event("scroll"))
|
||||
);
|
||||
|
@ -83,6 +83,10 @@
|
|||
this.scrollBox.addEventListener("underflow", ev =>
|
||||
this.dispatchEvent(new Event("underflow"))
|
||||
);
|
||||
this.scrollBox._scrollButtonUp.classList.add("menupopup-scrollbutton");
|
||||
this.scrollBox._scrollButtonDown.classList.add(
|
||||
"menupopup-scrollbutton"
|
||||
);
|
||||
}
|
||||
return super.shadowRoot;
|
||||
}
|
||||
|
@ -111,13 +115,13 @@
|
|||
|
||||
get styles() {
|
||||
let s = `
|
||||
:host(.in-menulist) .popup-internal-box > .scrollbutton-up,
|
||||
:host(.in-menulist) .popup-internal-box > .arrowscrollbox-overflow-start-indicator,
|
||||
:host(.in-menulist) .popup-internal-box > .arrowscrollbox-overflow-end-indicator,
|
||||
:host(.in-menulist) .popup-internal-box > .scrollbutton-down {
|
||||
:host(.in-menulist) .popup-internal-box::part(scrollbutton-up),
|
||||
:host(.in-menulist) .popup-internal-box::part(arrowscrollbox-overflow-start-indicator),
|
||||
:host(.in-menulist) .popup-internal-box::part(arrowscrollbox-overflow-end-indicator),
|
||||
:host(.in-menulist) .popup-internal-box::part(scrollbutton-down) {
|
||||
display: none;
|
||||
}
|
||||
:host(.in-menulist) .popup-internal-box > .arrowscrollbox-scrollbox {
|
||||
:host(.in-menulist) .popup-internal-box::part(scrollbox) {
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
|
@ -166,6 +170,7 @@
|
|||
this._draggingState = this.NOT_DRAGGING;
|
||||
this._clearScrollTimer();
|
||||
this.releaseCapture();
|
||||
this.scrollBox.scrollbox.scrollTop = 0;
|
||||
});
|
||||
|
||||
this.addEventListener("mousedown", event => {
|
||||
|
|
|
@ -1,806 +0,0 @@
|
|||
<?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="arrowscrollbox" extends="chrome://global/content/bindings/general.xml#basecontrol">
|
||||
<content>
|
||||
<xul:toolbarbutton class="scrollbutton-up"
|
||||
anonid="scrollbutton-up"
|
||||
xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"/>
|
||||
<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"/>
|
||||
</content>
|
||||
|
||||
<implementation>
|
||||
<constructor><![CDATA[
|
||||
if (!this.hasAttribute("smoothscroll")) {
|
||||
this.smoothScroll = this._prefBranch
|
||||
.getBoolPref("toolkit.scrollbox.smoothScroll", true);
|
||||
}
|
||||
|
||||
this.setAttribute("notoverflowing", "true");
|
||||
this._updateScrollButtonsDisabledState();
|
||||
|
||||
// Ultimately Bug 1514926 will convert arrowscrollbox binding to a custom element.
|
||||
// For the needs of Bug 1497189, where we apply a custom CSP to about:addons, we had
|
||||
// to remove inline handlers and hence added event listeners for mouse events here.
|
||||
this.addEventListener("click", (e) => {
|
||||
if (e.originalTarget != this._scrollButtonUp && e.originalTarget != this._scrollButtonDown) {
|
||||
return;
|
||||
}
|
||||
this._onButtonClick(e);
|
||||
});
|
||||
this.addEventListener("mousedown", (e) => {
|
||||
if (e.originalTarget == this._scrollButtonUp) {
|
||||
this._onButtonMouseDown(e, -1);
|
||||
}
|
||||
if (e.originalTarget == this._scrollButtonDown) {
|
||||
this._onButtonMouseDown(e, 1);
|
||||
}
|
||||
});
|
||||
this.addEventListener("mouseup", (e) => {
|
||||
if (e.originalTarget != this._scrollButtonUp && e.originalTarget != this._scrollButtonDown) {
|
||||
return;
|
||||
}
|
||||
this._onButtonMouseUp(e);
|
||||
});
|
||||
this.addEventListener("mouseover", (e) => {
|
||||
if (e.originalTarget == this._scrollButtonUp) {
|
||||
this._onButtonMouseOver(-1);
|
||||
}
|
||||
if (e.originalTarget == this._scrollButtonDown) {
|
||||
this._onButtonMouseOver(1);
|
||||
}
|
||||
});
|
||||
this.addEventListener("mouseout", (e) => {
|
||||
if (e.originalTarget != this._scrollButtonUp && e.originalTarget != this._scrollButtonDown) {
|
||||
return;
|
||||
}
|
||||
this._onButtonMouseOut();
|
||||
});
|
||||
]]></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="_scrollIndex">0</field>
|
||||
|
||||
<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>
|
||||
|
||||
<property name="_clickToScroll" readonly="true">
|
||||
<getter><![CDATA[
|
||||
return this.hasAttribute("clicktoscroll");
|
||||
]]></getter>
|
||||
</property>
|
||||
|
||||
<property name="_scrollDelay" readonly="true">
|
||||
<getter><![CDATA[
|
||||
if (this._clickToScroll) {
|
||||
return this._prefBranch.getIntPref(
|
||||
"toolkit.scrollbox.clickToScroll.scrollDelay", 150);
|
||||
}
|
||||
|
||||
// Use the same REPEAT_DELAY as "nsRepeatService.h".
|
||||
return /Mac/.test(navigator.platform) ? 25 : 50;
|
||||
]]></getter>
|
||||
</property>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<method name="_onButtonClick">
|
||||
<parameter name="event"/>
|
||||
<body><![CDATA[
|
||||
if (this._clickToScroll) {
|
||||
this._distanceScroll(event);
|
||||
}
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_onButtonMouseDown">
|
||||
<parameter name="event"/>
|
||||
<parameter name="index"/>
|
||||
<body><![CDATA[
|
||||
if (this._clickToScroll && event.button == 0) {
|
||||
this._startScroll(index);
|
||||
}
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_onButtonMouseUp">
|
||||
<parameter name="event"/>
|
||||
<body><![CDATA[
|
||||
if (this._clickToScroll && event.button == 0) {
|
||||
this._stopScroll();
|
||||
}
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_onButtonMouseOver">
|
||||
<parameter name="index"/>
|
||||
<body><![CDATA[
|
||||
if (this._clickToScroll) {
|
||||
this._continueScroll(index);
|
||||
} else {
|
||||
this._startScroll(index);
|
||||
}
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_onButtonMouseOut">
|
||||
<parameter name="index"/>
|
||||
<body><![CDATA[
|
||||
if (this._clickToScroll) {
|
||||
this._pauseScroll();
|
||||
} else {
|
||||
this._stopScroll();
|
||||
}
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_boundsWithoutFlushing">
|
||||
<parameter name="element"/>
|
||||
<body><![CDATA[
|
||||
if (!("_DOMWindowUtils" in this)) {
|
||||
this._DOMWindowUtils = window.windowUtils;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<field name="_ensureElementIsVisibleAnimationFrame">0</field>
|
||||
<method name="ensureElementIsVisible">
|
||||
<parameter name="element"/>
|
||||
<parameter name="aInstant"/>
|
||||
<body><![CDATA[
|
||||
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;
|
||||
});
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="scrollByIndex">
|
||||
<parameter name="index"/>
|
||||
<parameter name="aInstant"/>
|
||||
<body><![CDATA[
|
||||
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);
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_getScrollableElements">
|
||||
<body><![CDATA[
|
||||
let nodes = this.children;
|
||||
if (nodes.length == 1) {
|
||||
let node = nodes[0];
|
||||
if (node.localName == "children" &&
|
||||
node.namespaceURI == "http://www.mozilla.org/xbl") {
|
||||
nodes = document.getBindingParent(this).children;
|
||||
} else 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);
|
||||
]]></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._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();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="_stopScroll">
|
||||
<body><![CDATA[
|
||||
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();
|
||||
}
|
||||
]]></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="_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.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);
|
||||
]]></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="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(() => {
|
||||
if (!this._startEndProps) {
|
||||
// We've been destroyed in the meantime.
|
||||
return;
|
||||
}
|
||||
|
||||
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[
|
||||
// Don't consume the event if we can't scroll.
|
||||
if (this.hasAttribute("notoverflowing")) {
|
||||
return;
|
||||
}
|
||||
|
||||
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>
|
||||
</bindings>
|
|
@ -535,10 +535,6 @@ scrollbox[smoothscroll=true] {
|
|||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
arrowscrollbox {
|
||||
-moz-binding: url("chrome://global/content/bindings/scrollbox.xml#arrowscrollbox");
|
||||
}
|
||||
|
||||
/********** stringbundle **********/
|
||||
|
||||
stringbundle,
|
||||
|
|
|
@ -4,19 +4,26 @@
|
|||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
.scrollbutton-up > .toolbarbutton-icon {
|
||||
arrowscrollbox[scrolledtoend=true]::part(arrowscrollbox-overflow-end-indicator),
|
||||
arrowscrollbox[scrolledtostart=true]::part(arrowscrollbox-overflow-start-indicator),
|
||||
arrowscrollbox[notoverflowing=true]::part(scrollbutton-up),
|
||||
arrowscrollbox[notoverflowing=true]::part(scrollbutton-down) {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
.scrollbutton-up.menupopup-scrollbutton > .toolbarbutton-icon {
|
||||
-moz-appearance: button-arrow-up;
|
||||
}
|
||||
|
||||
.scrollbutton-down > .toolbarbutton-icon {
|
||||
.scrollbutton-down.menupopup-scrollbutton > .toolbarbutton-icon {
|
||||
-moz-appearance: button-arrow-down;
|
||||
}
|
||||
|
||||
.scrollbutton-up[orient="horizontal"] > .toolbarbutton-icon {
|
||||
.scrollbutton-up.menupopup-scrollbutton[orient="horizontal"] > .toolbarbutton-icon {
|
||||
-moz-appearance: button-arrow-previous;
|
||||
}
|
||||
|
||||
.scrollbutton-down[orient="horizontal"] > .toolbarbutton-icon {
|
||||
.scrollbutton-down.menupopup-scrollbutton[orient="horizontal"] > .toolbarbutton-icon {
|
||||
-moz-appearance: button-arrow-next;
|
||||
}
|
||||
|
||||
|
@ -24,8 +31,8 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
arrowscrollbox:not([clicktoscroll="true"]) > .scrollbutton-up,
|
||||
arrowscrollbox:not([clicktoscroll="true"]) > .scrollbutton-down {
|
||||
arrowscrollbox:not([clicktoscroll="true"])::part(scrollbutton-up),
|
||||
arrowscrollbox:not([clicktoscroll="true"])::part(scrollbutton-down) {
|
||||
-moz-appearance: none;
|
||||
border: 1px solid ThreeDShadow;
|
||||
padding: 0;
|
||||
|
|
|
@ -216,8 +216,7 @@ xul|popupnotificationcontent {
|
|||
|
||||
/* autorepeatbuttons in menus */
|
||||
|
||||
.popup-internal-box > .scrollbutton-up,
|
||||
.popup-internal-box > .scrollbutton-down {
|
||||
.menupopup-scrollbutton {
|
||||
height: 15px;
|
||||
position: relative;
|
||||
list-style-image: none;
|
||||
|
@ -233,25 +232,24 @@ xul|popupnotificationcontent {
|
|||
-moz-appearance: menuitem;
|
||||
}
|
||||
|
||||
.popup-internal-box > .scrollbutton-up {
|
||||
.scrollbutton-up.menupopup-scrollbutton {
|
||||
padding-top: 1px; /* 4px padding-top from the .popup-internal-box. */
|
||||
margin-bottom: -15px;
|
||||
}
|
||||
|
||||
.popup-internal-box > .scrollbutton-up > .toolbarbutton-icon {
|
||||
.scrollbutton-up.menupopup-scrollbutton > .toolbarbutton-icon {
|
||||
-moz-appearance: button-arrow-up;
|
||||
}
|
||||
|
||||
.popup-internal-box > .scrollbutton-down {
|
||||
.scrollbutton-down.menupopup-scrollbutton {
|
||||
padding-top: 5px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.popup-internal-box > .scrollbutton-down > .toolbarbutton-icon {
|
||||
.scrollbutton-down.menupopup-scrollbutton > .toolbarbutton-icon {
|
||||
-moz-appearance: button-arrow-down;
|
||||
}
|
||||
|
||||
.popup-internal-box > .scrollbutton-up[disabled="true"],
|
||||
.popup-internal-box > .scrollbutton-down[disabled="true"] {
|
||||
.menupopup-scrollbutton[disabled="true"] {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,14 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
|
||||
arrowscrollbox[scrolledtoend=true]::part(arrowscrollbox-overflow-end-indicator),
|
||||
arrowscrollbox[scrolledtostart=true]::part(arrowscrollbox-overflow-start-indicator),
|
||||
arrowscrollbox[notoverflowing=true]::part(scrollbutton-up),
|
||||
arrowscrollbox[notoverflowing=true]::part(scrollbutton-down) {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
/* Horizontal enabled */
|
||||
.scrollbutton-up[orient="horizontal"] {
|
||||
list-style-image: url("chrome://global/skin/arrow/arrow-lft-sharp.gif");
|
||||
|
|
|
@ -4,6 +4,13 @@
|
|||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
arrowscrollbox[scrolledtoend=true]::part(arrowscrollbox-overflow-end-indicator),
|
||||
arrowscrollbox[scrolledtostart=true]::part(arrowscrollbox-overflow-start-indicator),
|
||||
arrowscrollbox[notoverflowing=true]::part(scrollbutton-up),
|
||||
arrowscrollbox[notoverflowing=true]::part(scrollbutton-down) {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
/*
|
||||
* Scroll arrows
|
||||
*/
|
||||
|
@ -71,15 +78,15 @@
|
|||
margin-inline-end: 2px;
|
||||
}
|
||||
|
||||
arrowscrollbox:not([clicktoscroll="true"]) > .scrollbutton-up,
|
||||
arrowscrollbox:not([clicktoscroll="true"]) > .scrollbutton-down {
|
||||
arrowscrollbox:not([clicktoscroll="true"])::part(scrollbutton-up),
|
||||
arrowscrollbox:not([clicktoscroll="true"])::part(scrollbutton-down) {
|
||||
-moz-appearance: none;
|
||||
border: 1px solid transparent;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
arrowscrollbox:not([clicktoscroll="true"]) > .scrollbutton-up:not([disabled="true"]):hover,
|
||||
arrowscrollbox:not([clicktoscroll="true"]) > .scrollbutton-down:not([disabled="true"]):hover {
|
||||
arrowscrollbox:not([clicktoscroll="true"]):not([scrolledtostart=true])::part(scrollbutton-up):hover,
|
||||
arrowscrollbox:not([clicktoscroll="true"]):not([scrolledtoend=true])::part(scrollbutton-down):hover {
|
||||
margin: 1px;
|
||||
border: 1px inset ThreeDFace;
|
||||
padding-top: 2px;
|
||||
|
|
Загрузка…
Ссылка в новой задаче