michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: let Cc = Components.classes; michael@0: let Ci = Components.interfaces; michael@0: let Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: var global = this; michael@0: michael@0: let ClickEventHandler = { michael@0: init: function init() { michael@0: this._scrollable = null; michael@0: this._scrolldir = ""; michael@0: this._startX = null; michael@0: this._startY = null; michael@0: this._screenX = null; michael@0: this._screenY = null; michael@0: this._lastFrame = null; michael@0: michael@0: Cc["@mozilla.org/eventlistenerservice;1"] michael@0: .getService(Ci.nsIEventListenerService) michael@0: .addSystemEventListener(global, "mousedown", this, true); michael@0: michael@0: addMessageListener("Autoscroll:Stop", this); michael@0: }, michael@0: michael@0: isAutoscrollBlocker: function(node) { michael@0: let mmPaste = Services.prefs.getBoolPref("middlemouse.paste"); michael@0: let mmScrollbarPosition = Services.prefs.getBoolPref("middlemouse.scrollbarPosition"); michael@0: michael@0: while (node) { michael@0: if ((node instanceof content.HTMLAnchorElement || node instanceof content.HTMLAreaElement) && michael@0: node.hasAttribute("href")) { michael@0: return true; michael@0: } michael@0: michael@0: if (mmPaste && (node instanceof content.HTMLInputElement || michael@0: node instanceof content.HTMLTextAreaElement)) { michael@0: return true; michael@0: } michael@0: michael@0: if (node instanceof content.XULElement && mmScrollbarPosition michael@0: && (node.localName == "scrollbar" || node.localName == "scrollcorner")) { michael@0: return true; michael@0: } michael@0: michael@0: node = node.parentNode; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: startScroll: function(event) { michael@0: // this is a list of overflow property values that allow scrolling michael@0: const scrollingAllowed = ['scroll', 'auto']; michael@0: michael@0: // go upward in the DOM and find any parent element that has a overflow michael@0: // area and can therefore be scrolled michael@0: for (this._scrollable = event.originalTarget; this._scrollable; michael@0: this._scrollable = this._scrollable.parentNode) { michael@0: // do not use overflow based autoscroll for and michael@0: // Elements or non-html elements such as svg or Document nodes michael@0: // also make sure to skip select elements that are not multiline michael@0: if (!(this._scrollable instanceof content.HTMLElement) || michael@0: ((this._scrollable instanceof content.HTMLSelectElement) && !this._scrollable.multiple)) { michael@0: continue; michael@0: } michael@0: michael@0: var overflowx = this._scrollable.ownerDocument.defaultView michael@0: .getComputedStyle(this._scrollable, '') michael@0: .getPropertyValue('overflow-x'); michael@0: var overflowy = this._scrollable.ownerDocument.defaultView michael@0: .getComputedStyle(this._scrollable, '') michael@0: .getPropertyValue('overflow-y'); michael@0: // we already discarded non-multiline selects so allow vertical michael@0: // scroll for multiline ones directly without checking for a michael@0: // overflow property michael@0: var scrollVert = this._scrollable.scrollTopMax && michael@0: (this._scrollable instanceof content.HTMLSelectElement || michael@0: scrollingAllowed.indexOf(overflowy) >= 0); michael@0: michael@0: // do not allow horizontal scrolling for select elements, it leads michael@0: // to visual artifacts and is not the expected behavior anyway michael@0: if (!(this._scrollable instanceof content.HTMLSelectElement) && michael@0: this._scrollable.scrollLeftMax && michael@0: scrollingAllowed.indexOf(overflowx) >= 0) { michael@0: this._scrolldir = scrollVert ? "NSEW" : "EW"; michael@0: break; michael@0: } else if (scrollVert) { michael@0: this._scrolldir = "NS"; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!this._scrollable) { michael@0: this._scrollable = event.originalTarget.ownerDocument.defaultView; michael@0: if (this._scrollable.scrollMaxX > 0) { michael@0: this._scrolldir = this._scrollable.scrollMaxY > 0 ? "NSEW" : "EW"; michael@0: } else if (this._scrollable.scrollMaxY > 0) { michael@0: this._scrolldir = "NS"; michael@0: } else { michael@0: this._scrollable = null; // abort scrolling michael@0: return; michael@0: } michael@0: } michael@0: michael@0: let [enabled] = sendSyncMessage("Autoscroll:Start", michael@0: {scrolldir: this._scrolldir, michael@0: screenX: event.screenX, michael@0: screenY: event.screenY}); michael@0: if (!enabled) { michael@0: this._scrollable = null; michael@0: return; michael@0: } michael@0: michael@0: Cc["@mozilla.org/eventlistenerservice;1"] michael@0: .getService(Ci.nsIEventListenerService) michael@0: .addSystemEventListener(global, "mousemove", this, true); michael@0: addEventListener("pagehide", this, true); michael@0: michael@0: this._ignoreMouseEvents = true; michael@0: this._startX = event.screenX; michael@0: this._startY = event.screenY; michael@0: this._screenX = event.screenX; michael@0: this._screenY = event.screenY; michael@0: this._scrollErrorX = 0; michael@0: this._scrollErrorY = 0; michael@0: this._lastFrame = content.mozAnimationStartTime; michael@0: michael@0: content.mozRequestAnimationFrame(this); michael@0: }, michael@0: michael@0: stopScroll: function() { michael@0: if (this._scrollable) { michael@0: this._scrollable = null; michael@0: michael@0: Cc["@mozilla.org/eventlistenerservice;1"] michael@0: .getService(Ci.nsIEventListenerService) michael@0: .removeSystemEventListener(global, "mousemove", this, true); michael@0: removeEventListener("pagehide", this, true); michael@0: } michael@0: }, michael@0: michael@0: accelerate: function(curr, start) { michael@0: const speed = 12; michael@0: var val = (curr - start) / speed; michael@0: michael@0: if (val > 1) michael@0: return val * Math.sqrt(val) - 1; michael@0: if (val < -1) michael@0: return val * Math.sqrt(-val) + 1; michael@0: return 0; michael@0: }, michael@0: michael@0: roundToZero: function(num) { michael@0: if (num > 0) michael@0: return Math.floor(num); michael@0: return Math.ceil(num); michael@0: }, michael@0: michael@0: autoscrollLoop: function(timestamp) { michael@0: if (!this._scrollable) { michael@0: // Scrolling has been canceled michael@0: return; michael@0: } michael@0: michael@0: // avoid long jumps when the browser hangs for more than michael@0: // |maxTimeDelta| ms michael@0: const maxTimeDelta = 100; michael@0: var timeDelta = Math.min(maxTimeDelta, timestamp - this._lastFrame); michael@0: // we used to scroll |accelerate()| pixels every 20ms (50fps) michael@0: var timeCompensation = timeDelta / 20; michael@0: this._lastFrame = timestamp; michael@0: michael@0: var actualScrollX = 0; michael@0: var actualScrollY = 0; michael@0: // don't bother scrolling vertically when the scrolldir is only horizontal michael@0: // and the other way around michael@0: if (this._scrolldir != 'EW') { michael@0: var y = this.accelerate(this._screenY, this._startY) * timeCompensation; michael@0: var desiredScrollY = this._scrollErrorY + y; michael@0: actualScrollY = this.roundToZero(desiredScrollY); michael@0: this._scrollErrorY = (desiredScrollY - actualScrollY); michael@0: } michael@0: if (this._scrolldir != 'NS') { michael@0: var x = this.accelerate(this._screenX, this._startX) * timeCompensation; michael@0: var desiredScrollX = this._scrollErrorX + x; michael@0: actualScrollX = this.roundToZero(desiredScrollX); michael@0: this._scrollErrorX = (desiredScrollX - actualScrollX); michael@0: } michael@0: michael@0: if (this._scrollable instanceof content.Window) { michael@0: this._scrollable.scrollBy(actualScrollX, actualScrollY); michael@0: } else { // an element with overflow michael@0: this._scrollable.scrollLeft += actualScrollX; michael@0: this._scrollable.scrollTop += actualScrollY; michael@0: } michael@0: content.mozRequestAnimationFrame(this); michael@0: }, michael@0: michael@0: sample: function(timestamp) { michael@0: this.autoscrollLoop(timestamp); michael@0: }, michael@0: michael@0: handleEvent: function(event) { michael@0: if (event.type == "mousemove") { michael@0: this._screenX = event.screenX; michael@0: this._screenY = event.screenY; michael@0: } else if (event.type == "mousedown") { michael@0: if (event.isTrusted & michael@0: !event.defaultPrevented && michael@0: event.button == 1 && michael@0: !this._scrollable && michael@0: !this.isAutoscrollBlocker(event.originalTarget)) { michael@0: this.startScroll(event); michael@0: } michael@0: } else if (event.type == "pagehide") { michael@0: if (this._scrollable) { michael@0: var doc = michael@0: this._scrollable.ownerDocument || this._scrollable.document; michael@0: if (doc == event.target) { michael@0: sendAsyncMessage("Autoscroll:Cancel"); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: receiveMessage: function(msg) { michael@0: switch (msg.name) { michael@0: case "Autoscroll:Stop": { michael@0: this.stopScroll(); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: }; michael@0: ClickEventHandler.init(); michael@0: michael@0: let PopupBlocking = { michael@0: popupData: null, michael@0: popupDataInternal: null, michael@0: michael@0: init: function() { michael@0: addEventListener("DOMPopupBlocked", this, true); michael@0: addEventListener("pageshow", this, true); michael@0: addEventListener("pagehide", this, true); michael@0: michael@0: addMessageListener("PopupBlocking:UnblockPopup", this); michael@0: }, michael@0: michael@0: receiveMessage: function(msg) { michael@0: switch (msg.name) { michael@0: case "PopupBlocking:UnblockPopup": { michael@0: let i = msg.data.index; michael@0: if (this.popupData && this.popupData[i]) { michael@0: let data = this.popupData[i]; michael@0: let internals = this.popupDataInternal[i]; michael@0: let dwi = internals.requestingWindow; michael@0: michael@0: // If we have a requesting window and the requesting document is michael@0: // still the current document, open the popup. michael@0: if (dwi && dwi.document == internals.requestingDocument) { michael@0: dwi.open(data.popupWindowURI, data.popupWindowName, data.popupWindowFeatures); michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function(ev) { michael@0: switch (ev.type) { michael@0: case "DOMPopupBlocked": michael@0: return this.onPopupBlocked(ev); michael@0: case "pageshow": michael@0: return this.onPageShow(ev); michael@0: case "pagehide": michael@0: return this.onPageHide(ev); michael@0: } michael@0: }, michael@0: michael@0: onPopupBlocked: function(ev) { michael@0: if (!this.popupData) { michael@0: this.popupData = new Array(); michael@0: this.popupDataInternal = new Array(); michael@0: } michael@0: michael@0: let obj = { michael@0: popupWindowURI: ev.popupWindowURI.spec, michael@0: popupWindowFeatures: ev.popupWindowFeatures, michael@0: popupWindowName: ev.popupWindowName michael@0: }; michael@0: michael@0: let internals = { michael@0: requestingWindow: ev.requestingWindow, michael@0: requestingDocument: ev.requestingWindow.document, michael@0: }; michael@0: michael@0: this.popupData.push(obj); michael@0: this.popupDataInternal.push(internals); michael@0: this.updateBlockedPopups(true); michael@0: }, michael@0: michael@0: onPageShow: function(ev) { michael@0: if (this.popupData) { michael@0: let i = 0; michael@0: while (i < this.popupData.length) { michael@0: // Filter out irrelevant reports. michael@0: if (this.popupDataInternal[i].requestingWindow && michael@0: (this.popupDataInternal[i].requestingWindow.document == michael@0: this.popupDataInternal[i].requestingDocument)) { michael@0: i++; michael@0: } else { michael@0: this.popupData.splice(i, 1); michael@0: this.popupDataInternal.splice(i, 1); michael@0: } michael@0: } michael@0: if (this.popupData.length == 0) { michael@0: this.popupData = null; michael@0: this.popupDataInternal = null; michael@0: } michael@0: this.updateBlockedPopups(false); michael@0: } michael@0: }, michael@0: michael@0: onPageHide: function(ev) { michael@0: if (this.popupData) { michael@0: this.popupData = null; michael@0: this.popupDataInternal = null; michael@0: this.updateBlockedPopups(false); michael@0: } michael@0: }, michael@0: michael@0: updateBlockedPopups: function(freshPopup) { michael@0: sendAsyncMessage("PopupBlocking:UpdateBlockedPopups", michael@0: {blockedPopups: this.popupData, freshPopup: freshPopup}); michael@0: }, michael@0: }; michael@0: PopupBlocking.init();