|
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/. */ |
|
5 |
|
6 let Cc = Components.classes; |
|
7 let Ci = Components.interfaces; |
|
8 let Cu = Components.utils; |
|
9 |
|
10 Cu.import("resource://gre/modules/Services.jsm"); |
|
11 |
|
12 var global = this; |
|
13 |
|
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; |
|
23 |
|
24 Cc["@mozilla.org/eventlistenerservice;1"] |
|
25 .getService(Ci.nsIEventListenerService) |
|
26 .addSystemEventListener(global, "mousedown", this, true); |
|
27 |
|
28 addMessageListener("Autoscroll:Stop", this); |
|
29 }, |
|
30 |
|
31 isAutoscrollBlocker: function(node) { |
|
32 let mmPaste = Services.prefs.getBoolPref("middlemouse.paste"); |
|
33 let mmScrollbarPosition = Services.prefs.getBoolPref("middlemouse.scrollbarPosition"); |
|
34 |
|
35 while (node) { |
|
36 if ((node instanceof content.HTMLAnchorElement || node instanceof content.HTMLAreaElement) && |
|
37 node.hasAttribute("href")) { |
|
38 return true; |
|
39 } |
|
40 |
|
41 if (mmPaste && (node instanceof content.HTMLInputElement || |
|
42 node instanceof content.HTMLTextAreaElement)) { |
|
43 return true; |
|
44 } |
|
45 |
|
46 if (node instanceof content.XULElement && mmScrollbarPosition |
|
47 && (node.localName == "scrollbar" || node.localName == "scrollcorner")) { |
|
48 return true; |
|
49 } |
|
50 |
|
51 node = node.parentNode; |
|
52 } |
|
53 return false; |
|
54 }, |
|
55 |
|
56 startScroll: function(event) { |
|
57 // this is a list of overflow property values that allow scrolling |
|
58 const scrollingAllowed = ['scroll', 'auto']; |
|
59 |
|
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 } |
|
71 |
|
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); |
|
84 |
|
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 } |
|
97 |
|
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 } |
|
109 |
|
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 } |
|
118 |
|
119 Cc["@mozilla.org/eventlistenerservice;1"] |
|
120 .getService(Ci.nsIEventListenerService) |
|
121 .addSystemEventListener(global, "mousemove", this, true); |
|
122 addEventListener("pagehide", this, true); |
|
123 |
|
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; |
|
132 |
|
133 content.mozRequestAnimationFrame(this); |
|
134 }, |
|
135 |
|
136 stopScroll: function() { |
|
137 if (this._scrollable) { |
|
138 this._scrollable = null; |
|
139 |
|
140 Cc["@mozilla.org/eventlistenerservice;1"] |
|
141 .getService(Ci.nsIEventListenerService) |
|
142 .removeSystemEventListener(global, "mousemove", this, true); |
|
143 removeEventListener("pagehide", this, true); |
|
144 } |
|
145 }, |
|
146 |
|
147 accelerate: function(curr, start) { |
|
148 const speed = 12; |
|
149 var val = (curr - start) / speed; |
|
150 |
|
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 }, |
|
157 |
|
158 roundToZero: function(num) { |
|
159 if (num > 0) |
|
160 return Math.floor(num); |
|
161 return Math.ceil(num); |
|
162 }, |
|
163 |
|
164 autoscrollLoop: function(timestamp) { |
|
165 if (!this._scrollable) { |
|
166 // Scrolling has been canceled |
|
167 return; |
|
168 } |
|
169 |
|
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; |
|
177 |
|
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 } |
|
194 |
|
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 }, |
|
203 |
|
204 sample: function(timestamp) { |
|
205 this.autoscrollLoop(timestamp); |
|
206 }, |
|
207 |
|
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 }, |
|
230 |
|
231 receiveMessage: function(msg) { |
|
232 switch (msg.name) { |
|
233 case "Autoscroll:Stop": { |
|
234 this.stopScroll(); |
|
235 break; |
|
236 } |
|
237 } |
|
238 }, |
|
239 }; |
|
240 ClickEventHandler.init(); |
|
241 |
|
242 let PopupBlocking = { |
|
243 popupData: null, |
|
244 popupDataInternal: null, |
|
245 |
|
246 init: function() { |
|
247 addEventListener("DOMPopupBlocked", this, true); |
|
248 addEventListener("pageshow", this, true); |
|
249 addEventListener("pagehide", this, true); |
|
250 |
|
251 addMessageListener("PopupBlocking:UnblockPopup", this); |
|
252 }, |
|
253 |
|
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; |
|
262 |
|
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 }, |
|
273 |
|
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 }, |
|
284 |
|
285 onPopupBlocked: function(ev) { |
|
286 if (!this.popupData) { |
|
287 this.popupData = new Array(); |
|
288 this.popupDataInternal = new Array(); |
|
289 } |
|
290 |
|
291 let obj = { |
|
292 popupWindowURI: ev.popupWindowURI.spec, |
|
293 popupWindowFeatures: ev.popupWindowFeatures, |
|
294 popupWindowName: ev.popupWindowName |
|
295 }; |
|
296 |
|
297 let internals = { |
|
298 requestingWindow: ev.requestingWindow, |
|
299 requestingDocument: ev.requestingWindow.document, |
|
300 }; |
|
301 |
|
302 this.popupData.push(obj); |
|
303 this.popupDataInternal.push(internals); |
|
304 this.updateBlockedPopups(true); |
|
305 }, |
|
306 |
|
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 }, |
|
328 |
|
329 onPageHide: function(ev) { |
|
330 if (this.popupData) { |
|
331 this.popupData = null; |
|
332 this.popupDataInternal = null; |
|
333 this.updateBlockedPopups(false); |
|
334 } |
|
335 }, |
|
336 |
|
337 updateBlockedPopups: function(freshPopup) { |
|
338 sendAsyncMessage("PopupBlocking:UpdateBlockedPopups", |
|
339 {blockedPopups: this.popupData, freshPopup: freshPopup}); |
|
340 }, |
|
341 }; |
|
342 PopupBlocking.init(); |