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