Wed, 31 Dec 2014 07:53:36 +0100
Correct small whitespace inconsistency, lost while renaming variables.
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 | var EXPORTED_SYMBOLS = ["init", "map"]; |
michael@0 | 6 | |
michael@0 | 7 | const Cc = Components.classes; |
michael@0 | 8 | const Ci = Components.interfaces; |
michael@0 | 9 | const Cu = Components.utils; |
michael@0 | 10 | |
michael@0 | 11 | // imports |
michael@0 | 12 | var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); |
michael@0 | 13 | |
michael@0 | 14 | var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); |
michael@0 | 15 | |
michael@0 | 16 | /** |
michael@0 | 17 | * The window map is used to store information about the current state of |
michael@0 | 18 | * open windows, e.g. loaded state |
michael@0 | 19 | */ |
michael@0 | 20 | var map = { |
michael@0 | 21 | _windows : { }, |
michael@0 | 22 | |
michael@0 | 23 | /** |
michael@0 | 24 | * Check if a given window id is contained in the map of windows |
michael@0 | 25 | * |
michael@0 | 26 | * @param {Number} aWindowId |
michael@0 | 27 | * Outer ID of the window to check. |
michael@0 | 28 | * @returns {Boolean} True if the window is part of the map, otherwise false. |
michael@0 | 29 | */ |
michael@0 | 30 | contains : function (aWindowId) { |
michael@0 | 31 | return (aWindowId in this._windows); |
michael@0 | 32 | }, |
michael@0 | 33 | |
michael@0 | 34 | /** |
michael@0 | 35 | * Retrieve the value of the specified window's property. |
michael@0 | 36 | * |
michael@0 | 37 | * @param {Number} aWindowId |
michael@0 | 38 | * Outer ID of the window to check. |
michael@0 | 39 | * @param {String} aProperty |
michael@0 | 40 | * Property to retrieve the value from |
michael@0 | 41 | * @return {Object} Value of the window's property |
michael@0 | 42 | */ |
michael@0 | 43 | getValue : function (aWindowId, aProperty) { |
michael@0 | 44 | if (!this.contains(aWindowId)) { |
michael@0 | 45 | return undefined; |
michael@0 | 46 | } else { |
michael@0 | 47 | var win = this._windows[aWindowId]; |
michael@0 | 48 | |
michael@0 | 49 | return (aProperty in win) ? win[aProperty] |
michael@0 | 50 | : undefined; |
michael@0 | 51 | } |
michael@0 | 52 | }, |
michael@0 | 53 | |
michael@0 | 54 | /** |
michael@0 | 55 | * Remove the entry for a given window |
michael@0 | 56 | * |
michael@0 | 57 | * @param {Number} aWindowId |
michael@0 | 58 | * Outer ID of the window to check. |
michael@0 | 59 | */ |
michael@0 | 60 | remove : function (aWindowId) { |
michael@0 | 61 | if (this.contains(aWindowId)) { |
michael@0 | 62 | delete this._windows[aWindowId]; |
michael@0 | 63 | } |
michael@0 | 64 | |
michael@0 | 65 | // dump("* current map: " + JSON.stringify(this._windows) + "\n"); |
michael@0 | 66 | }, |
michael@0 | 67 | |
michael@0 | 68 | /** |
michael@0 | 69 | * Update the property value of a given window |
michael@0 | 70 | * |
michael@0 | 71 | * @param {Number} aWindowId |
michael@0 | 72 | * Outer ID of the window to check. |
michael@0 | 73 | * @param {String} aProperty |
michael@0 | 74 | * Property to update the value for |
michael@0 | 75 | * @param {Object} |
michael@0 | 76 | * Value to set |
michael@0 | 77 | */ |
michael@0 | 78 | update : function (aWindowId, aProperty, aValue) { |
michael@0 | 79 | if (!this.contains(aWindowId)) { |
michael@0 | 80 | this._windows[aWindowId] = { }; |
michael@0 | 81 | } |
michael@0 | 82 | |
michael@0 | 83 | this._windows[aWindowId][aProperty] = aValue; |
michael@0 | 84 | // dump("* current map: " + JSON.stringify(this._windows) + "\n"); |
michael@0 | 85 | }, |
michael@0 | 86 | |
michael@0 | 87 | /** |
michael@0 | 88 | * Update the internal loaded state of the given content window. To identify |
michael@0 | 89 | * an active (re)load action we make use of an uuid. |
michael@0 | 90 | * |
michael@0 | 91 | * @param {Window} aId - The outer id of the window to update |
michael@0 | 92 | * @param {Boolean} aIsLoaded - Has the window been loaded |
michael@0 | 93 | */ |
michael@0 | 94 | updatePageLoadStatus : function (aId, aIsLoaded) { |
michael@0 | 95 | this.update(aId, "loaded", aIsLoaded); |
michael@0 | 96 | |
michael@0 | 97 | var uuid = this.getValue(aId, "id_load_in_transition"); |
michael@0 | 98 | |
michael@0 | 99 | // If no uuid has been set yet or when the page gets unloaded create a new id |
michael@0 | 100 | if (!uuid || !aIsLoaded) { |
michael@0 | 101 | uuid = uuidgen.generateUUID(); |
michael@0 | 102 | this.update(aId, "id_load_in_transition", uuid); |
michael@0 | 103 | } |
michael@0 | 104 | |
michael@0 | 105 | // dump("*** Page status updated: id=" + aId + ", loaded=" + aIsLoaded + ", uuid=" + uuid + "\n"); |
michael@0 | 106 | }, |
michael@0 | 107 | |
michael@0 | 108 | /** |
michael@0 | 109 | * This method only applies to content windows, where we have to check if it has |
michael@0 | 110 | * been successfully loaded or reloaded. An uuid allows us to wait for the next |
michael@0 | 111 | * load action triggered by e.g. controller.open(). |
michael@0 | 112 | * |
michael@0 | 113 | * @param {Window} aId - The outer id of the content window to check |
michael@0 | 114 | * |
michael@0 | 115 | * @returns {Boolean} True if the content window has been loaded |
michael@0 | 116 | */ |
michael@0 | 117 | hasPageLoaded : function (aId) { |
michael@0 | 118 | var load_current = this.getValue(aId, "id_load_in_transition"); |
michael@0 | 119 | var load_handled = this.getValue(aId, "id_load_handled"); |
michael@0 | 120 | |
michael@0 | 121 | var isLoaded = this.contains(aId) && this.getValue(aId, "loaded") && |
michael@0 | 122 | (load_current !== load_handled); |
michael@0 | 123 | |
michael@0 | 124 | if (isLoaded) { |
michael@0 | 125 | // Backup the current uuid so we can check later if another page load happened. |
michael@0 | 126 | this.update(aId, "id_load_handled", load_current); |
michael@0 | 127 | } |
michael@0 | 128 | |
michael@0 | 129 | // dump("** Page has been finished loading: id=" + aId + ", status=" + isLoaded + ", uuid=" + load_current + "\n"); |
michael@0 | 130 | |
michael@0 | 131 | return isLoaded; |
michael@0 | 132 | } |
michael@0 | 133 | }; |
michael@0 | 134 | |
michael@0 | 135 | |
michael@0 | 136 | // Observer when a new top-level window is ready |
michael@0 | 137 | var windowReadyObserver = { |
michael@0 | 138 | observe: function (aSubject, aTopic, aData) { |
michael@0 | 139 | // Not in all cases we get a ChromeWindow. So ensure we really operate |
michael@0 | 140 | // on such an instance. Otherwise load events will not be handled. |
michael@0 | 141 | var win = utils.getChromeWindow(aSubject); |
michael@0 | 142 | |
michael@0 | 143 | // var id = utils.getWindowId(win); |
michael@0 | 144 | // dump("*** 'toplevel-window-ready' observer notification: id=" + id + "\n"); |
michael@0 | 145 | attachEventListeners(win); |
michael@0 | 146 | } |
michael@0 | 147 | }; |
michael@0 | 148 | |
michael@0 | 149 | |
michael@0 | 150 | // Observer when a top-level window is closed |
michael@0 | 151 | var windowCloseObserver = { |
michael@0 | 152 | observe: function (aSubject, aTopic, aData) { |
michael@0 | 153 | var id = utils.getWindowId(aSubject); |
michael@0 | 154 | // dump("*** 'outer-window-destroyed' observer notification: id=" + id + "\n"); |
michael@0 | 155 | |
michael@0 | 156 | map.remove(id); |
michael@0 | 157 | } |
michael@0 | 158 | }; |
michael@0 | 159 | |
michael@0 | 160 | // Bug 915554 |
michael@0 | 161 | // Support for the old Private Browsing Mode (eg. ESR17) |
michael@0 | 162 | // TODO: remove once ESR17 is no longer supported |
michael@0 | 163 | var enterLeavePrivateBrowsingObserver = { |
michael@0 | 164 | observe: function (aSubject, aTopic, aData) { |
michael@0 | 165 | handleAttachEventListeners(); |
michael@0 | 166 | } |
michael@0 | 167 | }; |
michael@0 | 168 | |
michael@0 | 169 | /** |
michael@0 | 170 | * Attach event listeners |
michael@0 | 171 | * |
michael@0 | 172 | * @param {ChromeWindow} aWindow |
michael@0 | 173 | * Window to attach listeners on. |
michael@0 | 174 | */ |
michael@0 | 175 | function attachEventListeners(aWindow) { |
michael@0 | 176 | // These are the event handlers |
michael@0 | 177 | var pageShowHandler = function (aEvent) { |
michael@0 | 178 | var doc = aEvent.originalTarget; |
michael@0 | 179 | |
michael@0 | 180 | // Only update the flag if we have a document as target |
michael@0 | 181 | // see https://bugzilla.mozilla.org/show_bug.cgi?id=690829 |
michael@0 | 182 | if ("defaultView" in doc) { |
michael@0 | 183 | var id = utils.getWindowId(doc.defaultView); |
michael@0 | 184 | // dump("*** 'pageshow' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); |
michael@0 | 185 | map.updatePageLoadStatus(id, true); |
michael@0 | 186 | } |
michael@0 | 187 | |
michael@0 | 188 | // We need to add/remove the unload/pagehide event listeners to preserve caching. |
michael@0 | 189 | aWindow.addEventListener("beforeunload", beforeUnloadHandler, true); |
michael@0 | 190 | aWindow.addEventListener("pagehide", pageHideHandler, true); |
michael@0 | 191 | }; |
michael@0 | 192 | |
michael@0 | 193 | var DOMContentLoadedHandler = function (aEvent) { |
michael@0 | 194 | var doc = aEvent.originalTarget; |
michael@0 | 195 | |
michael@0 | 196 | // Only update the flag if we have a document as target |
michael@0 | 197 | if ("defaultView" in doc) { |
michael@0 | 198 | var id = utils.getWindowId(doc.defaultView); |
michael@0 | 199 | // dump("*** 'DOMContentLoaded' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); |
michael@0 | 200 | |
michael@0 | 201 | // We only care about error pages for DOMContentLoaded |
michael@0 | 202 | var errorRegex = /about:.+(error)|(blocked)\?/; |
michael@0 | 203 | if (errorRegex.exec(doc.baseURI)) { |
michael@0 | 204 | // Wait about 1s to be sure the DOM is ready |
michael@0 | 205 | utils.sleep(1000); |
michael@0 | 206 | |
michael@0 | 207 | map.updatePageLoadStatus(id, true); |
michael@0 | 208 | } |
michael@0 | 209 | |
michael@0 | 210 | // We need to add/remove the unload event listener to preserve caching. |
michael@0 | 211 | aWindow.addEventListener("beforeunload", beforeUnloadHandler, true); |
michael@0 | 212 | } |
michael@0 | 213 | }; |
michael@0 | 214 | |
michael@0 | 215 | // beforeunload is still needed because pagehide doesn't fire before the page is unloaded. |
michael@0 | 216 | // still use pagehide for cases when beforeunload doesn't get fired |
michael@0 | 217 | var beforeUnloadHandler = function (aEvent) { |
michael@0 | 218 | var doc = aEvent.originalTarget; |
michael@0 | 219 | |
michael@0 | 220 | // Only update the flag if we have a document as target |
michael@0 | 221 | if ("defaultView" in doc) { |
michael@0 | 222 | var id = utils.getWindowId(doc.defaultView); |
michael@0 | 223 | // dump("*** 'beforeunload' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); |
michael@0 | 224 | map.updatePageLoadStatus(id, false); |
michael@0 | 225 | } |
michael@0 | 226 | |
michael@0 | 227 | aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true); |
michael@0 | 228 | }; |
michael@0 | 229 | |
michael@0 | 230 | var pageHideHandler = function (aEvent) { |
michael@0 | 231 | var doc = aEvent.originalTarget; |
michael@0 | 232 | |
michael@0 | 233 | // Only update the flag if we have a document as target |
michael@0 | 234 | if ("defaultView" in doc) { |
michael@0 | 235 | var id = utils.getWindowId(doc.defaultView); |
michael@0 | 236 | // dump("*** 'pagehide' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); |
michael@0 | 237 | map.updatePageLoadStatus(id, false); |
michael@0 | 238 | } |
michael@0 | 239 | // If event.persisted is true the beforeUnloadHandler would never fire |
michael@0 | 240 | // and we have to remove the event handler here to avoid memory leaks. |
michael@0 | 241 | if (aEvent.persisted) |
michael@0 | 242 | aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true); |
michael@0 | 243 | }; |
michael@0 | 244 | |
michael@0 | 245 | var onWindowLoaded = function (aEvent) { |
michael@0 | 246 | var id = utils.getWindowId(aWindow); |
michael@0 | 247 | // dump("*** 'load' event: id=" + id + ", baseURI=" + aWindow.document.baseURI + "\n"); |
michael@0 | 248 | |
michael@0 | 249 | map.update(id, "loaded", true); |
michael@0 | 250 | |
michael@0 | 251 | // Note: Error pages will never fire a "pageshow" event. For those we |
michael@0 | 252 | // have to wait for the "DOMContentLoaded" event. That's the final state. |
michael@0 | 253 | // Error pages will always have a baseURI starting with |
michael@0 | 254 | // "about:" followed by "error" or "blocked". |
michael@0 | 255 | aWindow.addEventListener("DOMContentLoaded", DOMContentLoadedHandler, true); |
michael@0 | 256 | |
michael@0 | 257 | // Page is ready |
michael@0 | 258 | aWindow.addEventListener("pageshow", pageShowHandler, true); |
michael@0 | 259 | |
michael@0 | 260 | // Leave page (use caching) |
michael@0 | 261 | aWindow.addEventListener("pagehide", pageHideHandler, true); |
michael@0 | 262 | }; |
michael@0 | 263 | |
michael@0 | 264 | // If the window has already been finished loading, call the load handler |
michael@0 | 265 | // directly. Otherwise attach it to the current window. |
michael@0 | 266 | if (aWindow.document.readyState === 'complete') { |
michael@0 | 267 | onWindowLoaded(); |
michael@0 | 268 | } else { |
michael@0 | 269 | aWindow.addEventListener("load", onWindowLoaded, false); |
michael@0 | 270 | } |
michael@0 | 271 | } |
michael@0 | 272 | |
michael@0 | 273 | // Attach event listeners to all already open top-level windows |
michael@0 | 274 | function handleAttachEventListeners() { |
michael@0 | 275 | var enumerator = Cc["@mozilla.org/appshell/window-mediator;1"]. |
michael@0 | 276 | getService(Ci.nsIWindowMediator).getEnumerator(""); |
michael@0 | 277 | while (enumerator.hasMoreElements()) { |
michael@0 | 278 | var win = enumerator.getNext(); |
michael@0 | 279 | attachEventListeners(win); |
michael@0 | 280 | } |
michael@0 | 281 | } |
michael@0 | 282 | |
michael@0 | 283 | function init() { |
michael@0 | 284 | // Activate observer for new top level windows |
michael@0 | 285 | var observerService = Cc["@mozilla.org/observer-service;1"]. |
michael@0 | 286 | getService(Ci.nsIObserverService); |
michael@0 | 287 | observerService.addObserver(windowReadyObserver, "toplevel-window-ready", false); |
michael@0 | 288 | observerService.addObserver(windowCloseObserver, "outer-window-destroyed", false); |
michael@0 | 289 | observerService.addObserver(enterLeavePrivateBrowsingObserver, "private-browsing", false); |
michael@0 | 290 | |
michael@0 | 291 | handleAttachEventListeners(); |
michael@0 | 292 | } |