1// Copyright 2006 Google Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14//
15// This startup script should be included in host pages either just after
16// <body> or inside the <head> after module <meta> tags.
17//
18
19//////////////////////////////////////////////////////////////////////////////
20// DynamicResources
21//
22
23function DynamicResources() {
24  this.pendingElemsBySrc_ = {};
25  this.pendingScriptElems_ = new Array();
26}
27DynamicResources.prototype = {};
28
29// The array is set up such that, pairwise, the entries are (src, readyFnStr).
30// Called once for each module that is attached to the host page.
31// It is theoretically possible that addScripts() could be called reentrantly
32// if the browser event loop is pumped during this function and an iframe loads;
33// we may want to enhance this method in the future to support that case.
34DynamicResources.prototype.addScripts = function(scriptArray, insertBeforeElem) {
35  var wasEmpty = (this.pendingScriptElems_.length == 0);
36  var anyAdded = false;
37  for (var i = 0, n = scriptArray.length; i < n; i += 2) {
38    var src = scriptArray[i];
39    if (this.pendingElemsBySrc_[src]) {
40      // Don't load the same script twice.
41      continue;
42    }
43    // Set up the element but don't add it to the DOM until its turn.
44    anyAdded = true;
45    var e = document.createElement("script");
46    this.pendingElemsBySrc_[src] = e;
47    var readyFn;
48    eval("readyFn = " + scriptArray[i+1]);
49    e.__readyFn = readyFn;
50    e.type = "text/javascript";
51    e.src = src;
52    e.__insertBeforeElem = insertBeforeElem;
53    this.pendingScriptElems_ = this.pendingScriptElems_.concat(e);
54  }
55
56  if (wasEmpty && anyAdded) {
57    // Kickstart.
58    this.injectScript(this.pendingScriptElems_[0]);
59  }
60}
61
62DynamicResources.prototype.injectScript = function(scriptElem) {
63  var parentElem = scriptElem.__insertBeforeElem.parentNode;
64  parentElem.insertBefore(scriptElem, scriptElem.__insertBeforeElem);
65}
66
67DynamicResources.prototype.addStyles = function(styleSrcArray, insertBeforeElem) {
68  var parent = insertBeforeElem.parentNode;
69  for (var i = 0, n = styleSrcArray.length; i < n; ++i) {
70    var src = styleSrcArray[i];
71    if (this.pendingElemsBySrc_[src])
72      continue;
73    var e = document.createElement("link");
74    this.pendingElemsBySrc_[src] = e;
75    e.type = "text/css";
76    e.rel = "stylesheet";
77    e.href = src;
78    parent.insertBefore(e, insertBeforeElem);
79  }
80}
81
82DynamicResources.prototype.isReady = function() {
83  var elems = this.pendingScriptElems_;
84  if (elems.length > 0) {
85    var e = elems[0];
86    if (!e.__readyFn()) {
87      // The pending script isn't ready yet.
88      return false;
89    }
90
91    // The pending script has now finished loading. Enqueue the next, if any.
92    e.__readyFn = null;
93    elems.shift();
94    if (elems.length > 0) {
95      // There is another script.
96      this.injectScript(elems[0]);
97      return false;
98    }
99  }
100
101  // There are no more pending scripts.
102  return true;
103}
104
105//////////////////////////////////////////////////////////////////////////////
106// ModuleControlBlock
107//
108function ModuleControlBlock(metaElem, rawName) {
109  var parts = ["", rawName];
110  var i = rawName.lastIndexOf("=");
111  if (i != -1) {
112    parts[0] = rawName.substring(0, i) + '/';
113    parts[1] = rawName.substring(i+1);
114  }
115
116  this.metaElem_ = metaElem;
117  this.baseUrl_ = parts[0];
118  this.name_ = parts[1];
119  this.compilationLoaded_ = false;
120  this.frameWnd_ = null;
121}
122ModuleControlBlock.prototype = {};
123
124/**
125 * Determines whether this module is fully loaded and ready to run.
126 */
127ModuleControlBlock.prototype.isReady = function() {
128  return this.compilationLoaded_;
129};
130
131/**
132 * Called when the compilation for this module is loaded.
133 */
134ModuleControlBlock.prototype.compilationLoaded = function(frameWnd) {
135  this.frameWnd_ = frameWnd;
136  this.compilationLoaded_ = true;
137}
138
139/**
140 * Gets the logical module name, not including a base url prefix if one was
141 * specified.
142 */
143ModuleControlBlock.prototype.getName = function() {
144  return this.name_;
145}
146
147/**
148 * Gets the base URL of the module, guaranteed to end with a slash.
149 */
150ModuleControlBlock.prototype.getBaseURL = function() {
151  return this.baseUrl_;
152}
153
154/**
155 * Gets the window of the module's frame.
156 */
157ModuleControlBlock.prototype.getModuleFrameWindow = function() {
158  return this.frameWnd_;
159}
160
161/**
162 * Injects a set of dynamic scripts.
163 * The array is set up such that, pairwise, the entries are (src, readyFnStr).
164 */
165ModuleControlBlock.prototype.addScripts = function(scriptSrcArray) {
166  return ModuleControlBlocks.dynamicResources_.addScripts(scriptSrcArray, this.metaElem_);
167}
168
169/**
170 * Injects a set of dynamic styles.
171 */
172ModuleControlBlock.prototype.addStyles = function(styleSrcArray) {
173  return ModuleControlBlocks.dynamicResources_.addStyles(styleSrcArray, this.metaElem_);
174}
175
176//////////////////////////////////////////////////////////////////////////////
177// ModuleControlBlocks
178//
179function ModuleControlBlocks() {
180  this.blocks_ = [];
181}
182ModuleControlBlocks.dynamicResources_ = new DynamicResources(); // "static"
183ModuleControlBlocks.prototype = {};
184
185/**
186 * Adds a module control control block for the named module.
187 * @param metaElem the meta element that caused the module to be added
188 * @param name the name of the module being added, optionally preceded by
189 * an alternate base url of the form "_path_=_module_".
190 */
191ModuleControlBlocks.prototype.add = function(metaElem, name) {
192  var mcb = new ModuleControlBlock(metaElem, name);
193  this.blocks_ = this.blocks_.concat(mcb);
194};
195
196/**
197 * Determines whether all the modules are loaded and ready to run.
198 */
199ModuleControlBlocks.prototype.isReady = function() {
200  for (var i = 0, n = this.blocks_.length; i < n; ++i) {
201    var mcb = this.blocks_[i];
202    if (!mcb.isReady()) {
203      return false;
204    }
205  }
206
207  // Are there any pending dynamic resources (e.g. styles, scripts)?
208  if (!ModuleControlBlocks.dynamicResources_.isReady()) {
209    // No, we're still waiting on one or more dynamic resources.
210    return false;
211  }
212
213  return true;
214}
215
216/**
217 * Determines whether there are any module control blocks.
218 */
219ModuleControlBlocks.prototype.isEmpty = function() {
220  return this.blocks_.length == 0;
221}
222
223/**
224 * Gets the module control block at the specified index.
225 */
226ModuleControlBlocks.prototype.get = function(index) {
227  return this.blocks_[index];
228}
229
230/**
231 * Injects an iframe for each module.
232 */
233ModuleControlBlocks.prototype.injectFrames = function() {
234  for (var i = 0, n = this.blocks_.length; i < n; ++i) {
235    var mcb = this.blocks_[i];
236
237    // Insert an iframe for the module
238    var iframe = document.createElement("iframe");
239    var selectorUrl = mcb.getBaseURL() + mcb.getName() + ".nocache.html";
240    selectorUrl += "?" + (__gwt_isHosted() ? "h&" : "" ) + i;
241    var unique = new Date().getTime();
242    selectorUrl += "&" + unique;
243    iframe.style.border = '0px';
244    iframe.style.width = '0px';
245    iframe.style.height = '0px';
246
247    // Fragile browser-specific ordering issues below
248
249/*@cc_on
250    // prevent extra clicky noises on IE
251    iframe.src = selectorUrl;
252@*/
253
254    if (document.body.firstChild) {
255      document.body.insertBefore(iframe, document.body.firstChild);
256    } else {
257      document.body.appendChild(iframe);
258    }
259
260/*@cc_on
261    // prevent extra clicky noises on IE
262    return;
263@*/
264
265    if (iframe.contentWindow) {
266      // Older Mozilla has a caching bug for the iframe and won't reload the nocache.
267      iframe.contentWindow.location.replace(selectorUrl);
268    } else {
269      // Older Safari doesn't have a contentWindow.
270      iframe.src = selectorUrl;
271    }
272  }
273}
274
275/**
276 * Runs the entry point for each module.
277 */
278ModuleControlBlocks.prototype.run = function() {
279  for (var i = 0, n = this.blocks_.length; i < n; ++i) {
280    var mcb = this.blocks_[i];
281    var name = mcb.getName();
282    var frameWnd = mcb.getModuleFrameWindow();
283    if (__gwt_isHosted()) {
284      if (!window.external.gwtOnLoad(frameWnd, name)) {
285        // Module failed to load.
286        if (__gwt_onLoadError) {
287            __gwt_onLoadError(name);
288        } else {
289            window.alert("Failed to load module '" + name +
290            "'.\nPlease see the log in the development shell for details.");
291        }
292      }
293    } else {
294      // The compilation itself handles calling the error function.
295      frameWnd.gwtOnLoad(__gwt_onLoadError, name);
296    }
297  }
298}
299
300//////////////////////////////////////////////////////////////////////////////
301// Globals
302//
303
304var __gwt_retryWaitMillis = 10;
305var __gwt_isHostPageLoaded = false;
306var __gwt_metaProps = {};
307var __gwt_onPropertyError = null;
308var __gwt_onLoadError = null;
309var __gwt_moduleControlBlocks = new ModuleControlBlocks();
310
311//////////////////////////////////////////////////////////////////////////////
312// Common
313//
314
315/**
316 * Determines whether or not the page is being loaded in the GWT hosted browser.
317 */
318function __gwt_isHosted() {
319  if (window.external && window.external.gwtOnLoad) {
320    // gwt.hybrid makes the hosted browser pretend not to be
321    if (document.location.href.indexOf("gwt.hybrid") == -1) {
322      return true;
323    }
324  }
325  return false;
326}
327
328/**
329 * Tries to get a module control block based on a query string passed in from
330 * the caller. Used by iframes to get references back to their mcbs.
331 * @param queryString the entire query string as returned by location.search,
332 * which notably includes the leading '?' if one is specified
333 * @return the relevant module control block, or <code>null</code> if it cannot
334 * be derived based on <code>queryString</code>
335 */
336function __gwt_tryGetModuleControlBlock(queryString) {
337  if (queryString.length > 0) {
338    // The pattern is ?[h&]<index>[&<unique>]
339    var queryString = queryString.substring(1);
340    if (queryString.indexOf("h&") == 0) {
341      // Ignore the hosted mode flag here; only GWTShellServlet cares about it.
342      queryString = queryString.substring(2);
343    }
344    var pos = queryString.indexOf("&");
345    if (pos >= 0) {
346      queryString = queryString.substring(0, pos);
347    }
348    var mcbIndex = parseInt(queryString);
349    if (!isNaN(mcbIndex)) {
350      var mcb = __gwt_moduleControlBlocks.get(mcbIndex);
351      return mcb;
352    }
353    // Ignore the unique number that remains on the query string.
354  }
355  return null;
356}
357
358/**
359 * Parses meta tags from the host html.
360 *
361 * <meta name="gwt:module" content="_module-name_">
362 *    causes the specified module to be loaded
363 *
364 * <meta name="gwt:property" content="_name_=_value_">
365 *    statically defines a deferred binding client property
366 *
367 * <meta name="gwt:onPropertyErrorFn" content="_fnName_">
368 *    specifies the name of a function to call if a client property is set to
369 *    an invalid value (meaning that no matching compilation will be found)
370 *
371 * <meta name="gwt:onLoadErrorFn" content="_fnName_">
372 *    specifies the name of a function to call if an exception happens during
373 *    bootstrapping or if a module throws an exception out of onModuleLoad();
374 *    the function should take a message parameter
375 */
376function __gwt_processMetas() {
377  var metas = document.getElementsByTagName("meta");
378  for (var i = 0, n = metas.length; i < n; ++i) {
379    var meta = metas[i];
380    var name = meta.getAttribute("name");
381    if (name) {
382      if (name == "gwt:module") {
383        var moduleName = meta.getAttribute("content");
384        if (moduleName) {
385          __gwt_moduleControlBlocks.add(meta, moduleName);
386        }
387      } else if (name == "gwt:property") {
388        var content = meta.getAttribute("content");
389        if (content) {
390          var name = content, value = "";
391          var eq = content.indexOf("=");
392          if (eq != -1) {
393            name = content.substring(0, eq);
394            value = content.substring(eq+1);
395          }
396          __gwt_metaProps[name] = value;
397        }
398      } else if (name == "gwt:onPropertyErrorFn") {
399        var content = meta.getAttribute("content");
400        if (content) {
401          try {
402            __gwt_onPropertyError = eval(content);
403          } catch (e) {
404            window.alert("Bad handler \"" + content +
405              "\" for \"gwt:onPropertyErrorFn\"");
406          }
407        }
408      } else if (name == "gwt:onLoadErrorFn") {
409        var content = meta.getAttribute("content");
410        if (content) {
411          try {
412            __gwt_onLoadError = eval(content);
413          } catch (e) {
414            window.alert("Bad handler \"" + content +
415              "\" for \"gwt:onLoadErrorFn\"");
416          }
417        }
418      }
419    }
420  }
421}
422
423/**
424 * Determines the value of a deferred binding client property specified
425 * statically in host html.
426 */
427function __gwt_getMetaProperty(name) {
428  var value = __gwt_metaProps[name];
429  if (value) {
430    return value;
431  } else {
432    return null;
433  }
434}
435
436/**
437 * Determines whether or not a particular property value is allowed.
438 * @param wnd the caller's window object (not $wnd!)
439 * @param propName the name of the property being checked
440 * @param propValue the property value being tested
441 */
442function __gwt_isKnownPropertyValue(wnd, propName, propValue) {
443  return propValue in wnd["values$" + propName];
444}
445
446/**
447 * Called by the selection script when a property has a bad value or is missing.
448 * 'allowedValues' is an array of strings. Can be hooked in the host page using
449 * gwt:onPropertyErrorFn.
450 */
451function __gwt_onBadProperty(moduleName, propName, allowedValues, badValue) {
452  if (__gwt_onPropertyError) {
453    __gwt_onPropertyError(moduleName, propName, allowedValues, badValue);
454    return;
455  } else {
456    var msg = "While attempting to load module \"" + moduleName + "\", ";
457    if (badValue != null) {
458       msg += "property \"" + propName + "\" was set to the unexpected value \""
459        + badValue + "\"";
460    } else {
461       msg += "property \"" + propName + "\" was not specified";
462    }
463
464    msg += "\n\nAllowed values: " + allowedValues;
465
466    window.alert(msg);
467  }
468}
469
470/**
471 * Called directly from compiled code.
472 */
473function __gwt_initHandlers(resize, beforeunload, unload) {
474   var oldOnResize = window.onresize;
475   window.onresize = function() {
476      resize();
477      if (oldOnResize)
478         oldOnResize();
479   };
480
481   var oldOnBeforeUnload = window.onbeforeunload;
482   window.onbeforeunload = function() {
483      var ret = beforeunload();
484
485      var oldRet;
486      if (oldOnBeforeUnload)
487        oldRet = oldOnBeforeUnload();
488
489      if (ret !== null)
490        return ret;
491      return oldRet;
492   };
493
494   var oldOnUnload = window.onunload;
495   window.onunload = function() {
496      unload();
497      if (oldOnUnload)
498         oldOnUnload();
499   };
500}
501
502//////////////////////////////////////////////////////////////////////////////
503// Hosted Mode
504//
505function __gwt_onUnloadHostedMode() {
506    window.external.gwtOnLoad(null, null);
507    if (__gwt_onUnloadHostedMode.oldUnloadHandler) {
508        __gwt_onUnloadHostedMode.oldUnloadHandler();
509    }
510}
511
512//////////////////////////////////////////////////////////////////////////////
513// Bootstrap
514//
515
516/**
517 * Waits until all startup preconditions are satisfied, then launches the
518 * user-defined startup code for each module.
519 */
520function __gwt_latchAndLaunch() {
521  var ready = true;
522
523  // Are there any compilations still pending?
524  if (ready && !__gwt_moduleControlBlocks.isReady()) {
525    // Yes, we're still waiting on one or more compilations.
526    ready = false;
527  }
528
529  // Has the host html onload event fired?
530  if (ready && !__gwt_isHostPageLoaded) {
531    // No, the host html page hasn't fully loaded.
532    ready = false;
533  }
534
535  // Are we ready to run user code?
536  if (ready) {
537    // Yes: run entry points.
538    __gwt_moduleControlBlocks.run();
539  } else {
540    // No: try again soon.
541    window.setTimeout(__gwt_latchAndLaunch, __gwt_retryWaitMillis);
542  }
543}
544
545/**
546 * Starts the module-loading sequence after meta tags have been processed and
547 * the body element exists.
548 */
549function __gwt_loadModules() {
550  // Make sure the body element exists before starting.
551  if (!document.body) {
552    // Try again soon.
553    window.setTimeout(__gwt_loadModules, __gwt_retryWaitMillis);
554    return;
555  }
556
557  // Inject a frame for each module.
558  __gwt_moduleControlBlocks.injectFrames();
559
560  // Try to launch module entry points once everything is ready.
561  __gwt_latchAndLaunch();
562}
563
564/**
565 * The very first thing to run, and it runs exactly once unconditionally.
566 */
567function __gwt_bootstrap() {
568  // Hook onunload for hosted mode.
569  if (__gwt_isHosted()) {
570    __gwt_onUnloadHostedMode.oldUnloadHandler = window.onunload;
571    window.onunload = __gwt_onUnloadHostedMode;
572  }
573
574  // Hook the current window onload handler.
575  var oldHandler = window.onload;
576  window.onload = function() {
577    __gwt_isHostPageLoaded = true;
578    if (oldHandler) {
579      oldHandler();
580    }
581  };
582
583  // Parse meta tags from host html.
584  __gwt_processMetas();
585
586  // Load any modules.
587  __gwt_loadModules();
588}
589
590// Go.
591__gwt_bootstrap();
592