Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 let Cc = Components.classes;
7 let Ci = Components.interfaces;
8 let Cu = Components.utils;
10 Cu.import("resource://gre/modules/Services.jsm");
12 var global = this;
14 let ClickEventHandler = {
15 init: function init() {
16 this._scrollable = null;
17 this._scrolldir = "";
18 this._startX = null;
19 this._startY = null;
20 this._screenX = null;
21 this._screenY = null;
22 this._lastFrame = null;
24 Cc["@mozilla.org/eventlistenerservice;1"]
25 .getService(Ci.nsIEventListenerService)
26 .addSystemEventListener(global, "mousedown", this, true);
28 addMessageListener("Autoscroll:Stop", this);
29 },
31 isAutoscrollBlocker: function(node) {
32 let mmPaste = Services.prefs.getBoolPref("middlemouse.paste");
33 let mmScrollbarPosition = Services.prefs.getBoolPref("middlemouse.scrollbarPosition");
35 while (node) {
36 if ((node instanceof content.HTMLAnchorElement || node instanceof content.HTMLAreaElement) &&
37 node.hasAttribute("href")) {
38 return true;
39 }
41 if (mmPaste && (node instanceof content.HTMLInputElement ||
42 node instanceof content.HTMLTextAreaElement)) {
43 return true;
44 }
46 if (node instanceof content.XULElement && mmScrollbarPosition
47 && (node.localName == "scrollbar" || node.localName == "scrollcorner")) {
48 return true;
49 }
51 node = node.parentNode;
52 }
53 return false;
54 },
56 startScroll: function(event) {
57 // this is a list of overflow property values that allow scrolling
58 const scrollingAllowed = ['scroll', 'auto'];
60 // go upward in the DOM and find any parent element that has a overflow
61 // area and can therefore be scrolled
62 for (this._scrollable = event.originalTarget; this._scrollable;
63 this._scrollable = this._scrollable.parentNode) {
64 // do not use overflow based autoscroll for <html> and <body>
65 // Elements or non-html elements such as svg or Document nodes
66 // also make sure to skip select elements that are not multiline
67 if (!(this._scrollable instanceof content.HTMLElement) ||
68 ((this._scrollable instanceof content.HTMLSelectElement) && !this._scrollable.multiple)) {
69 continue;
70 }
72 var overflowx = this._scrollable.ownerDocument.defaultView
73 .getComputedStyle(this._scrollable, '')
74 .getPropertyValue('overflow-x');
75 var overflowy = this._scrollable.ownerDocument.defaultView
76 .getComputedStyle(this._scrollable, '')
77 .getPropertyValue('overflow-y');
78 // we already discarded non-multiline selects so allow vertical
79 // scroll for multiline ones directly without checking for a
80 // overflow property
81 var scrollVert = this._scrollable.scrollTopMax &&
82 (this._scrollable instanceof content.HTMLSelectElement ||
83 scrollingAllowed.indexOf(overflowy) >= 0);
85 // do not allow horizontal scrolling for select elements, it leads
86 // to visual artifacts and is not the expected behavior anyway
87 if (!(this._scrollable instanceof content.HTMLSelectElement) &&
88 this._scrollable.scrollLeftMax &&
89 scrollingAllowed.indexOf(overflowx) >= 0) {
90 this._scrolldir = scrollVert ? "NSEW" : "EW";
91 break;
92 } else if (scrollVert) {
93 this._scrolldir = "NS";
94 break;
95 }
96 }
98 if (!this._scrollable) {
99 this._scrollable = event.originalTarget.ownerDocument.defaultView;
100 if (this._scrollable.scrollMaxX > 0) {
101 this._scrolldir = this._scrollable.scrollMaxY > 0 ? "NSEW" : "EW";
102 } else if (this._scrollable.scrollMaxY > 0) {
103 this._scrolldir = "NS";
104 } else {
105 this._scrollable = null; // abort scrolling
106 return;
107 }
108 }
110 let [enabled] = sendSyncMessage("Autoscroll:Start",
111 {scrolldir: this._scrolldir,
112 screenX: event.screenX,
113 screenY: event.screenY});
114 if (!enabled) {
115 this._scrollable = null;
116 return;
117 }
119 Cc["@mozilla.org/eventlistenerservice;1"]
120 .getService(Ci.nsIEventListenerService)
121 .addSystemEventListener(global, "mousemove", this, true);
122 addEventListener("pagehide", this, true);
124 this._ignoreMouseEvents = true;
125 this._startX = event.screenX;
126 this._startY = event.screenY;
127 this._screenX = event.screenX;
128 this._screenY = event.screenY;
129 this._scrollErrorX = 0;
130 this._scrollErrorY = 0;
131 this._lastFrame = content.mozAnimationStartTime;
133 content.mozRequestAnimationFrame(this);
134 },
136 stopScroll: function() {
137 if (this._scrollable) {
138 this._scrollable = null;
140 Cc["@mozilla.org/eventlistenerservice;1"]
141 .getService(Ci.nsIEventListenerService)
142 .removeSystemEventListener(global, "mousemove", this, true);
143 removeEventListener("pagehide", this, true);
144 }
145 },
147 accelerate: function(curr, start) {
148 const speed = 12;
149 var val = (curr - start) / speed;
151 if (val > 1)
152 return val * Math.sqrt(val) - 1;
153 if (val < -1)
154 return val * Math.sqrt(-val) + 1;
155 return 0;
156 },
158 roundToZero: function(num) {
159 if (num > 0)
160 return Math.floor(num);
161 return Math.ceil(num);
162 },
164 autoscrollLoop: function(timestamp) {
165 if (!this._scrollable) {
166 // Scrolling has been canceled
167 return;
168 }
170 // avoid long jumps when the browser hangs for more than
171 // |maxTimeDelta| ms
172 const maxTimeDelta = 100;
173 var timeDelta = Math.min(maxTimeDelta, timestamp - this._lastFrame);
174 // we used to scroll |accelerate()| pixels every 20ms (50fps)
175 var timeCompensation = timeDelta / 20;
176 this._lastFrame = timestamp;
178 var actualScrollX = 0;
179 var actualScrollY = 0;
180 // don't bother scrolling vertically when the scrolldir is only horizontal
181 // and the other way around
182 if (this._scrolldir != 'EW') {
183 var y = this.accelerate(this._screenY, this._startY) * timeCompensation;
184 var desiredScrollY = this._scrollErrorY + y;
185 actualScrollY = this.roundToZero(desiredScrollY);
186 this._scrollErrorY = (desiredScrollY - actualScrollY);
187 }
188 if (this._scrolldir != 'NS') {
189 var x = this.accelerate(this._screenX, this._startX) * timeCompensation;
190 var desiredScrollX = this._scrollErrorX + x;
191 actualScrollX = this.roundToZero(desiredScrollX);
192 this._scrollErrorX = (desiredScrollX - actualScrollX);
193 }
195 if (this._scrollable instanceof content.Window) {
196 this._scrollable.scrollBy(actualScrollX, actualScrollY);
197 } else { // an element with overflow
198 this._scrollable.scrollLeft += actualScrollX;
199 this._scrollable.scrollTop += actualScrollY;
200 }
201 content.mozRequestAnimationFrame(this);
202 },
204 sample: function(timestamp) {
205 this.autoscrollLoop(timestamp);
206 },
208 handleEvent: function(event) {
209 if (event.type == "mousemove") {
210 this._screenX = event.screenX;
211 this._screenY = event.screenY;
212 } else if (event.type == "mousedown") {
213 if (event.isTrusted &
214 !event.defaultPrevented &&
215 event.button == 1 &&
216 !this._scrollable &&
217 !this.isAutoscrollBlocker(event.originalTarget)) {
218 this.startScroll(event);
219 }
220 } else if (event.type == "pagehide") {
221 if (this._scrollable) {
222 var doc =
223 this._scrollable.ownerDocument || this._scrollable.document;
224 if (doc == event.target) {
225 sendAsyncMessage("Autoscroll:Cancel");
226 }
227 }
228 }
229 },
231 receiveMessage: function(msg) {
232 switch (msg.name) {
233 case "Autoscroll:Stop": {
234 this.stopScroll();
235 break;
236 }
237 }
238 },
239 };
240 ClickEventHandler.init();
242 let PopupBlocking = {
243 popupData: null,
244 popupDataInternal: null,
246 init: function() {
247 addEventListener("DOMPopupBlocked", this, true);
248 addEventListener("pageshow", this, true);
249 addEventListener("pagehide", this, true);
251 addMessageListener("PopupBlocking:UnblockPopup", this);
252 },
254 receiveMessage: function(msg) {
255 switch (msg.name) {
256 case "PopupBlocking:UnblockPopup": {
257 let i = msg.data.index;
258 if (this.popupData && this.popupData[i]) {
259 let data = this.popupData[i];
260 let internals = this.popupDataInternal[i];
261 let dwi = internals.requestingWindow;
263 // If we have a requesting window and the requesting document is
264 // still the current document, open the popup.
265 if (dwi && dwi.document == internals.requestingDocument) {
266 dwi.open(data.popupWindowURI, data.popupWindowName, data.popupWindowFeatures);
267 }
268 }
269 break;
270 }
271 }
272 },
274 handleEvent: function(ev) {
275 switch (ev.type) {
276 case "DOMPopupBlocked":
277 return this.onPopupBlocked(ev);
278 case "pageshow":
279 return this.onPageShow(ev);
280 case "pagehide":
281 return this.onPageHide(ev);
282 }
283 },
285 onPopupBlocked: function(ev) {
286 if (!this.popupData) {
287 this.popupData = new Array();
288 this.popupDataInternal = new Array();
289 }
291 let obj = {
292 popupWindowURI: ev.popupWindowURI.spec,
293 popupWindowFeatures: ev.popupWindowFeatures,
294 popupWindowName: ev.popupWindowName
295 };
297 let internals = {
298 requestingWindow: ev.requestingWindow,
299 requestingDocument: ev.requestingWindow.document,
300 };
302 this.popupData.push(obj);
303 this.popupDataInternal.push(internals);
304 this.updateBlockedPopups(true);
305 },
307 onPageShow: function(ev) {
308 if (this.popupData) {
309 let i = 0;
310 while (i < this.popupData.length) {
311 // Filter out irrelevant reports.
312 if (this.popupDataInternal[i].requestingWindow &&
313 (this.popupDataInternal[i].requestingWindow.document ==
314 this.popupDataInternal[i].requestingDocument)) {
315 i++;
316 } else {
317 this.popupData.splice(i, 1);
318 this.popupDataInternal.splice(i, 1);
319 }
320 }
321 if (this.popupData.length == 0) {
322 this.popupData = null;
323 this.popupDataInternal = null;
324 }
325 this.updateBlockedPopups(false);
326 }
327 },
329 onPageHide: function(ev) {
330 if (this.popupData) {
331 this.popupData = null;
332 this.popupDataInternal = null;
333 this.updateBlockedPopups(false);
334 }
335 },
337 updateBlockedPopups: function(freshPopup) {
338 sendAsyncMessage("PopupBlocking:UpdateBlockedPopups",
339 {blockedPopups: this.popupData, freshPopup: freshPopup});
340 },
341 };
342 PopupBlocking.init();