Source: core/document-resources.js

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;