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