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