UNPKG

curl-amd

Version:

curl.js is small, fast, extensible module loader that handles AMD, CommonJS Modules/1.1, CSS, HTML/text, and legacy scripts.

581 lines (523 loc) 16.9 kB
/** MIT License (c) copyright 2010-2013 B Cavalier & J Hann */ /** * curl css! plugin * * Licensed under the MIT License at: * http://www.opensource.org/licenses/mit-license.php * */ (function (global) { "use strict"; /* * AMD css! plugin * This plugin will load and wait for css files. This could be handy when * loading css files as part of a component or a theme. * Some browsers do not support the load event handler of the link element. * Therefore, we have to use other means to detect when a css file loads. * Some browsers don't support the error event handler, either. * The HTML5 spec states that the LINK element should have both load and * error events: * http://www.w3.org/TR/html5/semantics.html#the-link-element * * This plugin tries to use the load event and a universal work-around when * it is invoked. If the load event works, it is used on every successive load. * Therefore, browsers that support the load event will just work (i.e. no * need for hacks!). FYI, sniffing for the load event is tricky * since most browsers still have a non-functional onload property. * * IE is a special case since it also has a 31-stylesheet limit (finally * fixed in IE 10). To get around this, we can use a set of <style> * elements instead of <link> elements and add @import; rules into them. * This allows us to add considerably more than 31 stylesheets. See the * comment for the loadImport method for more information. * * The universal work-around for other browsers watches a stylesheet * until its rules are available (not null or undefined). There are * nuances, of course, between the various browsers. The isLinkReady * function accounts for these. * * Note: it appears that all browsers load @import'ed stylesheets before * fully processing the rest of the importing stylesheet. Therefore, we * don't need to find and wait for any @import rules explicitly. They'll * be waited for implicitly. * * Global configuration options: * * cssNoWait: Boolean. You can instruct this plugin to not wait * for any css resources. They'll get loaded asap, but other code won't wait * for them. Note: you cannot use this option and use more than 31 * stylesheets in IE6-9! * * cssWatchPeriod: if direct load-detection techniques fail, this option * determines the msec to wait between brute-force checks for rules. The * default is 50 msec. * * You may specify an alternate file extension or no extension: * require('css!myproj/component.less') // --> myproj/component.less * require('css!myproj/component') // --> myproj/component.css * * When using alternative file extensions, be sure to serve the files from * the server with the correct mime type (text/css) or some browsers won't * parse them, causing an error. * * usage: * require(['css!myproj/comp.css']); // load and wait for myproj/comp.css * define(['css!some/folder/file'], {}); // wait for some/folder/file.css * require(['css!myWidget']); * * Tested in: * Firefox 3.6, 4.0, 11-16 * Safari 3.0.4, 3.2.1, 5.0 * Chrome 19 * Opera 11.62, 12.01 * IE 6-10 * Error handlers work in the following: * Firefox 12+ * Safari 6+ * Chrome 9+ * IE6-9 * Error handlers don't work in: * Opera 11.62, 12.01 * Firefox 3.6, 4.0 * IE 10 */ var // compressibility shortcuts createElement = 'createElement', parentNode = 'parentNode', setTimeout = global.setTimeout, // doc will be undefined during a build doc = global.document, // find the head element and set it to it's standard property if nec. head, // infer IE 6-9 // IE 10 still doesn't seem to have link.onerror support, // but it doesn't choke on >31 stylesheets at least! shouldCollectSheets = doc && doc.createStyleSheet && !(doc.documentMode >= 10), ieCollectorSheets = [], ieCollectorPool = [], ieCollectorQueue = [], ieMaxCollectorSheets = 12, loadSheet, msgHttp = 'HTTP or network error.', hasEvent = {}; if (doc) { head = doc.head || doc.getElementsByTagName('head')[0]; if (shouldCollectSheets) { loadSheet = loadImport; } else { loadSheet = loadLink; } } /** * Once we've absolutely determined if the current browser supports an * event handler, we set it with this function. * @private * @param event * @param hasNative */ function setLoadDetection (event, hasNative) { hasEvent[event] = hasEvent[event] || hasNative; } /** * Creates a link element. * IE6-9 don't use this function. * @private * @return {Element} */ function createLink () { var link; link = doc[createElement]('link'); link.rel = "stylesheet"; link.type = "text/css"; return link; } /***** load functions for compliant browsers *****/ /** * This is the load handler for compliant browsers. * Latest Chrome, Safari, Opera, FF, IE10 * @private * @param link * @param cb */ function loadHandler (link, cb) { link.onload = function () { // we know browser is compliant now! setLoadDetection('load', true); cb(); }; } /** * This is the load handler for compliant browsers. * Latest Chrome, Safari (not Opera 12 or IE10) * @private * @param link * @param cb */ function errorHandler (link, cb) { link.onerror = function () { // we know browser is compliant now! setLoadDetection('error', true); cb(); }; } /***** ie load functions *****/ /** * Loads a stylesheet via IE's addImport() method, which is the only * way to detect both onload and onerror in IE. If we create a "parent * stylesheet", we can addImport() other sheets into it. The tricky part * is that we have to load one sheet at a time and create a new onload * and onerror event for each one. (IE only fires an onload or onerror * function once, but if you replace the onload or onerror functions, * it'll fire the new ones if there's another load or error event. * Way to be awesome, IE team!) * * To get around the one-sheet-at-a-time problem, we create many * parent stylesheets at once. If we create 12 parent sheets, we can load * up to 12 imported sheets at once. This has an additional benefit: * we can load 372 (12 * 31) stylesheets. IE 6-9 can dynamically load only * 31 stylesheets in any one scope. By creating multiple parent sheets, we * create multiple scopes. * * The astute reader will have discovered a major flaw with this approach: * we've killed the cascade (the "C" in CSS). Rules in stylesheets override * rules in stylesheets that were declared earlier. This is universal. * However, the IE team interpreted the word "earlier" differently than * everybody else (including the w3c). IE interprets it as meaning "earlier * in time" (temporal), rather than "earlier in the document" (spacial). * Specifically, the temporal order of the insertion of the sheet into the * DOM/BOM is what matters in IE. * * In other words: the bungling of the IE team (both in allowing sheet * error handlers to execute multiple times and in forcing us to use * temporal order rather than dom order) has allowed us to implement * this work-around. Almost seems like they planned it. * * Note: CSS debugging tools in IE 6-8 seem to sometimes fail when inserting * stylesheets dynamically, no matter which method we use to insert them. * * IE 6-9 only. * @private * @param url {String} * @param cb {Function} * @param eb {Function} */ function loadImport (url, cb, eb) { var coll; // push stylesheet and callbacks on queue ieCollectorQueue.push({ url:url, cb:cb, eb: function failure () { eb(new Error(msgHttp)); } }); // find an available collector coll = getIeCollector(); // if we have an available collector, import a stylesheet from the queue if (coll) { loadNextImport(coll); } } /** * Grabs the next sheet/callback item from the queue and imports it into * the provided collector sheet. * IE 6-9 only. * @private * @param coll {Stylesheet} */ function loadNextImport (coll) { var imp, collSheet; imp = ieCollectorQueue.shift(); collSheet = coll.styleSheet; if (imp) { coll.onload = function () { imp.cb(imp.ss); loadNextImport(coll); }; coll.onerror = function () { imp.eb(); loadNextImport(coll); }; imp.ss = collSheet.imports[collSheet.addImport(imp.url)]; } else { finalize(coll); returnIeCollector(coll); } } /** * Returns a collector sheet to the pool. * IE 6-9 only. * @private * @param coll {Stylesheet} */ function returnIeCollector (coll) { ieCollectorPool.push(coll); } /** * Gets the next collector sheet in the pool. If there is no collector * in the pool and less than the maximum collector sheets has been created, * a new one is created. If the max collectors have been created, * undefined is returned. * IE 6-9 only. * @private * @return {HTMLElement} a stylesheet element to act as a collector sheet */ function getIeCollector () { var el; el = ieCollectorPool.shift(); if (!el && ieCollectorSheets.length < ieMaxCollectorSheets) { el = doc.createElement('style'); ieCollectorSheets.push(el); head.appendChild(el); } return el; } /***** load functions for legacy browsers (old Safari and FF, Opera) *****/ /** * Try all sorts of crazy shiz to determine when the stylesheet is loaded. * @private * @param link * @return {Boolean} */ function isLinkReady (link) { var ready, sheet, rules; // don't bother testing until we've fully initialized the link and doc. if (!link.href || !isDocumentComplete()) return false; ready = false; try { sheet = link.sheet; if (sheet) { // old FF will throw a security exception here when an XD // sheet is loaded. webkits (that don't support onload) // will return null when an XD sheet is loaded rules = sheet.cssRules; ready = rules === null; if (!ready && rules) { // Safari needs to further test for rule manipulation // on local stylesheets (Opera too?) sheet.insertRule('-curl-css-test {}', 0); sheet.deleteRule(0); ready = true; } } } catch (ex) { // a "security" or "access denied" error indicates that an XD // stylesheet has been successfully loaded in old FF // Opera throws before the sheet is loaded (and before onload // in some cases, so we have to test for it here) ready = Object.prototype.toString.call(window.opera) != '[object Opera]' && /security|denied/i.test(ex.message); } return ready; } /** * Indicate that a link element is loaded or errored. * @private * @param link */ function finalize (link) { // noop serves as a flag that a link event fired // note: Opera and IE won't clear handlers if we use a non-function link.onload = link.onerror = noop; } /** * Detect is a link is loaded or errored. * @private * @param link * @return {Boolean} */ function isFinalized (link) { return link.onload == noop || !link.onload; } /** * This is the function that will be used when a browser doesn't support * the standard onload and handler -- or until we've verified that the * browser supports it. * @private * @param link * @param wait {Number} msec between checks * @param cb */ function loadWatcher (link, wait, cb) { // watches a stylesheet for loading signs. if (hasEvent['load']) return; // always check on re-entry if (isLinkReady(link)) { cb(link.sheet); } else if (!isFinalized(link)) { setTimeout(function () { loadWatcher(link, wait, cb); }, wait); } } /** * This is the function that wil be used when a browser doens't support * the standard onerror handler -- or until we've verifies that the * browser supports it. * Note: so far we don't have a good way to check or failed stylesheets * without attempting to refetch the stylesheet using an IMG, OBJECT, * or any other means, so we haven't implemented this. * @private * @param link * @param wait {Number} msec between checks * @param eb */ function errorWatcher (link, wait, eb) { if (hasEvent['error']) return; // TODO: figure out a method to test for stylesheet failure without risk of re-fetching // TODO: timeout? } /** * Launch both standards-compliant onload and fallback at the same time. * One of these will eventually work. * @private * @param link * @param wait {Number} msec between checks * @param cb */ function linkLoaded (link, wait, cb) { // most browsers now support link.onload, but many older browsers // don't. Browsers that don't will launch the loadWatcher to repeatedly // test the link for readiness. function load () { // only executes once (link.onload is acting as a flag) if (isFinalized(link)) return; finalize(link); waitForDocumentComplete(function () { cb(link.sheet); }); } // always try standard handler loadHandler(link, load); // also try the fallback loadWatcher(link, wait, load); } /** * Launch both standards-compliant onerror and fallback at the same time. * One of these will eventually work in most browsers. * @private * @param link * @param wait {Number} msec between checks * @param cb */ function linkErrored (link, wait, cb) { // very few browsers (Chrome 19+ and FF9+ as of Apr 2012) have a // functional onerror handler (and those only detect 40X/50X http // errors, not parsing errors as per the w3c spec). // IE6-10 call onload when there's an http error. (nice, real nice) // this only matters in IE10 since IE6-9 use the addImport method // which does call onerror. function error () { // only executes once (link.onload is acting as a flag) if (isFinalized(link)) return; finalize(link); cb(new Error(msgHttp)); } // always try standard handler errorHandler(link, error); // if we are not sure if the native error event works, try the fallback errorWatcher(link, wait, error); } /** * Kick-start the load and detection process. * @private * @param url * @param cb * @param eb * @param period {Number} msec between checks */ function loadLink (url, cb, eb, period) { var link; link = createLink(); linkLoaded(link, period, cb); linkErrored(link, period, eb); link.href = url; head.appendChild(link); } /** * Keep checking for the document readyState to be "complete" since * Chrome doesn't apply the styles to the document until that time. * If we return before readyState == 'complete', Chrome may not have * applied the styles, yet. * Chrome only. * @private * @param cb */ function waitForDocumentComplete (cb) { // this isn't exactly the same as domReady (when dom can be // manipulated). it's later (when styles are applied). // chrome needs this (and opera?) function complete () { if (isDocumentComplete()) { cb(); } else { setTimeout(complete, 10); } } complete(); } /** * Returns true if the documents' readyState == 'complete' or the * document doesn't implement readyState. * Chrome only. * @private * @return {Boolean} */ function isDocumentComplete () { return !doc.readyState || doc.readyState == 'complete'; } function nameWithExt (name, defaultExt) { return name.lastIndexOf('.') <= name.lastIndexOf('/') ? name + '.' + defaultExt : name; } function noop () {} /***** finally! the actual plugin *****/ define(/*=='curl/plugin/css',==*/ { 'normalize': function (resourceId, normalize) { var resources, normalized; if (!resourceId) return resourceId; resources = resourceId.split(","); normalized = []; for (var i = 0, len = resources.length; i < len; i++) { normalized.push(normalize(resources[i])); } return normalized.join(','); }, 'load': function (resourceId, require, callback, config) { var sheets, resources, cssWatchPeriod, cssNoWait, loadingCount, i; sheets = []; resources = (resourceId || '').split(","); cssWatchPeriod = config['cssWatchPeriod'] || 50; cssNoWait = config['cssNoWait']; loadingCount = resources.length; // this function must get called just once per stylesheet! function loaded (ss) { if (resources.length > 1) sheets.push(ss); if (--loadingCount == 0) { callback(resources.length == 1 ? ss : sheets); } } function failed (ex) { var eb; eb = callback.reject || function (ex) { throw ex; }; eb(ex); } for (i = 0; i < resources.length; i++) { resourceId = resources[i]; var url, link; url = nameWithExt(require['toUrl'](resourceId), 'css'); if (cssNoWait) { link = createLink(); link.href = url; head.appendChild(link); loaded(link.sheet || link.styleSheet); } else { loadSheet(url, loaded, failed, cssWatchPeriod); } } }, 'cramPlugin': '../cram/css' }); })(this);