css.js 13 KB

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