michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: let Cc = Components.classes; michael@0: let Ci = Components.interfaces; michael@0: michael@0: Components.utils.import("resource:///modules/ContentUtil.jsm"); michael@0: michael@0: let Util = { michael@0: /* michael@0: * General purpose utilities michael@0: */ michael@0: michael@0: getWindowUtils: function getWindowUtils(aWindow) { michael@0: return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: }, michael@0: michael@0: // Put the Mozilla networking code into a state that will kick the michael@0: // auto-connection process. michael@0: forceOnline: function forceOnline() { michael@0: Services.io.offline = false; michael@0: }, michael@0: michael@0: /* michael@0: * Timing utilties michael@0: */ michael@0: michael@0: // Executes aFunc after other events have been processed. michael@0: executeSoon: function executeSoon(aFunc) { michael@0: Services.tm.mainThread.dispatch({ michael@0: run: function() { michael@0: aFunc(); michael@0: } michael@0: }, Ci.nsIThread.DISPATCH_NORMAL); michael@0: }, michael@0: michael@0: /* michael@0: * Console printing utilities michael@0: */ michael@0: michael@0: // Like dump, but each arg is handled and there's an automatic newline michael@0: dumpLn: function dumpLn() { michael@0: for (let i = 0; i < arguments.length; i++) michael@0: dump(arguments[i] + " "); michael@0: dump("\n"); michael@0: }, michael@0: michael@0: /* michael@0: * Element utilities michael@0: */ michael@0: michael@0: transitionElementVisibility: function(aNodes, aVisible) { michael@0: // accept single node or a collection of nodes michael@0: aNodes = aNodes.length ? aNodes : [aNodes]; michael@0: let defd = Promise.defer(); michael@0: let pending = 0; michael@0: Array.forEach(aNodes, function(aNode) { michael@0: if (aVisible) { michael@0: aNode.hidden = false; michael@0: aNode.removeAttribute("fade"); // trigger transition to full opacity michael@0: } else { michael@0: aNode.setAttribute("fade", true); // trigger transition to 0 opacity michael@0: } michael@0: aNode.addEventListener("transitionend", function onTransitionEnd(aEvent){ michael@0: aNode.removeEventListener("transitionend", onTransitionEnd); michael@0: if (!aVisible) { michael@0: aNode.hidden = true; michael@0: } michael@0: pending--; michael@0: if (!pending){ michael@0: defd.resolve(true); michael@0: } michael@0: }, false); michael@0: pending++; michael@0: }); michael@0: return defd.promise; michael@0: }, michael@0: michael@0: isTextInput: function isTextInput(aElement) { michael@0: return ((aElement instanceof Ci.nsIDOMHTMLInputElement && michael@0: aElement.mozIsTextField(false)) || michael@0: aElement instanceof Ci.nsIDOMHTMLTextAreaElement); michael@0: }, michael@0: michael@0: /** michael@0: * Checks whether aElement's content can be edited either if it(or any of its michael@0: * parents) has "contenteditable" attribute set to "true" or aElement's michael@0: * ownerDocument is in design mode. michael@0: */ michael@0: isEditableContent: function isEditableContent(aElement) { michael@0: return !!aElement && (aElement.isContentEditable || michael@0: this.isOwnerDocumentInDesignMode(aElement)); michael@0: michael@0: }, michael@0: michael@0: isEditable: function isEditable(aElement) { michael@0: if (!aElement) { michael@0: return false; michael@0: } michael@0: michael@0: if (this.isTextInput(aElement) || this.isEditableContent(aElement)) { michael@0: return true; michael@0: } michael@0: michael@0: // If a body element is editable and the body is the child of an michael@0: // iframe or div we can assume this is an advanced HTML editor michael@0: if ((aElement instanceof Ci.nsIDOMHTMLIFrameElement || michael@0: aElement instanceof Ci.nsIDOMHTMLDivElement) && michael@0: aElement.contentDocument && michael@0: this.isEditableContent(aElement.contentDocument.body)) { michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Checks whether aElement's owner document has design mode turned on. michael@0: */ michael@0: isOwnerDocumentInDesignMode: function(aElement) { michael@0: return !!aElement && !!aElement.ownerDocument && michael@0: aElement.ownerDocument.designMode == "on"; michael@0: }, michael@0: michael@0: isMultilineInput: function isMultilineInput(aElement) { michael@0: return (aElement instanceof Ci.nsIDOMHTMLTextAreaElement); michael@0: }, michael@0: michael@0: isLink: function isLink(aElement) { michael@0: return ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) || michael@0: (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) || michael@0: aElement instanceof Ci.nsIDOMHTMLLinkElement || michael@0: aElement.getAttributeNS(kXLinkNamespace, "type") == "simple"); michael@0: }, michael@0: michael@0: isText: function isText(aElement) { michael@0: return (aElement instanceof Ci.nsIDOMHTMLParagraphElement || michael@0: aElement instanceof Ci.nsIDOMHTMLDivElement || michael@0: aElement instanceof Ci.nsIDOMHTMLLIElement || michael@0: aElement instanceof Ci.nsIDOMHTMLPreElement || michael@0: aElement instanceof Ci.nsIDOMHTMLHeadingElement || michael@0: aElement instanceof Ci.nsIDOMHTMLTableCellElement || michael@0: aElement instanceof Ci.nsIDOMHTMLBodyElement); michael@0: }, michael@0: michael@0: /* michael@0: * Rect and nsIDOMRect utilities michael@0: */ michael@0: michael@0: getCleanRect: function getCleanRect() { michael@0: return { michael@0: left: 0, top: 0, right: 0, bottom: 0 michael@0: }; michael@0: }, michael@0: michael@0: pointWithinRect: function pointWithinRect(aX, aY, aRect) { michael@0: return (aRect.left < aX && aRect.top < aY && michael@0: aRect.right > aX && aRect.bottom > aY); michael@0: }, michael@0: michael@0: pointWithinDOMRect: function pointWithinDOMRect(aX, aY, aRect) { michael@0: if (!aRect.width || !aRect.height) michael@0: return false; michael@0: return this.pointWithinRect(aX, aY, aRect); michael@0: }, michael@0: michael@0: isEmptyDOMRect: function isEmptyDOMRect(aRect) { michael@0: if ((aRect.bottom - aRect.top) <= 0 && michael@0: (aRect.right - aRect.left) <= 0) michael@0: return true; michael@0: return false; michael@0: }, michael@0: michael@0: // Dumps the details of a dom rect to the console michael@0: dumpDOMRect: function dumpDOMRect(aMsg, aRect) { michael@0: try { michael@0: Util.dumpLn(aMsg, michael@0: "left:" + Math.round(aRect.left) + ",", michael@0: "top:" + Math.round(aRect.top) + ",", michael@0: "right:" + Math.round(aRect.right) + ",", michael@0: "bottom:" + Math.round(aRect.bottom) + ",", michael@0: "width:" + Math.round(aRect.right - aRect.left) + ",", michael@0: "height:" + Math.round(aRect.bottom - aRect.top) ); michael@0: } catch (ex) { michael@0: Util.dumpLn("dumpDOMRect:", ex.message); michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * DownloadUtils.convertByteUnits returns [size, localized-unit-string] michael@0: * so they are joined for a single download size string. michael@0: */ michael@0: getDownloadSize: function dv__getDownloadSize (aSize) { michael@0: let [size, units] = DownloadUtils.convertByteUnits(aSize); michael@0: if (aSize > 0) michael@0: return size + units; michael@0: else michael@0: return Strings.browser.GetStringFromName("downloadsUnknownSize"); michael@0: }, michael@0: michael@0: /* michael@0: * URIs and schemes michael@0: */ michael@0: michael@0: makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) { michael@0: return Services.io.newURI(aURL, aOriginCharset, aBaseURI); michael@0: }, michael@0: michael@0: makeURLAbsolute: function makeURLAbsolute(base, url) { michael@0: // Note: makeURI() will throw if url is not a valid URI michael@0: return this.makeURI(url, null, this.makeURI(base)).spec; michael@0: }, michael@0: michael@0: isLocalScheme: function isLocalScheme(aURL) { michael@0: return ((aURL.indexOf("about:") == 0 && michael@0: aURL != "about:blank" && michael@0: aURL != "about:empty" && michael@0: aURL != "about:start") || michael@0: aURL.indexOf("chrome:") == 0); michael@0: }, michael@0: michael@0: // Don't display anything in the urlbar for these special URIs. michael@0: isURLEmpty: function isURLEmpty(aURL) { michael@0: return (!aURL || michael@0: aURL == "about:blank" || michael@0: aURL == "about:empty" || michael@0: aURL == "about:home" || michael@0: aURL == "about:newtab" || michael@0: aURL.startsWith("about:newtab")); michael@0: }, michael@0: michael@0: // Title to use for emptyURL tabs. michael@0: getEmptyURLTabTitle: function getEmptyURLTabTitle() { michael@0: let browserStrings = Services.strings.createBundle("chrome://browser/locale/browser.properties"); michael@0: michael@0: return browserStrings.GetStringFromName("tabs.emptyTabTitle"); michael@0: }, michael@0: michael@0: // Don't remember these pages in the session store. michael@0: isURLMemorable: function isURLMemorable(aURL) { michael@0: return !(aURL == "about:blank" || michael@0: aURL == "about:empty" || michael@0: aURL == "about:start"); michael@0: }, michael@0: michael@0: /* michael@0: * Math utilities michael@0: */ michael@0: michael@0: clamp: function(num, min, max) { michael@0: return Math.max(min, Math.min(max, num)); michael@0: }, michael@0: michael@0: /* michael@0: * Screen and layout utilities michael@0: */ michael@0: michael@0: /* michael@0: * translateToTopLevelWindow - Given an element potentially within michael@0: * a subframe, calculate the offsets up to the top level browser. michael@0: */ michael@0: translateToTopLevelWindow: function translateToTopLevelWindow(aElement) { michael@0: let offsetX = 0; michael@0: let offsetY = 0; michael@0: let element = aElement; michael@0: while (element && michael@0: element.ownerDocument && michael@0: element.ownerDocument.defaultView != content) { michael@0: element = element.ownerDocument.defaultView.frameElement; michael@0: let rect = element.getBoundingClientRect(); michael@0: offsetX += rect.left; michael@0: offsetY += rect.top; michael@0: } michael@0: let win = null; michael@0: if (element == aElement) michael@0: win = content; michael@0: else michael@0: win = element.contentDocument.defaultView; michael@0: return { targetWindow: win, offsetX: offsetX, offsetY: offsetY }; michael@0: }, michael@0: michael@0: get displayDPI() { michael@0: delete this.displayDPI; michael@0: return this.displayDPI = this.getWindowUtils(window).displayDPI; michael@0: }, michael@0: michael@0: /* michael@0: * aViewHeight - the height of the viewable area in the browser michael@0: * aRect - a bounding rectangle of a selection or element. michael@0: * michael@0: * return - number of pixels for the browser to be shifted up by such michael@0: * that aRect is centered vertically within aViewHeight. michael@0: */ michael@0: centerElementInView: function centerElementInView(aViewHeight, aRect) { michael@0: // If the bottom of the target bounds is higher than the new height, michael@0: // there's no need to adjust. It will be above the keyboard. michael@0: if (aRect.bottom <= aViewHeight) { michael@0: return 0; michael@0: } michael@0: michael@0: // height of the target element michael@0: let targetHeight = aRect.bottom - aRect.top; michael@0: // height of the browser view. michael@0: let viewBottom = content.innerHeight; michael@0: michael@0: // If the target is shorter than the new content height, we can go ahead michael@0: // and center it. michael@0: if (targetHeight <= aViewHeight) { michael@0: // Try to center the element vertically in the new content area, but michael@0: // don't position such that the bottom of the browser view moves above michael@0: // the top of the chrome. We purposely do not resize the browser window michael@0: // by making it taller when trying to center elements that are near the michael@0: // lower bounds. This would trigger reflow which can cause content to michael@0: // shift around. michael@0: let splitMargin = Math.round((aViewHeight - targetHeight) * .5); michael@0: let distanceToPageBounds = viewBottom - aRect.bottom; michael@0: let distanceFromChromeTop = aRect.bottom - aViewHeight; michael@0: let distanceToCenter = michael@0: distanceFromChromeTop + Math.min(distanceToPageBounds, splitMargin); michael@0: return distanceToCenter; michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * Local system utilities michael@0: */ michael@0: michael@0: copyImageToClipboard: function Util_copyImageToClipboard(aImageLoadingContent) { michael@0: let image = aImageLoadingContent.QueryInterface(Ci.nsIImageLoadingContent); michael@0: if (!image) { michael@0: Util.dumpLn("copyImageToClipboard error: image is not an nsIImageLoadingContent"); michael@0: return; michael@0: } michael@0: try { michael@0: let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); michael@0: xferable.init(null); michael@0: let imgRequest = aImageLoadingContent.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); michael@0: let mimeType = imgRequest.mimeType; michael@0: let imgContainer = imgRequest.image; michael@0: let imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(Ci.nsISupportsInterfacePointer); michael@0: imgPtr.data = imgContainer; michael@0: xferable.setTransferData(mimeType, imgPtr, null); michael@0: let clip = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); michael@0: clip.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard); michael@0: } catch (e) { michael@0: Util.dumpLn(e.message); michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: michael@0: /* michael@0: * Timeout michael@0: * michael@0: * Helper class to nsITimer that adds a little more pizazz. Callback can be an michael@0: * object with a notify method or a function. michael@0: */ michael@0: Util.Timeout = function(aCallback) { michael@0: this._callback = aCallback; michael@0: this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this._type = null; michael@0: }; michael@0: michael@0: Util.Timeout.prototype = { michael@0: // Timer callback. Don't call this manually. michael@0: notify: function notify() { michael@0: if (this._type == this._timer.TYPE_ONE_SHOT) michael@0: this._type = null; michael@0: michael@0: if (this._callback.notify) michael@0: this._callback.notify(); michael@0: else michael@0: this._callback.apply(null); michael@0: }, michael@0: michael@0: // Helper function for once and interval. michael@0: _start: function _start(aDelay, aType, aCallback) { michael@0: if (aCallback) michael@0: this._callback = aCallback; michael@0: this.clear(); michael@0: this._timer.initWithCallback(this, aDelay, aType); michael@0: this._type = aType; michael@0: return this; michael@0: }, michael@0: michael@0: // Do the callback once. Cancels other timeouts on this object. michael@0: once: function once(aDelay, aCallback) { michael@0: return this._start(aDelay, this._timer.TYPE_ONE_SHOT, aCallback); michael@0: }, michael@0: michael@0: // Do the callback every aDelay msecs. Cancels other timeouts on this object. michael@0: interval: function interval(aDelay, aCallback) { michael@0: return this._start(aDelay, this._timer.TYPE_REPEATING_SLACK, aCallback); michael@0: }, michael@0: michael@0: // Clear any pending timeouts. michael@0: clear: function clear() { michael@0: if (this.isPending()) { michael@0: this._timer.cancel(); michael@0: this._type = null; michael@0: } michael@0: return this; michael@0: }, michael@0: michael@0: // If there is a pending timeout, call it and cancel the timeout. michael@0: flush: function flush() { michael@0: if (this.isPending()) { michael@0: this.notify(); michael@0: this.clear(); michael@0: } michael@0: return this; michael@0: }, michael@0: michael@0: // Return true if we are waiting for a callback. michael@0: isPending: function isPending() { michael@0: return this._type !== null; michael@0: } michael@0: }; michael@0: michael@0: // Mixin the ContentUtil module exports michael@0: { michael@0: for (let name in ContentUtil) { michael@0: let copy = ContentUtil[name]; michael@0: if (copy !== undefined) michael@0: Util[name] = copy; michael@0: } michael@0: } michael@0: michael@0: this.Util = Util;