1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview Deferred resource loader for OOBE/Login screens.
7 */
8
9cr.define('cr.ui.login.ResourceLoader', function() {
10  'use strict';
11
12  // Deferred assets.
13  var ASSETS = {};
14
15  /**
16   * Register assets for deferred loading.  When the bundle is loaded
17   * assets will be added to the current page's DOM: <link> and <script>
18   * tags pointing to the CSS and JavaScript will be added to the
19   * <head>, and HTML will be appended to a specified element.
20   *
21   * @param {Object} desc Descriptor for the asset bundle
22   * @param {string} desc.id Unique identifier for the asset bundle.
23   * @param {Array=} desc.js URLs containing JavaScript sources.
24   * @param {Array=} desc.css URLs containing CSS rules.
25   * @param {Array.<Object>=} desc.html Descriptors for HTML fragments,
26   * each of which has a 'url' property and a 'targetID' property that
27   * specifies the node under which the HTML should be appended.
28   *
29   * Example:
30   *   ResourceLoader.registerAssets({
31   *     id: 'bundle123',
32   *     js: ['//foo.com/src.js', '//bar.com/lib.js'],
33   *     css: ['//foo.com/style.css'],
34   *     html: [{ url: '//foo.com/tmpls.html' targetID: 'tmpls'}]
35   *   });
36   *
37   * Note: to avoid cross-site requests, all HTML assets must be served
38   * from the same host as the rendered page.  For example, if the
39   * rendered page is served as chrome://oobe, then all the HTML assets
40   * must be served as chrome://oobe/path/to/something.html.
41   */
42  function registerAssets(desc) {
43    var html = desc.html || [];
44    var css = desc.css || [];
45    var js = desc.js || [];
46    ASSETS[desc.id] = {
47      html: html, css: css, js: js,
48      loaded: false,
49      count: html.length + css.length + js.length
50    };
51  }
52
53  /**
54   * Determines whether an asset bundle is defined for a specified id.
55   * @param {string} id The possible identifier.
56   */
57  function hasDeferredAssets(id) {
58    return id in ASSETS;
59  }
60
61  /**
62   * Determines whether an asset bundle has already been loaded.
63   * @param {string} id The identifier of the asset bundle.
64   */
65  function alreadyLoadedAssets(id) {
66    return hasDeferredAssets(id) && ASSETS[id].loaded;
67  }
68
69  /**
70   * Load a stylesheet into the current document.
71   * @param {string} id Identifier of the stylesheet's asset bundle.
72   * @param {string} url The URL resolving to a stylesheet.
73   */
74  function loadCSS(id, url) {
75    var link = document.createElement('link');
76    link.setAttribute('rel', 'stylesheet');
77    link.setAttribute('href', url);
78    link.onload = resourceLoaded.bind(null, id);
79    document.head.appendChild(link);
80  }
81
82  /**
83   * Load a script into the current document.
84   * @param {string} id Identifier of the script's asset bundle.
85   * @param {string} url The URL resolving to a script.
86   */
87  function loadJS(id, url) {
88    var script = document.createElement('script');
89    script.src = url;
90    script.onload = resourceLoaded.bind(null, id);
91    document.head.appendChild(script);
92  }
93
94  /**
95   * Move DOM nodes from one parent element to another.
96   * @param {HTMLElement} from Element whose children should be moved.
97   * @param {HTMLElement} to Element to which nodes should be appended.
98   */
99  function moveNodes(from, to) {
100    Array.prototype.forEach.call(from.children, to.appendChild, to);
101  }
102
103  /**
104   * Tests whether an XMLHttpRequest has successfully finished loading.
105   * @param {string} url The requested URL.
106   * @param {XMLHttpRequest} xhr The XHR object.
107   */
108  function isSuccessful(url, xhr) {
109    var fileURL = /^file:\/\//;
110    return xhr.readyState == 4 &&
111        (xhr.status == 200 || fileURL.test(url) && xhr.status == 0);
112  }
113
114  /*
115   * Load a chunk of HTML into the current document.
116   * @param {string} id Identifier of the page's asset bundle.
117   * @param {Object} html Descriptor of the HTML to fetch.
118   * @param {string} html.url The URL resolving to some HTML.
119   * @param {string} html.targetID The element ID to which the retrieved
120   * HTML nodes should be appended.
121   */
122  function loadHTML(id, html) {
123    var xhr = new XMLHttpRequest();
124    xhr.open('GET', html.url);
125    xhr.onreadystatechange = function() {
126      if (isSuccessful(html.url, xhr)) {
127        moveNodes(this.responseXML.body, $(html.targetID));
128        resourceLoaded(id);
129      }
130    };
131    xhr.responseType = 'document';
132    xhr.send();
133  }
134
135  /**
136   * Record that a resource has been loaded for an asset bundle.  When
137   * all the resources have been loaded the callback that was specified
138   * in the loadAssets call is invoked.
139   * @param {string} id Identifier of the asset bundle.
140   */
141  function resourceLoaded(id) {
142    var assets = ASSETS[id];
143    assets.count--;
144    if (assets.count == 0)
145      finishedLoading(id);
146  }
147
148  /**
149   * Finishes loading an asset bundle.
150   * @param {string} id Identifier of the asset bundle.
151   */
152  function finishedLoading(id) {
153    var assets = ASSETS[id];
154    console.log('Finished loading asset bundle', id);
155    assets.loaded = true;
156    window.setTimeout(function() {
157      assets.callback();
158      chrome.send('screenAssetsLoaded', [id]);
159    }, 0);
160  }
161
162  /**
163   * Load an asset bundle, invoking the callback when finished.
164   * @param {string} id Identifier for the asset bundle to load.
165   * @param {function()=} callback Function to invoke when done loading.
166   */
167  function loadAssets(id, callback) {
168    var assets = ASSETS[id];
169    assets.callback = callback || function() {};
170    console.log('Loading asset bundle', id);
171    if (alreadyLoadedAssets(id))
172      console.warn('asset bundle', id, 'already loaded!');
173    if (assets.count == 0) {
174      finishedLoading(id);
175    } else {
176      assets.css.forEach(loadCSS.bind(null, id));
177      assets.js.forEach(loadJS.bind(null, id));
178      assets.html.forEach(loadHTML.bind(null, id));
179    }
180  }
181
182  return {
183    alreadyLoadedAssets: alreadyLoadedAssets,
184    hasDeferredAssets: hasDeferredAssets,
185    loadAssets: loadAssets,
186    registerAssets: registerAssets
187  };
188});
189