blob: d4e59371583a1454333bfe21b914d7bf20e3c5f3 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Deferred resource loader for OOBE/Login screens.
*/
cr.define('cr.ui.login.ResourceLoader', function() {
'use strict';
// Deferred assets.
var ASSETS = {};
/**
* Register assets for deferred loading. When the bundle is loaded
* assets will be added to the current page's DOM: <link> and <script>
* tags pointing to the CSS and JavaScript will be added to the
* <head>, and HTML will be appended to a specified element.
*
* @param {Object} desc Descriptor for the asset bundle
* @param {string} desc.id Unique identifier for the asset bundle.
* @param {Array=} desc.js URLs containing JavaScript sources.
* @param {Array=} desc.css URLs containing CSS rules.
* @param {Array<Object>=} desc.html Descriptors for HTML fragments,
* each of which has a 'url' property and a 'targetID' property that
* specifies the node under which the HTML should be appended. If 'targetID'
* is null, then the fetched body will be appended to document.body.
*
* Example:
* ResourceLoader.registerAssets({
* id: 'bundle123',
* js: ['//foo.com/src.js', '//bar.com/lib.js'],
* css: ['//foo.com/style.css'],
* html: [{ url: '//foo.com/tmpls.html' targetID: 'tmpls'}]
* });
*
* Note: to avoid cross-site requests, all HTML assets must be served
* from the same host as the rendered page. For example, if the
* rendered page is served as chrome://oobe, then all the HTML assets
* must be served as chrome://oobe/path/to/something.html.
*/
function registerAssets(desc) {
var html = desc.html || [];
var css = desc.css || [];
var js = desc.js || [];
ASSETS[desc.id] = {
html: html, css: css, js: js,
loaded: false,
count: html.length + css.length + js.length
};
}
/**
* Determines whether an asset bundle is defined for a specified id.
* @param {string} id The possible identifier.
*/
function hasDeferredAssets(id) {
return id in ASSETS;
}
/**
* Determines whether an asset bundle has already been loaded.
* @param {string} id The identifier of the asset bundle.
*/
function alreadyLoadedAssets(id) {
return hasDeferredAssets(id) && ASSETS[id].loaded;
}
/**
* Load a stylesheet into the current document.
* @param {string} id Identifier of the stylesheet's asset bundle.
* @param {string} url The URL resolving to a stylesheet.
*/
function loadCSS(id, url) {
var link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', url);
link.onload = resourceLoaded.bind(null, id);
document.head.appendChild(link);
}
/**
* Load a script into the current document.
* @param {string} id Identifier of the script's asset bundle.
* @param {string} url The URL resolving to a script.
*/
function loadJS(id, url) {
var script = document.createElement('script');
script.src = url;
script.onload = resourceLoaded.bind(null, id);
document.head.appendChild(script);
}
/**
* Move DOM nodes from one parent element to another.
* @param {HTMLElement} from Element whose children should be moved.
* @param {HTMLElement} to Element to which nodes should be appended.
*/
function moveNodes(from, to) {
Array.prototype.forEach.call(from.children, function(child) {
to.appendChild(document.importNode(child, true));
});
}
/**
* Tests whether an XMLHttpRequest has successfully finished loading.
* @param {string} url The requested URL.
* @param {XMLHttpRequest} xhr The XHR object.
*/
function isSuccessful(url, xhr) {
var fileURL = /^file:\/\//;
return xhr.readyState == 4 &&
(xhr.status == 200 || fileURL.test(url) && xhr.status == 0);
}
/*
* Load a chunk of HTML into the current document.
* @param {string} id Identifier of the page's asset bundle.
* @param {Object} html Descriptor of the HTML to fetch.
* @param {string} html.url The URL resolving to some HTML.
* @param {string?} html.targetID The element ID to which the retrieved
* HTML nodes should be appended. If null, then the elements will be appended
* to document.body instead.
*/
function loadHTML(id, html) {
var xhr = new XMLHttpRequest();
xhr.open('GET', html.url);
xhr.onreadystatechange = function() {
if (isSuccessful(html.url, xhr)) {
moveNodes(this.responseXML.head, document.head);
moveNodes(this.responseXML.body, $(html.targetID) || document.body);
resourceLoaded(id);
}
};
xhr.responseType = 'document';
xhr.send();
}
/**
* Record that a resource has been loaded for an asset bundle. When
* all the resources have been loaded the callback that was specified
* in the loadAssets call is invoked.
* @param {string} id Identifier of the asset bundle.
*/
function resourceLoaded(id) {
var assets = ASSETS[id];
assets.count--;
if (assets.count == 0)
finishedLoading(id);
}
/**
* Finishes loading an asset bundle.
* @param {string} id Identifier of the asset bundle.
*/
function finishedLoading(id) {
var assets = ASSETS[id];
console.log('Finished loading asset bundle ' + id);
assets.loaded = true;
window.setTimeout(function() {
assets.callback();
chrome.send('screenAssetsLoaded', [id]);
}, 0);
}
/**
* Load an asset bundle, invoking the callback when finished.
* @param {string} id Identifier for the asset bundle to load.
* @param {function()=} callback Function to invoke when done loading.
*/
function loadAssets(id, callback) {
var assets = ASSETS[id];
assets.callback = callback || function() {};
console.log('Loading asset bundle ' + id);
if (alreadyLoadedAssets(id))
console.warn('asset bundle', id, 'already loaded!');
if (assets.count == 0) {
finishedLoading(id);
} else {
assets.css.forEach(loadCSS.bind(null, id));
assets.js.forEach(loadJS.bind(null, id));
assets.html.forEach(loadHTML.bind(null, id));
}
}
/**
* Load an asset bundle after the document has been loaded and Chrome is idle.
* @param {string} id Identifier for the asset bundle to load.
* @param {function()=} callback Function to invoke when done loading.
* @param {number=} opt_idleTimeoutMs The maximum amount of time to wait for
* an idle notification.
*/
function loadAssetsOnIdle(id, callback, opt_idleTimeoutMs) {
opt_idleTimeoutMs = opt_idleTimeoutMs || 250;
var loadOnIdle = function() {
window.requestIdleCallback(function() {
loadAssets(id, callback);
}, { timeout: opt_idleTimeoutMs });
};
if (document.readyState == 'loading') {
window.addEventListener('DOMContentLoaded', loadOnIdle);
} else {
// DOMContentLoaded has already been called if document.readyState is
// 'interactive' or 'complete', so invoke the callback immediately.
loadOnIdle();
}
}
/**
* Wait until the element with the given |id| has finished its layout,
* specifically, after it has an offsetHeight > 0.
* @param {string|function()} selector Identifier of the element to wait
* or a callback function to obtain element to wait for.
* @param {function()} callback Function to invoke when done loading.
*/
function waitUntilLayoutComplete(selector, callback) {
if (typeof selector == 'string') {
var id = selector;
selector = function() { return $(id) };
}
var doWait = function() {
var element = selector();
if (!element || !element.offsetHeight) {
requestAnimationFrame(doWait);
return;
}
callback(element);
};
requestAnimationFrame(doWait);
}
return {
alreadyLoadedAssets: alreadyLoadedAssets,
hasDeferredAssets: hasDeferredAssets,
loadAssets: loadAssets,
loadAssetsOnIdle: loadAssetsOnIdle,
waitUntilLayoutComplete: waitUntilLayoutComplete,
registerAssets: registerAssets
};
});