var Montage = require("./core").Montage,
Promise = require("./promise").Promise,
URL = require("./mini-url");
var DocumentResources = Montage.specialize({
_SCRIPT_TIMEOUT: {
value: 5000
},
_document: {
value: null
},
_resources: {
value: null
},
_preloaded: {
value: null
},
_expectedStyles: {
value: null
},
constructor: {
value: function DocumentResources() {
this._expectedStyles = [];
this._isPollingDocumentStyleSheets = !this._isLinkLoadEventAvailable();
}
},
/**
* Returns major webkit version or null if not webkit
*/
_webkitVersion: {
value: function () {
var version = /AppleWebKit\/([\d.]+)/.exec(navigator.userAgent);
if (version) {
return parseInt(version[1]);
}
return null;
}
},
/**
* Returns if the load event is available for link elements
*/
_isLinkLoadEventAvailable: {
value: function () {
var link = document.createElement("link"),
webkitVersion = this._webkitVersion();
if ("onload" in link) {
// In webkits below version 535, onload is in link but
// the event doesn't fire when the file has been loaded
return !(webkitVersion !== null && webkitVersion < 535);
}
return false;
}
},
initWithDocument: {
value: function (_document) {
this.clear();
this._document = _document;
this._populateWithDocument(_document);
return this;
}
},
_populateWithDocument: {
value: function (_document) {
var scripts = _document.querySelectorAll("script"),
forEach = Array.prototype.forEach;
forEach.call(scripts, function (script) {
if (script.src) {
this._addResource(this.normalizeUrl(script.src));
}
}, this);
var links = _document.querySelectorAll("link");
forEach.call(links, function (link) {
if (link.rel === "stylesheet") {
this._addResource(this.normalizeUrl(link.href));
}
}, this);
}
},
clear: {
value: function () {
this._resources = Object.create(null);
this._preloaded = Object.create(null);
}
},
_addResource: {
value: function (url) {
this._resources[url] = true;
}
},
hasResource: {
value: function (url) {
return url in this._resources;
}
},
isResourcePreloaded: {
value: function (url) {
return this._preloaded[url] === true;
}
},
isResourcePreloading: {
value: function(url) {
return Promise.is(this._preloaded[url]);
}
},
setResourcePreloadedPromise: {
value: function (url, promise) {
this._preloaded[url] = promise;
}
},
setResourcePreloaded: {
value: function (url) {
this._preloaded[url] = true;
}
},
getResourcePreloadedPromise: {
value: function (url) {
return this._preloaded[url];
}
},
addScript: {
value: function (script) {
var url = this.normalizeUrl(script.src);
if (url) {
if (this.isResourcePreloaded(url)) {
return Promise.resolve();
} else if (this.isResourcePreloading(url)) {
return this.getResourcePreloadedPromise(url);
} else {
return this._importScript(script);
}
} else {
return this._importScript(script);
}
}
},
// TODO: this should probably be in TemplateResources, need to come up with
// a better scheme for know what has been loaded in what document.
// This change would make addStyle sync and up to whoever is adding
// to listen for its proper loading.
_importScript: {
value: function (script) {
var self = this,
_document = this._document,
documentHead = _document.head,
promise,
url = script.src;
if (url) {
self._addResource(url);
promise = new Promise(function(resolve, reject){
var loadingTimeout;
// We wait until all scripts are loaded, this is important
// because templateDidLoad might need to access objects that
// are defined in these scripts, the downsize is that it takes
// more time for the template to be considered loaded.
var scriptLoadedFunction = function scriptLoaded(event) {
self.setResourcePreloaded(url);
script.removeEventListener("load", scriptLoaded, false);
script.removeEventListener("error", scriptLoaded, false);
clearTimeout(loadingTimeout);
resolve(event);
};
script.addEventListener("load", scriptLoadedFunction, false);
script.addEventListener("error", scriptLoadedFunction, false);
// Setup the timeout to wait for the script until the resource
// is considered loaded. The template doesn't fail loading just
// because a single script didn't load.
//Benoit: It is odd that we act as if everything was fine here...
loadingTimeout = setTimeout(function () {
self.setResourcePreloaded(url);
resolve();
}, self._SCRIPT_TIMEOUT);
});
this.setResourcePreloadedPromise(url, promise);
} else {
promise = new Promise(function(resolve,reject){
resolve();
});
}
// This is one of the very few ocasions where we go around the draw
// loop to modify the DOM. Since it doesn't affect the layout
// (unless the script itself does) it shouldn't be a problem.
documentHead.appendChild(
_document.createComment("Inserted from FIXME")
);
documentHead.appendChild(script);
return promise;
}
},
handleEvent: {
value: function (event) {
var target = event.target,
index;
if (target.tagName === "LINK") {
index = this._expectedStyles.indexOf(target.href);
if (index >= 0) {
this._expectedStyles.splice(index, 1);
}
target.removeEventListener("load", this, false);
target.removeEventListener("error", this, false);
}
}
},
addStyle: {
value: function (element, DOMParent) {
var url = element.getAttribute("href"),
documentHead;
if (url) {
url = this.normalizeUrl(url);
if (this.hasResource(url)) {
return;
}
this._addResource(url);
this._expectedStyles.push(url);
if (!this._isPollingDocumentStyleSheets) {
// fixme: Quick workaround for IE 11. Need a better patch.
// -> link DOM elements are loaded before they are attached to the DOM
element.setAttribute("href", url);
element.addEventListener("load", this, false);
element.addEventListener("error", this, false);
}
}
documentHead = DOMParent || this._document.head;
documentHead.insertBefore(element, documentHead.firstChild);
}
},
normalizeUrl: {
value: function (url, baseUrl) {
if (!baseUrl) {
baseUrl = this._document.location.href;
}
return URL.resolve(baseUrl, url);
}
},
domain: {
value: global.location ? global.location.protocol + "//" + global.location.host : ''
},
isCrossDomain: {
value: function (url) {
return url.indexOf(this.domain + "/") !== 0 ||
url.indexOf("file://") === 0;
}
},
preloadResource: {
value: function (url, forcePreload) {
var skipPreload;
url = this.normalizeUrl(url);
// We do not preload cross-domain urls to avoid x-domain security
// errors unless forcePreload is requested, it could be a server
// configured with CORS.
if (!forcePreload && this.isCrossDomain(url)) {
skipPreload = true;
}
if (skipPreload || this.isResourcePreloaded(url)) {
return Promise.resolve();
} else if (this.isResourcePreloading(url)) {
return this.getResourcePreloadedPromise(url);
} else {
return this._preloadResource(url);
}
}
},
_preloadResource: {
value: function (url) {
var self = this,
promise = new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open("GET", url);
req.addEventListener("load", resolve, false);
req.addEventListener("error", resolve, false);
req.addEventListener("timeout", resolve, false);
req.timeout = self._SCRIPT_TIMEOUT;
req.send();
req.listener = resolve;
})
.bind(this)
.then(function loadHandler(event) {
this.setResourcePreloaded(url);
event.target.removeEventListener("load", event.target.listener);
event.target.removeEventListener("error", event.target.listener);
event.target.removeEventListener("timeout", event.target.listener);
});
this.setResourcePreloadedPromise(url, promise);
return promise;
}
},
areStylesLoaded: {
get: function () {
var styleSheets,
ix;
if (this._isPollingDocumentStyleSheets) {
if (this._expectedStyles.length > 0) {
styleSheets = this._document.styleSheets;
for (var i = 0, styleSheet; (styleSheet = styleSheets[i]); i++) {
ix = this._expectedStyles.indexOf(styleSheet.href);
if (ix >= 0) {
this._expectedStyles.splice(ix, 1);
}
}
}
}
return this._expectedStyles.length === 0;
}
}
}, {
getInstanceForDocument: {
value: function (_document) {
//jshint -W106
var documentResources = _document.__montage_resources__;
if (!documentResources) {
documentResources = _document.__montage_resources__ = new DocumentResources().initWithDocument(_document);
}
return documentResources;
//jshint +W106
}
}
});
exports.DocumentResources = DocumentResources;