|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 let Cc = Components.classes; |
|
6 let Ci = Components.interfaces; |
|
7 |
|
8 Components.utils.import("resource:///modules/ContentUtil.jsm"); |
|
9 |
|
10 let Util = { |
|
11 /* |
|
12 * General purpose utilities |
|
13 */ |
|
14 |
|
15 getWindowUtils: function getWindowUtils(aWindow) { |
|
16 return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); |
|
17 }, |
|
18 |
|
19 // Put the Mozilla networking code into a state that will kick the |
|
20 // auto-connection process. |
|
21 forceOnline: function forceOnline() { |
|
22 Services.io.offline = false; |
|
23 }, |
|
24 |
|
25 /* |
|
26 * Timing utilties |
|
27 */ |
|
28 |
|
29 // Executes aFunc after other events have been processed. |
|
30 executeSoon: function executeSoon(aFunc) { |
|
31 Services.tm.mainThread.dispatch({ |
|
32 run: function() { |
|
33 aFunc(); |
|
34 } |
|
35 }, Ci.nsIThread.DISPATCH_NORMAL); |
|
36 }, |
|
37 |
|
38 /* |
|
39 * Console printing utilities |
|
40 */ |
|
41 |
|
42 // Like dump, but each arg is handled and there's an automatic newline |
|
43 dumpLn: function dumpLn() { |
|
44 for (let i = 0; i < arguments.length; i++) |
|
45 dump(arguments[i] + " "); |
|
46 dump("\n"); |
|
47 }, |
|
48 |
|
49 /* |
|
50 * Element utilities |
|
51 */ |
|
52 |
|
53 transitionElementVisibility: function(aNodes, aVisible) { |
|
54 // accept single node or a collection of nodes |
|
55 aNodes = aNodes.length ? aNodes : [aNodes]; |
|
56 let defd = Promise.defer(); |
|
57 let pending = 0; |
|
58 Array.forEach(aNodes, function(aNode) { |
|
59 if (aVisible) { |
|
60 aNode.hidden = false; |
|
61 aNode.removeAttribute("fade"); // trigger transition to full opacity |
|
62 } else { |
|
63 aNode.setAttribute("fade", true); // trigger transition to 0 opacity |
|
64 } |
|
65 aNode.addEventListener("transitionend", function onTransitionEnd(aEvent){ |
|
66 aNode.removeEventListener("transitionend", onTransitionEnd); |
|
67 if (!aVisible) { |
|
68 aNode.hidden = true; |
|
69 } |
|
70 pending--; |
|
71 if (!pending){ |
|
72 defd.resolve(true); |
|
73 } |
|
74 }, false); |
|
75 pending++; |
|
76 }); |
|
77 return defd.promise; |
|
78 }, |
|
79 |
|
80 isTextInput: function isTextInput(aElement) { |
|
81 return ((aElement instanceof Ci.nsIDOMHTMLInputElement && |
|
82 aElement.mozIsTextField(false)) || |
|
83 aElement instanceof Ci.nsIDOMHTMLTextAreaElement); |
|
84 }, |
|
85 |
|
86 /** |
|
87 * Checks whether aElement's content can be edited either if it(or any of its |
|
88 * parents) has "contenteditable" attribute set to "true" or aElement's |
|
89 * ownerDocument is in design mode. |
|
90 */ |
|
91 isEditableContent: function isEditableContent(aElement) { |
|
92 return !!aElement && (aElement.isContentEditable || |
|
93 this.isOwnerDocumentInDesignMode(aElement)); |
|
94 |
|
95 }, |
|
96 |
|
97 isEditable: function isEditable(aElement) { |
|
98 if (!aElement) { |
|
99 return false; |
|
100 } |
|
101 |
|
102 if (this.isTextInput(aElement) || this.isEditableContent(aElement)) { |
|
103 return true; |
|
104 } |
|
105 |
|
106 // If a body element is editable and the body is the child of an |
|
107 // iframe or div we can assume this is an advanced HTML editor |
|
108 if ((aElement instanceof Ci.nsIDOMHTMLIFrameElement || |
|
109 aElement instanceof Ci.nsIDOMHTMLDivElement) && |
|
110 aElement.contentDocument && |
|
111 this.isEditableContent(aElement.contentDocument.body)) { |
|
112 return true; |
|
113 } |
|
114 |
|
115 return false; |
|
116 }, |
|
117 |
|
118 /** |
|
119 * Checks whether aElement's owner document has design mode turned on. |
|
120 */ |
|
121 isOwnerDocumentInDesignMode: function(aElement) { |
|
122 return !!aElement && !!aElement.ownerDocument && |
|
123 aElement.ownerDocument.designMode == "on"; |
|
124 }, |
|
125 |
|
126 isMultilineInput: function isMultilineInput(aElement) { |
|
127 return (aElement instanceof Ci.nsIDOMHTMLTextAreaElement); |
|
128 }, |
|
129 |
|
130 isLink: function isLink(aElement) { |
|
131 return ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) || |
|
132 (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) || |
|
133 aElement instanceof Ci.nsIDOMHTMLLinkElement || |
|
134 aElement.getAttributeNS(kXLinkNamespace, "type") == "simple"); |
|
135 }, |
|
136 |
|
137 isText: function isText(aElement) { |
|
138 return (aElement instanceof Ci.nsIDOMHTMLParagraphElement || |
|
139 aElement instanceof Ci.nsIDOMHTMLDivElement || |
|
140 aElement instanceof Ci.nsIDOMHTMLLIElement || |
|
141 aElement instanceof Ci.nsIDOMHTMLPreElement || |
|
142 aElement instanceof Ci.nsIDOMHTMLHeadingElement || |
|
143 aElement instanceof Ci.nsIDOMHTMLTableCellElement || |
|
144 aElement instanceof Ci.nsIDOMHTMLBodyElement); |
|
145 }, |
|
146 |
|
147 /* |
|
148 * Rect and nsIDOMRect utilities |
|
149 */ |
|
150 |
|
151 getCleanRect: function getCleanRect() { |
|
152 return { |
|
153 left: 0, top: 0, right: 0, bottom: 0 |
|
154 }; |
|
155 }, |
|
156 |
|
157 pointWithinRect: function pointWithinRect(aX, aY, aRect) { |
|
158 return (aRect.left < aX && aRect.top < aY && |
|
159 aRect.right > aX && aRect.bottom > aY); |
|
160 }, |
|
161 |
|
162 pointWithinDOMRect: function pointWithinDOMRect(aX, aY, aRect) { |
|
163 if (!aRect.width || !aRect.height) |
|
164 return false; |
|
165 return this.pointWithinRect(aX, aY, aRect); |
|
166 }, |
|
167 |
|
168 isEmptyDOMRect: function isEmptyDOMRect(aRect) { |
|
169 if ((aRect.bottom - aRect.top) <= 0 && |
|
170 (aRect.right - aRect.left) <= 0) |
|
171 return true; |
|
172 return false; |
|
173 }, |
|
174 |
|
175 // Dumps the details of a dom rect to the console |
|
176 dumpDOMRect: function dumpDOMRect(aMsg, aRect) { |
|
177 try { |
|
178 Util.dumpLn(aMsg, |
|
179 "left:" + Math.round(aRect.left) + ",", |
|
180 "top:" + Math.round(aRect.top) + ",", |
|
181 "right:" + Math.round(aRect.right) + ",", |
|
182 "bottom:" + Math.round(aRect.bottom) + ",", |
|
183 "width:" + Math.round(aRect.right - aRect.left) + ",", |
|
184 "height:" + Math.round(aRect.bottom - aRect.top) ); |
|
185 } catch (ex) { |
|
186 Util.dumpLn("dumpDOMRect:", ex.message); |
|
187 } |
|
188 }, |
|
189 |
|
190 /* |
|
191 * DownloadUtils.convertByteUnits returns [size, localized-unit-string] |
|
192 * so they are joined for a single download size string. |
|
193 */ |
|
194 getDownloadSize: function dv__getDownloadSize (aSize) { |
|
195 let [size, units] = DownloadUtils.convertByteUnits(aSize); |
|
196 if (aSize > 0) |
|
197 return size + units; |
|
198 else |
|
199 return Strings.browser.GetStringFromName("downloadsUnknownSize"); |
|
200 }, |
|
201 |
|
202 /* |
|
203 * URIs and schemes |
|
204 */ |
|
205 |
|
206 makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) { |
|
207 return Services.io.newURI(aURL, aOriginCharset, aBaseURI); |
|
208 }, |
|
209 |
|
210 makeURLAbsolute: function makeURLAbsolute(base, url) { |
|
211 // Note: makeURI() will throw if url is not a valid URI |
|
212 return this.makeURI(url, null, this.makeURI(base)).spec; |
|
213 }, |
|
214 |
|
215 isLocalScheme: function isLocalScheme(aURL) { |
|
216 return ((aURL.indexOf("about:") == 0 && |
|
217 aURL != "about:blank" && |
|
218 aURL != "about:empty" && |
|
219 aURL != "about:start") || |
|
220 aURL.indexOf("chrome:") == 0); |
|
221 }, |
|
222 |
|
223 // Don't display anything in the urlbar for these special URIs. |
|
224 isURLEmpty: function isURLEmpty(aURL) { |
|
225 return (!aURL || |
|
226 aURL == "about:blank" || |
|
227 aURL == "about:empty" || |
|
228 aURL == "about:home" || |
|
229 aURL == "about:newtab" || |
|
230 aURL.startsWith("about:newtab")); |
|
231 }, |
|
232 |
|
233 // Title to use for emptyURL tabs. |
|
234 getEmptyURLTabTitle: function getEmptyURLTabTitle() { |
|
235 let browserStrings = Services.strings.createBundle("chrome://browser/locale/browser.properties"); |
|
236 |
|
237 return browserStrings.GetStringFromName("tabs.emptyTabTitle"); |
|
238 }, |
|
239 |
|
240 // Don't remember these pages in the session store. |
|
241 isURLMemorable: function isURLMemorable(aURL) { |
|
242 return !(aURL == "about:blank" || |
|
243 aURL == "about:empty" || |
|
244 aURL == "about:start"); |
|
245 }, |
|
246 |
|
247 /* |
|
248 * Math utilities |
|
249 */ |
|
250 |
|
251 clamp: function(num, min, max) { |
|
252 return Math.max(min, Math.min(max, num)); |
|
253 }, |
|
254 |
|
255 /* |
|
256 * Screen and layout utilities |
|
257 */ |
|
258 |
|
259 /* |
|
260 * translateToTopLevelWindow - Given an element potentially within |
|
261 * a subframe, calculate the offsets up to the top level browser. |
|
262 */ |
|
263 translateToTopLevelWindow: function translateToTopLevelWindow(aElement) { |
|
264 let offsetX = 0; |
|
265 let offsetY = 0; |
|
266 let element = aElement; |
|
267 while (element && |
|
268 element.ownerDocument && |
|
269 element.ownerDocument.defaultView != content) { |
|
270 element = element.ownerDocument.defaultView.frameElement; |
|
271 let rect = element.getBoundingClientRect(); |
|
272 offsetX += rect.left; |
|
273 offsetY += rect.top; |
|
274 } |
|
275 let win = null; |
|
276 if (element == aElement) |
|
277 win = content; |
|
278 else |
|
279 win = element.contentDocument.defaultView; |
|
280 return { targetWindow: win, offsetX: offsetX, offsetY: offsetY }; |
|
281 }, |
|
282 |
|
283 get displayDPI() { |
|
284 delete this.displayDPI; |
|
285 return this.displayDPI = this.getWindowUtils(window).displayDPI; |
|
286 }, |
|
287 |
|
288 /* |
|
289 * aViewHeight - the height of the viewable area in the browser |
|
290 * aRect - a bounding rectangle of a selection or element. |
|
291 * |
|
292 * return - number of pixels for the browser to be shifted up by such |
|
293 * that aRect is centered vertically within aViewHeight. |
|
294 */ |
|
295 centerElementInView: function centerElementInView(aViewHeight, aRect) { |
|
296 // If the bottom of the target bounds is higher than the new height, |
|
297 // there's no need to adjust. It will be above the keyboard. |
|
298 if (aRect.bottom <= aViewHeight) { |
|
299 return 0; |
|
300 } |
|
301 |
|
302 // height of the target element |
|
303 let targetHeight = aRect.bottom - aRect.top; |
|
304 // height of the browser view. |
|
305 let viewBottom = content.innerHeight; |
|
306 |
|
307 // If the target is shorter than the new content height, we can go ahead |
|
308 // and center it. |
|
309 if (targetHeight <= aViewHeight) { |
|
310 // Try to center the element vertically in the new content area, but |
|
311 // don't position such that the bottom of the browser view moves above |
|
312 // the top of the chrome. We purposely do not resize the browser window |
|
313 // by making it taller when trying to center elements that are near the |
|
314 // lower bounds. This would trigger reflow which can cause content to |
|
315 // shift around. |
|
316 let splitMargin = Math.round((aViewHeight - targetHeight) * .5); |
|
317 let distanceToPageBounds = viewBottom - aRect.bottom; |
|
318 let distanceFromChromeTop = aRect.bottom - aViewHeight; |
|
319 let distanceToCenter = |
|
320 distanceFromChromeTop + Math.min(distanceToPageBounds, splitMargin); |
|
321 return distanceToCenter; |
|
322 } |
|
323 }, |
|
324 |
|
325 /* |
|
326 * Local system utilities |
|
327 */ |
|
328 |
|
329 copyImageToClipboard: function Util_copyImageToClipboard(aImageLoadingContent) { |
|
330 let image = aImageLoadingContent.QueryInterface(Ci.nsIImageLoadingContent); |
|
331 if (!image) { |
|
332 Util.dumpLn("copyImageToClipboard error: image is not an nsIImageLoadingContent"); |
|
333 return; |
|
334 } |
|
335 try { |
|
336 let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); |
|
337 xferable.init(null); |
|
338 let imgRequest = aImageLoadingContent.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); |
|
339 let mimeType = imgRequest.mimeType; |
|
340 let imgContainer = imgRequest.image; |
|
341 let imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(Ci.nsISupportsInterfacePointer); |
|
342 imgPtr.data = imgContainer; |
|
343 xferable.setTransferData(mimeType, imgPtr, null); |
|
344 let clip = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); |
|
345 clip.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard); |
|
346 } catch (e) { |
|
347 Util.dumpLn(e.message); |
|
348 } |
|
349 }, |
|
350 }; |
|
351 |
|
352 |
|
353 /* |
|
354 * Timeout |
|
355 * |
|
356 * Helper class to nsITimer that adds a little more pizazz. Callback can be an |
|
357 * object with a notify method or a function. |
|
358 */ |
|
359 Util.Timeout = function(aCallback) { |
|
360 this._callback = aCallback; |
|
361 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
|
362 this._type = null; |
|
363 }; |
|
364 |
|
365 Util.Timeout.prototype = { |
|
366 // Timer callback. Don't call this manually. |
|
367 notify: function notify() { |
|
368 if (this._type == this._timer.TYPE_ONE_SHOT) |
|
369 this._type = null; |
|
370 |
|
371 if (this._callback.notify) |
|
372 this._callback.notify(); |
|
373 else |
|
374 this._callback.apply(null); |
|
375 }, |
|
376 |
|
377 // Helper function for once and interval. |
|
378 _start: function _start(aDelay, aType, aCallback) { |
|
379 if (aCallback) |
|
380 this._callback = aCallback; |
|
381 this.clear(); |
|
382 this._timer.initWithCallback(this, aDelay, aType); |
|
383 this._type = aType; |
|
384 return this; |
|
385 }, |
|
386 |
|
387 // Do the callback once. Cancels other timeouts on this object. |
|
388 once: function once(aDelay, aCallback) { |
|
389 return this._start(aDelay, this._timer.TYPE_ONE_SHOT, aCallback); |
|
390 }, |
|
391 |
|
392 // Do the callback every aDelay msecs. Cancels other timeouts on this object. |
|
393 interval: function interval(aDelay, aCallback) { |
|
394 return this._start(aDelay, this._timer.TYPE_REPEATING_SLACK, aCallback); |
|
395 }, |
|
396 |
|
397 // Clear any pending timeouts. |
|
398 clear: function clear() { |
|
399 if (this.isPending()) { |
|
400 this._timer.cancel(); |
|
401 this._type = null; |
|
402 } |
|
403 return this; |
|
404 }, |
|
405 |
|
406 // If there is a pending timeout, call it and cancel the timeout. |
|
407 flush: function flush() { |
|
408 if (this.isPending()) { |
|
409 this.notify(); |
|
410 this.clear(); |
|
411 } |
|
412 return this; |
|
413 }, |
|
414 |
|
415 // Return true if we are waiting for a callback. |
|
416 isPending: function isPending() { |
|
417 return this._type !== null; |
|
418 } |
|
419 }; |
|
420 |
|
421 // Mixin the ContentUtil module exports |
|
422 { |
|
423 for (let name in ContentUtil) { |
|
424 let copy = ContentUtil[name]; |
|
425 if (copy !== undefined) |
|
426 Util[name] = copy; |
|
427 } |
|
428 } |
|
429 |
|
430 this.Util = Util; |