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: var EXPORTED_SYMBOLS = ["init", "map"]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: // imports michael@0: var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); michael@0: michael@0: var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); michael@0: michael@0: /** michael@0: * The window map is used to store information about the current state of michael@0: * open windows, e.g. loaded state michael@0: */ michael@0: var map = { michael@0: _windows : { }, michael@0: michael@0: /** michael@0: * Check if a given window id is contained in the map of windows michael@0: * michael@0: * @param {Number} aWindowId michael@0: * Outer ID of the window to check. michael@0: * @returns {Boolean} True if the window is part of the map, otherwise false. michael@0: */ michael@0: contains : function (aWindowId) { michael@0: return (aWindowId in this._windows); michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the value of the specified window's property. michael@0: * michael@0: * @param {Number} aWindowId michael@0: * Outer ID of the window to check. michael@0: * @param {String} aProperty michael@0: * Property to retrieve the value from michael@0: * @return {Object} Value of the window's property michael@0: */ michael@0: getValue : function (aWindowId, aProperty) { michael@0: if (!this.contains(aWindowId)) { michael@0: return undefined; michael@0: } else { michael@0: var win = this._windows[aWindowId]; michael@0: michael@0: return (aProperty in win) ? win[aProperty] michael@0: : undefined; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Remove the entry for a given window michael@0: * michael@0: * @param {Number} aWindowId michael@0: * Outer ID of the window to check. michael@0: */ michael@0: remove : function (aWindowId) { michael@0: if (this.contains(aWindowId)) { michael@0: delete this._windows[aWindowId]; michael@0: } michael@0: michael@0: // dump("* current map: " + JSON.stringify(this._windows) + "\n"); michael@0: }, michael@0: michael@0: /** michael@0: * Update the property value of a given window michael@0: * michael@0: * @param {Number} aWindowId michael@0: * Outer ID of the window to check. michael@0: * @param {String} aProperty michael@0: * Property to update the value for michael@0: * @param {Object} michael@0: * Value to set michael@0: */ michael@0: update : function (aWindowId, aProperty, aValue) { michael@0: if (!this.contains(aWindowId)) { michael@0: this._windows[aWindowId] = { }; michael@0: } michael@0: michael@0: this._windows[aWindowId][aProperty] = aValue; michael@0: // dump("* current map: " + JSON.stringify(this._windows) + "\n"); michael@0: }, michael@0: michael@0: /** michael@0: * Update the internal loaded state of the given content window. To identify michael@0: * an active (re)load action we make use of an uuid. michael@0: * michael@0: * @param {Window} aId - The outer id of the window to update michael@0: * @param {Boolean} aIsLoaded - Has the window been loaded michael@0: */ michael@0: updatePageLoadStatus : function (aId, aIsLoaded) { michael@0: this.update(aId, "loaded", aIsLoaded); michael@0: michael@0: var uuid = this.getValue(aId, "id_load_in_transition"); michael@0: michael@0: // If no uuid has been set yet or when the page gets unloaded create a new id michael@0: if (!uuid || !aIsLoaded) { michael@0: uuid = uuidgen.generateUUID(); michael@0: this.update(aId, "id_load_in_transition", uuid); michael@0: } michael@0: michael@0: // dump("*** Page status updated: id=" + aId + ", loaded=" + aIsLoaded + ", uuid=" + uuid + "\n"); michael@0: }, michael@0: michael@0: /** michael@0: * This method only applies to content windows, where we have to check if it has michael@0: * been successfully loaded or reloaded. An uuid allows us to wait for the next michael@0: * load action triggered by e.g. controller.open(). michael@0: * michael@0: * @param {Window} aId - The outer id of the content window to check michael@0: * michael@0: * @returns {Boolean} True if the content window has been loaded michael@0: */ michael@0: hasPageLoaded : function (aId) { michael@0: var load_current = this.getValue(aId, "id_load_in_transition"); michael@0: var load_handled = this.getValue(aId, "id_load_handled"); michael@0: michael@0: var isLoaded = this.contains(aId) && this.getValue(aId, "loaded") && michael@0: (load_current !== load_handled); michael@0: michael@0: if (isLoaded) { michael@0: // Backup the current uuid so we can check later if another page load happened. michael@0: this.update(aId, "id_load_handled", load_current); michael@0: } michael@0: michael@0: // dump("** Page has been finished loading: id=" + aId + ", status=" + isLoaded + ", uuid=" + load_current + "\n"); michael@0: michael@0: return isLoaded; michael@0: } michael@0: }; michael@0: michael@0: michael@0: // Observer when a new top-level window is ready michael@0: var windowReadyObserver = { michael@0: observe: function (aSubject, aTopic, aData) { michael@0: // Not in all cases we get a ChromeWindow. So ensure we really operate michael@0: // on such an instance. Otherwise load events will not be handled. michael@0: var win = utils.getChromeWindow(aSubject); michael@0: michael@0: // var id = utils.getWindowId(win); michael@0: // dump("*** 'toplevel-window-ready' observer notification: id=" + id + "\n"); michael@0: attachEventListeners(win); michael@0: } michael@0: }; michael@0: michael@0: michael@0: // Observer when a top-level window is closed michael@0: var windowCloseObserver = { michael@0: observe: function (aSubject, aTopic, aData) { michael@0: var id = utils.getWindowId(aSubject); michael@0: // dump("*** 'outer-window-destroyed' observer notification: id=" + id + "\n"); michael@0: michael@0: map.remove(id); michael@0: } michael@0: }; michael@0: michael@0: // Bug 915554 michael@0: // Support for the old Private Browsing Mode (eg. ESR17) michael@0: // TODO: remove once ESR17 is no longer supported michael@0: var enterLeavePrivateBrowsingObserver = { michael@0: observe: function (aSubject, aTopic, aData) { michael@0: handleAttachEventListeners(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Attach event listeners michael@0: * michael@0: * @param {ChromeWindow} aWindow michael@0: * Window to attach listeners on. michael@0: */ michael@0: function attachEventListeners(aWindow) { michael@0: // These are the event handlers michael@0: var pageShowHandler = function (aEvent) { michael@0: var doc = aEvent.originalTarget; michael@0: michael@0: // Only update the flag if we have a document as target michael@0: // see https://bugzilla.mozilla.org/show_bug.cgi?id=690829 michael@0: if ("defaultView" in doc) { michael@0: var id = utils.getWindowId(doc.defaultView); michael@0: // dump("*** 'pageshow' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); michael@0: map.updatePageLoadStatus(id, true); michael@0: } michael@0: michael@0: // We need to add/remove the unload/pagehide event listeners to preserve caching. michael@0: aWindow.addEventListener("beforeunload", beforeUnloadHandler, true); michael@0: aWindow.addEventListener("pagehide", pageHideHandler, true); michael@0: }; michael@0: michael@0: var DOMContentLoadedHandler = function (aEvent) { michael@0: var doc = aEvent.originalTarget; michael@0: michael@0: // Only update the flag if we have a document as target michael@0: if ("defaultView" in doc) { michael@0: var id = utils.getWindowId(doc.defaultView); michael@0: // dump("*** 'DOMContentLoaded' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); michael@0: michael@0: // We only care about error pages for DOMContentLoaded michael@0: var errorRegex = /about:.+(error)|(blocked)\?/; michael@0: if (errorRegex.exec(doc.baseURI)) { michael@0: // Wait about 1s to be sure the DOM is ready michael@0: utils.sleep(1000); michael@0: michael@0: map.updatePageLoadStatus(id, true); michael@0: } michael@0: michael@0: // We need to add/remove the unload event listener to preserve caching. michael@0: aWindow.addEventListener("beforeunload", beforeUnloadHandler, true); michael@0: } michael@0: }; michael@0: michael@0: // beforeunload is still needed because pagehide doesn't fire before the page is unloaded. michael@0: // still use pagehide for cases when beforeunload doesn't get fired michael@0: var beforeUnloadHandler = function (aEvent) { michael@0: var doc = aEvent.originalTarget; michael@0: michael@0: // Only update the flag if we have a document as target michael@0: if ("defaultView" in doc) { michael@0: var id = utils.getWindowId(doc.defaultView); michael@0: // dump("*** 'beforeunload' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); michael@0: map.updatePageLoadStatus(id, false); michael@0: } michael@0: michael@0: aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true); michael@0: }; michael@0: michael@0: var pageHideHandler = function (aEvent) { michael@0: var doc = aEvent.originalTarget; michael@0: michael@0: // Only update the flag if we have a document as target michael@0: if ("defaultView" in doc) { michael@0: var id = utils.getWindowId(doc.defaultView); michael@0: // dump("*** 'pagehide' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); michael@0: map.updatePageLoadStatus(id, false); michael@0: } michael@0: // If event.persisted is true the beforeUnloadHandler would never fire michael@0: // and we have to remove the event handler here to avoid memory leaks. michael@0: if (aEvent.persisted) michael@0: aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true); michael@0: }; michael@0: michael@0: var onWindowLoaded = function (aEvent) { michael@0: var id = utils.getWindowId(aWindow); michael@0: // dump("*** 'load' event: id=" + id + ", baseURI=" + aWindow.document.baseURI + "\n"); michael@0: michael@0: map.update(id, "loaded", true); michael@0: michael@0: // Note: Error pages will never fire a "pageshow" event. For those we michael@0: // have to wait for the "DOMContentLoaded" event. That's the final state. michael@0: // Error pages will always have a baseURI starting with michael@0: // "about:" followed by "error" or "blocked". michael@0: aWindow.addEventListener("DOMContentLoaded", DOMContentLoadedHandler, true); michael@0: michael@0: // Page is ready michael@0: aWindow.addEventListener("pageshow", pageShowHandler, true); michael@0: michael@0: // Leave page (use caching) michael@0: aWindow.addEventListener("pagehide", pageHideHandler, true); michael@0: }; michael@0: michael@0: // If the window has already been finished loading, call the load handler michael@0: // directly. Otherwise attach it to the current window. michael@0: if (aWindow.document.readyState === 'complete') { michael@0: onWindowLoaded(); michael@0: } else { michael@0: aWindow.addEventListener("load", onWindowLoaded, false); michael@0: } michael@0: } michael@0: michael@0: // Attach event listeners to all already open top-level windows michael@0: function handleAttachEventListeners() { michael@0: var enumerator = Cc["@mozilla.org/appshell/window-mediator;1"]. michael@0: getService(Ci.nsIWindowMediator).getEnumerator(""); michael@0: while (enumerator.hasMoreElements()) { michael@0: var win = enumerator.getNext(); michael@0: attachEventListeners(win); michael@0: } michael@0: } michael@0: michael@0: function init() { michael@0: // Activate observer for new top level windows michael@0: var observerService = Cc["@mozilla.org/observer-service;1"]. michael@0: getService(Ci.nsIObserverService); michael@0: observerService.addObserver(windowReadyObserver, "toplevel-window-ready", false); michael@0: observerService.addObserver(windowCloseObserver, "outer-window-destroyed", false); michael@0: observerService.addObserver(enterLeavePrivateBrowsingObserver, "private-browsing", false); michael@0: michael@0: handleAttachEventListeners(); michael@0: }