css.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. /*
  2. * Require-CSS RequireJS css! loader plugin
  3. * 0.0.8
  4. * Guy Bedford 2013
  5. * MIT
  6. */
  7. /*
  8. *
  9. * Usage:
  10. * require(['css!./mycssFile']);
  11. *
  12. * NB leave out the '.css' extension.
  13. *
  14. * - Fully supports cross origin CSS loading
  15. * - Works with builds
  16. *
  17. * Tested and working in (up to latest versions as of March 2013):
  18. * Android
  19. * iOS 6
  20. * IE 6 - 10
  21. * Chome 3 - 26
  22. * Firefox 3.5 - 19
  23. * Opera 10 - 12
  24. *
  25. * browserling.com used for virtual testing environment
  26. *
  27. * Credit to B Cavalier & J Hann for the elegant IE 6 - 9 hack.
  28. *
  29. * Sources that helped along the way:
  30. * - https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent
  31. * - http://www.phpied.com/when-is-a-stylesheet-really-loaded/
  32. * - https://github.com/cujojs/curl/blob/master/src/curl/plugin/css.js
  33. *
  34. */
  35. define(['./normalize'], function(normalize) {
  36. function indexOf(a, e) { for (var i=0, l=a.length; i < l; i++) if (a[i] === e) return i; return -1 }
  37. if (typeof window == 'undefined')
  38. return { load: function(n, r, load){ load() } };
  39. // set to true to enable test prompts for device testing
  40. var testing = false;
  41. var head = document.getElementsByTagName('head')[0];
  42. var engine = window.navigator.userAgent.match(/Trident\/([^ ;]*)|AppleWebKit\/([^ ;]*)|Opera\/([^ ;]*)|rv\:([^ ;]*)(.*?)Gecko\/([^ ;]*)|MSIE\s([^ ;]*)/);
  43. var hackLinks = false;
  44. if (!engine) {}
  45. else if (engine[1] || engine[7]) {
  46. hackLinks = parseInt(engine[1]) < 6 || parseInt(engine[7]) <= 9;
  47. engine = 'trident';
  48. }
  49. else if (engine[2]) {
  50. // unfortunately style querying still doesnt work with onload callback in webkit
  51. hackLinks = true;
  52. engine = 'webkit';
  53. }
  54. else if (engine[3]) {
  55. // engine = 'opera';
  56. }
  57. else if (engine[4]) {
  58. hackLinks = parseInt(engine[4]) < 18;
  59. engine = 'gecko';
  60. }
  61. else if (testing)
  62. alert('Engine detection failed');
  63. //main api object
  64. var cssAPI = {};
  65. var absUrlRegEx = /^\/|([^\:\/]*:)/;
  66. cssAPI.pluginBuilder = './css-builder';
  67. // used by layer builds to register their css buffers
  68. // the current layer buffer items (from addBuffer)
  69. var curBuffer = [];
  70. // the callbacks for buffer loads
  71. var onBufferLoad = {};
  72. // the full list of resources in the buffer
  73. var bufferResources = [];
  74. cssAPI.addBuffer = function(resourceId) {
  75. // just in case layer scripts are included twice, also check
  76. // against the previous buffers
  77. if (indexOf(curBuffer, resourceId) != -1)
  78. return;
  79. if (indexOf(bufferResources, resourceId) != -1)
  80. return;
  81. curBuffer.push(resourceId);
  82. bufferResources.push(resourceId);
  83. }
  84. cssAPI.setBuffer = function(css, isLess) {
  85. var pathname = window.location.pathname.split('/');
  86. pathname.pop();
  87. pathname = pathname.join('/') + '/';
  88. var baseParts = require.toUrl('base_url').split('/');
  89. baseParts.pop();
  90. var baseUrl = baseParts.join('/') + '/';
  91. baseUrl = normalize.convertURIBase(baseUrl, pathname, '/');
  92. if (!baseUrl.match(absUrlRegEx))
  93. baseUrl = '/' + baseUrl;
  94. if (baseUrl.substr(baseUrl.length - 1, 1) != '/')
  95. baseUrl = baseUrl + '/';
  96. cssAPI.inject(normalize(css, baseUrl, pathname));
  97. // set up attach callback if registered
  98. // clear the current buffer for the next layer
  99. // (just the less or css part as we have two buffers in one effectively)
  100. for (var i = 0; i < curBuffer.length; i++) {
  101. // find the resources in the less or css buffer dependening which one this is
  102. if ((isLess && curBuffer[i].substr(curBuffer[i].length - 5, 5) == '.less') ||
  103. (!isLess && curBuffer[i].substr(curBuffer[i].length - 4, 4) == '.css')) {
  104. (function(resourceId) {
  105. // mark that the onBufferLoad is about to be called (set to true if not already a callback function)
  106. onBufferLoad[resourceId] = onBufferLoad[resourceId] || true;
  107. // set a short timeout (as injection isn't instant in Chrome), then call the load
  108. setTimeout(function() {
  109. if (typeof onBufferLoad[resourceId] == 'function')
  110. onBufferLoad[resourceId]();
  111. // remove from onBufferLoad to indicate loaded
  112. delete onBufferLoad[resourceId];
  113. }, 7);
  114. })(curBuffer[i]);
  115. // remove the current resource from the buffer
  116. curBuffer.splice(i--, 1);
  117. }
  118. }
  119. }
  120. cssAPI.attachBuffer = function(resourceId, load) {
  121. // attach can happen during buffer collecting, or between injection and callback
  122. // we assume it is not possible to attach multiple callbacks
  123. // requirejs plugin load function ensures this by queueing duplicate calls
  124. // check if the resourceId is in the current buffer
  125. for (var i = 0; i < curBuffer.length; i++)
  126. if (curBuffer[i] == resourceId) {
  127. onBufferLoad[resourceId] = load;
  128. return true;
  129. }
  130. // check if the resourceId is waiting for injection callback
  131. // (onBufferLoad === true is a shortcut indicator for this)
  132. if (onBufferLoad[resourceId] === true) {
  133. onBufferLoad[resourceId] = load;
  134. return true;
  135. }
  136. // if it's in the full buffer list and not either of the above, its loaded already
  137. if (indexOf(bufferResources, resourceId) != -1) {
  138. load();
  139. return true;
  140. }
  141. }
  142. var webkitLoadCheck = function(link, callback) {
  143. setTimeout(function() {
  144. for (var i = 0; i < document.styleSheets.length; i++) {
  145. var sheet = document.styleSheets[i];
  146. if (sheet.href == link.href)
  147. return callback();
  148. }
  149. webkitLoadCheck(link, callback);
  150. }, 10);
  151. }
  152. var mozillaLoadCheck = function(style, callback) {
  153. setTimeout(function() {
  154. try {
  155. style.sheet.cssRules;
  156. return callback();
  157. } catch (e){}
  158. mozillaLoadCheck(style, callback);
  159. }, 10);
  160. }
  161. // ie link detection, as adapted from https://github.com/cujojs/curl/blob/master/src/curl/plugin/css.js
  162. if (engine == 'trident' && hackLinks) {
  163. var ieStyles = [],
  164. ieQueue = [],
  165. ieStyleCnt = 0;
  166. var ieLoad = function(url, callback) {
  167. var style;
  168. ieQueue.push({
  169. url: url,
  170. cb: callback
  171. });
  172. style = ieStyles.shift();
  173. if (!style && ieStyleCnt++ < 31) {
  174. style = document.createElement('style');
  175. head.appendChild(style);
  176. }
  177. if (style)
  178. ieLoadNextImport(style);
  179. }
  180. var ieLoadNextImport = function(style) {
  181. var curImport = ieQueue.shift();
  182. if (!curImport) {
  183. style.onload = noop;
  184. ieStyles.push(style);
  185. return;
  186. }
  187. style.onload = function() {
  188. curImport.cb(curImport.ss);
  189. ieLoadNextImport(style);
  190. };
  191. var curSheet = style.styleSheet;
  192. curImport.ss = curSheet.imports[curSheet.addImport(curImport.url)];
  193. }
  194. }
  195. // uses the <link> load method
  196. var createLink = function(url) {
  197. var link = document.createElement('link');
  198. link.type = 'text/css';
  199. link.rel = 'stylesheet';
  200. link.href = url;
  201. return link;
  202. }
  203. var noop = function(){}
  204. cssAPI.linkLoad = function(url, callback) {
  205. var timeout = setTimeout(function() {
  206. if (testing) alert('timeout');
  207. callback();
  208. }, waitSeconds * 1000 - 100);
  209. var _callback = function() {
  210. clearTimeout(timeout);
  211. if (link)
  212. link.onload = noop;
  213. // for style querying, a short delay still seems necessary
  214. setTimeout(callback, 7);
  215. }
  216. if (!hackLinks) {
  217. var link = createLink(url);
  218. link.onload = _callback;
  219. head.appendChild(link);
  220. }
  221. // hacks
  222. else {
  223. if (engine == 'webkit') {
  224. var link = createLink(url);
  225. webkitLoadCheck(link, _callback);
  226. head.appendChild(link);
  227. }
  228. else if (engine == 'gecko') {
  229. var style = document.createElement('style');
  230. style.textContent = '@import "' + url + '"';
  231. mozillaLoadCheck(style, _callback);
  232. head.appendChild(style);
  233. }
  234. else if (engine == 'trident')
  235. ieLoad(url, _callback);
  236. }
  237. }
  238. /* injection api */
  239. var progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
  240. var fileCache = {};
  241. var get = function(url, callback, errback) {
  242. if (fileCache[url]) {
  243. callback(fileCache[url]);
  244. return;
  245. }
  246. var xhr, i, progId;
  247. if (typeof XMLHttpRequest !== 'undefined')
  248. xhr = new XMLHttpRequest();
  249. else if (typeof ActiveXObject !== 'undefined')
  250. for (i = 0; i < 3; i += 1) {
  251. progId = progIds[i];
  252. try {
  253. xhr = new ActiveXObject(progId);
  254. }
  255. catch (e) {}
  256. if (xhr) {
  257. progIds = [progId]; // so faster next time
  258. break;
  259. }
  260. }
  261. xhr.open('GET', url, requirejs.inlineRequire ? false : true);
  262. xhr.onreadystatechange = function (evt) {
  263. var status, err;
  264. //Do not explicitly handle errors, those should be
  265. //visible via console output in the browser.
  266. if (xhr.readyState === 4) {
  267. status = xhr.status;
  268. if (status > 399 && status < 600) {
  269. //An http 4xx or 5xx error. Signal an error.
  270. err = new Error(url + ' HTTP status: ' + status);
  271. err.xhr = xhr;
  272. errback(err);
  273. }
  274. else {
  275. fileCache[url] = xhr.responseText;
  276. callback(xhr.responseText);
  277. }
  278. }
  279. };
  280. xhr.send(null);
  281. }
  282. //uses the <style> load method
  283. var styleCnt = 0;
  284. var curStyle;
  285. cssAPI.inject = function(css) {
  286. if (styleCnt < 31) {
  287. curStyle = document.createElement('style');
  288. curStyle.type = 'text/css';
  289. head.appendChild(curStyle);
  290. styleCnt++;
  291. }
  292. if (curStyle.styleSheet)
  293. curStyle.styleSheet.cssText += css;
  294. else
  295. curStyle.appendChild(document.createTextNode(css));
  296. }
  297. // NB add @media query support for media imports
  298. var importRegEx = /@import\s*(url)?\s*(('([^']*)'|"([^"]*)")|\(('([^']*)'|"([^"]*)"|([^\)]*))\))\s*;?/g;
  299. var pathname = window.location.pathname.split('/');
  300. pathname.pop();
  301. pathname = pathname.join('/') + '/';
  302. var loadCSS = function(fileUrl, callback, errback) {
  303. //make file url absolute
  304. if (!fileUrl.match(absUrlRegEx))
  305. fileUrl = '/' + normalize.convertURIBase(fileUrl, pathname, '/');
  306. get(fileUrl, function(css) {
  307. // normalize the css (except import statements)
  308. css = normalize(css, fileUrl, pathname);
  309. // detect all import statements in the css and normalize
  310. var importUrls = [];
  311. var importIndex = [];
  312. var importLength = [];
  313. var match;
  314. while (match = importRegEx.exec(css)) {
  315. var importUrl = match[4] || match[5] || match[7] || match[8] || match[9];
  316. importUrls.push(importUrl);
  317. importIndex.push(importRegEx.lastIndex - match[0].length);
  318. importLength.push(match[0].length);
  319. }
  320. // load the import stylesheets and substitute into the css
  321. var completeCnt = 0;
  322. for (var i = 0; i < importUrls.length; i++)
  323. (function(i) {
  324. loadCSS(importUrls[i], function(importCSS) {
  325. css = css.substr(0, importIndex[i]) + importCSS + css.substr(importIndex[i] + importLength[i]);
  326. var lenDiff = importCSS.length - importLength[i];
  327. for (var j = i + 1; j < importUrls.length; j++)
  328. importIndex[j] += lenDiff;
  329. completeCnt++;
  330. if (completeCnt == importUrls.length) {
  331. callback(css);
  332. }
  333. }, errback);
  334. })(i);
  335. if (importUrls.length == 0)
  336. callback(css);
  337. }, errback);
  338. }
  339. cssAPI.normalize = function(name, normalize) {
  340. if (name.substr(name.length - 4, 4) == '.css')
  341. name = name.substr(0, name.length - 4);
  342. return normalize(name);
  343. }
  344. var waitSeconds;
  345. var alerted = false;
  346. cssAPI.load = function(cssId, req, load, config, parse) {
  347. waitSeconds = waitSeconds || config.waitSeconds || 7;
  348. var resourceId = cssId + (!parse ? '.css' : '.less');
  349. // attach the load function to a buffer if there is one in registration
  350. // if not, we do a full injection load
  351. if (cssAPI.attachBuffer(resourceId, load))
  352. return;
  353. var fileUrl = req.toUrl(resourceId);
  354. if (!alerted && testing) {
  355. alert(hackLinks ? 'hacking links' : 'not hacking');
  356. alerted = true;
  357. }
  358. if (!parse) {
  359. cssAPI.linkLoad(fileUrl, load);
  360. }
  361. else {
  362. loadCSS(fileUrl, function(css) {
  363. // run parsing after normalization - since less is a CSS subset this works fine
  364. if (parse)
  365. css = parse(css, function(css) {
  366. cssAPI.inject(css);
  367. setTimeout(load, 7);
  368. });
  369. });
  370. }
  371. }
  372. if (testing)
  373. cssAPI.inspect = function() {
  374. if (stylesheet.styleSheet)
  375. return stylesheet.styleSheet.cssText;
  376. else if (stylesheet.innerHTML)
  377. return stylesheet.innerHTML;
  378. }
  379. return cssAPI;
  380. });