/** MIT License (c) copyright 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 layer or as a way to apply a run-time theme. * Most 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. * (The HTML5 spec states that the LINK element should have a load event, but * not even Chrome 8 or FF4b7 have it, yet. * 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 the first time. 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, Feature-detecting the load * event is tricky since most browsers have a non-functional onload property. * * The universal work-around 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. * * Note #2: for Opera compatibility, stylesheets must have at least one rule. * AFAIK, there's no way to tell the difference between an empty sheet and * one that isn't finished loading in Opera (XD or same-domain). * * Options: * !nowait - does not wait for the stylesheet to be parsed, just loads it * * Global configuration options: * * cssDeferLoad: Boolean. You can also instruct this plugin to not wait * for css resources. They'll get loaded asap, but other code won't wait * for them. This is just like using the !nowait option on every css file. * * 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: * require('css!myproj/component.less') // --> myproj/component.less * require('css!myproj/component.scss') // --> myproj/component.scss * * 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 in the plugin. * * usage: * require(['css!myproj/comp']); // load and wait for myproj/comp.css * define(['css!some/folder/file'], {}); // wait for some/folder/file.css * require(['css!myWidget!nowait']); * * Tested in: * Firefox 1.5, 2.0, 3.0, 3.5, 3.6, and 4.0b6 * Safari 3.0.4, 3.2.1, 5.0 * Chrome 7 (8+ is partly b0rked) * Opera 9.52, 10.63, and Opera 11.00 * IE 6, 7, and 8 * Netscape 7.2 (WTF? SRSLY!) * Does not work in Safari 2.x :( * In Chrome 8+, there's no way to wait for cross-domain (XD) stylesheets. * See comments in the code below. * TODO: figure out how to be forward-compatible when browsers support HTML5's * load handler without breaking IE and Opera */ var // compressibility shortcuts onreadystatechange = 'onreadystatechange', onload = 'onload', createElement = 'createElement', // failed is true if RequireJS threw an exception failed = false, undef, insertedSheets = {}, features = { // true if the onload event handler works // "event-link-onload" : false }, // this actually tests for absolute urls and root-relative urls // they're both non-relative nonRelUrlRe = /^\/|^[^:]*:\/\//, // Note: this will fail if there are parentheses in the url findUrlRx = /url\s*\(['"]?([^'"\)]*)['"]?\)/g, // 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, // collection of modules that have been written to the built file built = {}; if (doc) { head = doc.head || (doc.head = doc.getElementsByTagName('head')[0]); } function has (feature) { return features[feature]; } /***** load-detection functions *****/ function loadHandler (params, cb) { // We're using 'readystatechange' because IE and Opera happily support both var link = params.link; link[onreadystatechange] = link[onload] = function () { if (!link.readyState || link.readyState == 'complete') { features["event-link-onload"] = true; cleanup(params); cb(); } }; } function nameWithExt (name, defaultExt) { return name.lastIndexOf('.') <= name.lastIndexOf('/') ? name + '.' + defaultExt : name; } function parseSuffixes (name) { // creates a dual-structure: both an array and a hashmap // suffixes[0] is the actual name var parts = name.split('!'), suf, i = 1, pair; while ((suf = parts[i++])) { // double-parens to avoid jslint griping pair = suf.split('=', 2); parts[pair[0]] = pair.length == 2 ? pair[1] : true; } return parts; } var collectorSheet; function createLink (doc, optHref) { // detect if we need to avoid 31-sheet limit in IE (how to detect this for realz?) if (document.createStyleSheet) { if (!collectorSheet) { collectorSheet = document.createStyleSheet(); } if (document.styleSheets.length >= 30) { moveLinksToCollector(); } } var link = doc[createElement]('link'); link.rel = "stylesheet"; link.type = "text/css"; link.setAttribute('_curl_movable', true); if (optHref) { link.href = optHref; } return link; } var testEl; function styleIsApplied () { // Chrome 8 hax0rs! // This is an ugly hack needed by Chrome 8+ which no longer waits for rules // to be applied to the document before exposing them to javascript. // Unfortunately, this routine will never fire for XD stylesheets since // Chrome will also throw an exception if attempting to access the rules // of an XD stylesheet. Therefore, there's no way to detect the load // event of XD stylesheets until Google fixes this, preferably with a // functional load event! As a work-around, use domReady() before // rendering widgets / components that need the css to be ready. if (!testEl) { testEl = doc[createElement]('div'); testEl.id = '_cssx_load_test'; head.appendChild(testEl); } return doc.defaultView.getComputedStyle(testEl, null).marginTop == '-5px'; } function isLinkReady (link) { // This routine is a bit fragile: browser vendors seem oblivious to // the need to know precisely when stylesheets load. Therefore, we need // to continually test beta browsers until they all support the LINK load // event like IE and Opera. var sheet, rules, ready = false; try { // webkit's and IE's sheet is null until the sheet is loaded sheet = link.sheet || link.styleSheet; // mozilla's sheet throws an exception if trying to access xd rules rules = sheet.cssRules || sheet.rules; // webkit's xd sheet returns rules == null // opera's sheet always returns rules, but length is zero until loaded // friggin IE doesn't count @import rules as rules, but IE should // never hit this routine anyways. ready = rules ? rules.length > 0 : // || (sheet.imports && sheet.imports.length > 0) : rules !== undef; // thanks, Chrome 8+, for this lovely hack. TODO: find a better way if (ready && {}.toString.call(window.chrome) == '[object Chrome]') { // fwiw, we'll never get this far if this is an XD stylesheet sheet.insertRule('#_cssx_load_test{margin-top:-5px;}', 0); ready = styleIsApplied(); sheet.deleteRule(0); } } catch (ex) { // 1000 means FF loaded an xd stylesheet // other browsers just throw a security error here (IE uses the phrase 'Access is denied') ready = (ex.code == 1000) || (ex.message.match(/security|denied/i)); } return ready; } function ssWatcher (params, cb) { // watches a stylesheet for loading signs. if (isLinkReady(params.link)) { cleanup(params); cb(); } else if (!failed) { setTimeout(function () { ssWatcher(params, cb); }, params.wait); } } function loadDetector (params, cb) { // It would be nice to use onload everywhere, but the onload handler // only works in IE and Opera. // Detecting it cross-browser is completely impossible, too, since // THE BROWSERS ARE LIARS! DON'T TELL ME YOU HAVE AN ONLOAD PROPERTY // IF IT DOESN'T DO ANYTHING! var loaded; function cbOnce () { if (!loaded) { loaded = true; cb(); } } loadHandler(params, cbOnce); if (!has("event-link-onload")) { ssWatcher(params, cbOnce); } } function cleanup (params) { var link = params.link; link[onreadystatechange] = link[onload] = null; } function moveLinksToCollector () { // IE 6-8 fails when over 31 sheets, so we collect them. // Note: this hack relies on proper cache headers. var link, links, collector, pos = 0; collector = collectorSheet; collectorSheet = null; // so a new one will be created links = document.getElementsByTagName('link'); while ((link = links[pos])) { if (link.getAttribute('_curl_movable')) { // move to the collectorSheet (note: bad cache directive will cause a re-download) collector.addImport(link.href); // remove from document link.parentNode && link.parentNode.removeChild(link); } else { // skip this sheet pos++; } } } /***** style element functions *****/ var currentStyle; function translateUrls (cssText, baseUrl) { return cssText.replace(findUrlRx, function (all, url) { return 'url("' + translateUrl(url, baseUrl) + '")'; }); } function translateUrl (url, parentPath) { // if this is a relative url if (!nonRelUrlRe.test(url)) { // append path onto it url = parentPath + url; } return url; } function createStyle (cssText) { clearTimeout(createStyle.debouncer); if (createStyle.accum) { createStyle.accum.push(cssText); } else { createStyle.accum = [cssText]; currentStyle = doc.createStyleSheet ? doc.createStyleSheet() : head.appendChild(doc.createElement('style')); } createStyle.debouncer = setTimeout(function () { // Note: IE 6-8 won't accept the W3C method for inserting css text var style, allCssText; style = currentStyle; currentStyle = undef; allCssText = createStyle.accum.join('\n'); createStyle.accum = undef; // for safari which chokes on @charset "UTF-8"; allCssText = allCssText.replace(/.+charset[^;]+;/g, ''); // TODO: hoist all @imports to the top of the file to follow w3c spec 'cssText' in style ? style.cssText = allCssText : style.appendChild(doc.createTextNode(allCssText)); }, 0); return currentStyle; } function createSheetProxy (sheet) { return { cssRules: function () { return sheet.cssRules || sheet.rules; }, insertRule: sheet.insertRule || function (text, index) { var parts = text.split(/\{|\}/g); sheet.addRule(parts[0], parts[1], index); return index; }, deleteRule: sheet.deleteRule || function (index) { sheet.removeRule(index); return index; }, sheet: function () { return sheet; } }; } /***** finally! the actual plugin *****/ define(/*=='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 resources = (resourceId || '').split(","), loadingCount = resources.length; // all detector functions must ensure that this function only gets // called once per stylesheet! function loaded () { // load/error handler may have executed before stylesheet is // fully parsed / processed in Opera, so use setTimeout. // Opera will process before the it next enters the event loop // (so 0 msec is enough time). if(--loadingCount == 0){ // TODO: move this setTimeout to loadHandler setTimeout(function () { callback(createSheetProxy(link.sheet || link.styleSheet)); }, 0); } } if (!resourceId) { // return the run-time API callback({ 'translateUrls': function (cssText, baseId) { var baseUrl; baseUrl = require['toUrl'](baseId); baseUrl = baseUrl.substr(0, baseUrl.lastIndexOf('/') + 1); return translateUrls(cssText, baseUrl); }, 'injectStyle': function (cssText) { return createStyle(cssText); }, 'proxySheet': function (sheet) { // for W3C, `sheet` is a reference to a