Stackedit/js/libs/css.js

437 lines
13 KiB
JavaScript
Raw Normal View History

2013-08-04 00:54:25 +00:00
/*
* Require-CSS RequireJS css! loader plugin
* Guy Bedford 2013
* MIT
*/
/*
*
* Usage:
* require(['css!./mycssFile']);
*
* NB leave out the '.css' extension.
*
* - Fully supports cross origin CSS loading
* - Works with builds
*
* Tested and working in (up to latest versions as of March 2013):
* Android
* iOS 6
* IE 6 - 10
* Chome 3 - 26
* Firefox 3.5 - 19
* Opera 10 - 12
*
* browserling.com used for virtual testing environment
*
* Credit to B Cavalier & J Hann for the elegant IE 6 - 9 hack.
*
* Sources that helped along the way:
* - https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent
* - http://www.phpied.com/when-is-a-stylesheet-really-loaded/
* - https://github.com/cujojs/curl/blob/master/src/curl/plugin/css.js
*
*/
define(['./normalize'], function(normalize) {
function indexOf(a, e) { for (var i=0, l=a.length; i < l; i++) if (a[i] === e) return i; return -1 }
if (typeof window == 'undefined')
return { load: function(n, r, load){ load() } };
// set to true to enable test prompts for device testing
var testing = false;
var head = document.getElementsByTagName('head')[0];
var engine = window.navigator.userAgent.match(/Trident\/([^ ;]*)|AppleWebKit\/([^ ;]*)|Opera\/([^ ;]*)|rv\:([^ ;]*)(.*?)Gecko\/([^ ;]*)|MSIE\s([^ ;]*)/);
var hackLinks = false;
if (!engine) {}
else if (engine[1] || engine[7]) {
hackLinks = parseInt(engine[1]) < 6 || parseInt(engine[7]) <= 9;
engine = 'trident';
}
else if (engine[2]) {
// unfortunately style querying still doesnt work with onload callback in webkit
hackLinks = true;
engine = 'webkit';
}
else if (engine[3]) {
// engine = 'opera';
}
else if (engine[4]) {
hackLinks = parseInt(engine[4]) < 18;
engine = 'gecko';
}
else if (testing)
alert('Engine detection failed');
//main api object
var cssAPI = {};
var absUrlRegEx = /^\/|([^\:\/]*:)/;
cssAPI.pluginBuilder = './css-builder';
// used by layer builds to register their css buffers
// the current layer buffer items (from addBuffer)
var curBuffer = [];
// the callbacks for buffer loads
var onBufferLoad = {};
// the full list of resources in the buffer
var bufferResources = [];
cssAPI.addBuffer = function(resourceId) {
// just in case layer scripts are included twice, also check
// against the previous buffers
if (indexOf(curBuffer, resourceId) != -1)
return;
if (indexOf(bufferResources, resourceId) != -1)
return;
curBuffer.push(resourceId);
bufferResources.push(resourceId);
}
cssAPI.setBuffer = function(css, isLess) {
var pathname = window.location.pathname.split('/');
pathname.pop();
pathname = pathname.join('/') + '/';
var baseParts = require.toUrl('base_url').split('/');
baseParts.pop();
var baseUrl = baseParts.join('/') + '/';
baseUrl = normalize.convertURIBase(baseUrl, pathname, '/');
if (!baseUrl.match(absUrlRegEx))
baseUrl = '/' + baseUrl;
if (baseUrl.substr(baseUrl.length - 1, 1) != '/')
baseUrl = baseUrl + '/';
cssAPI.inject(normalize(css, baseUrl, pathname));
// set up attach callback if registered
// clear the current buffer for the next layer
// (just the less or css part as we have two buffers in one effectively)
for (var i = 0; i < curBuffer.length; i++) {
// find the resources in the less or css buffer dependening which one this is
if ((isLess && curBuffer[i].substr(curBuffer[i].length - 5, 5) == '.less') ||
(!isLess && curBuffer[i].substr(curBuffer[i].length - 4, 4) == '.css')) {
(function(resourceId) {
// mark that the onBufferLoad is about to be called (set to true if not already a callback function)
onBufferLoad[resourceId] = onBufferLoad[resourceId] || true;
// set a short timeout (as injection isn't instant in Chrome), then call the load
setTimeout(function() {
if (typeof onBufferLoad[resourceId] == 'function')
onBufferLoad[resourceId]();
// remove from onBufferLoad to indicate loaded
delete onBufferLoad[resourceId];
}, 7);
})(curBuffer[i]);
// remove the current resource from the buffer
curBuffer.splice(i--, 1);
}
}
}
cssAPI.attachBuffer = function(resourceId, load) {
// attach can happen during buffer collecting, or between injection and callback
// we assume it is not possible to attach multiple callbacks
// requirejs plugin load function ensures this by queueing duplicate calls
// check if the resourceId is in the current buffer
for (var i = 0; i < curBuffer.length; i++)
if (curBuffer[i] == resourceId) {
onBufferLoad[resourceId] = load;
return true;
}
// check if the resourceId is waiting for injection callback
// (onBufferLoad === true is a shortcut indicator for this)
if (onBufferLoad[resourceId] === true) {
onBufferLoad[resourceId] = load;
return true;
}
// if it's in the full buffer list and not either of the above, its loaded already
if (indexOf(bufferResources, resourceId) != -1) {
load();
return true;
}
}
var webkitLoadCheck = function(link, callback) {
setTimeout(function() {
for (var i = 0; i < document.styleSheets.length; i++) {
var sheet = document.styleSheets[i];
if (sheet.href == link.href)
return callback();
}
webkitLoadCheck(link, callback);
}, 10);
}
var mozillaLoadCheck = function(style, callback) {
setTimeout(function() {
try {
style.sheet.cssRules;
return callback();
} catch (e){}
mozillaLoadCheck(style, callback);
}, 10);
}
// ie link detection, as adapted from https://github.com/cujojs/curl/blob/master/src/curl/plugin/css.js
if (engine == 'trident' && hackLinks) {
var ieStyles = [],
ieQueue = [],
ieStyleCnt = 0;
var ieLoad = function(url, callback) {
var style;
ieQueue.push({
url: url,
cb: callback
});
style = ieStyles.shift();
if (!style && ieStyleCnt++ < 31) {
style = document.createElement('style');
head.appendChild(style);
}
if (style)
ieLoadNextImport(style);
}
var ieLoadNextImport = function(style) {
var curImport = ieQueue.shift();
if (!curImport) {
style.onload = noop;
ieStyles.push(style);
return;
}
style.onload = function() {
curImport.cb(curImport.ss);
ieLoadNextImport(style);
};
var curSheet = style.styleSheet;
curImport.ss = curSheet.imports[curSheet.addImport(curImport.url)];
}
}
// uses the <link> load method
var createLink = function(url) {
var link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = url;
return link;
}
var noop = function(){}
cssAPI.linkLoad = function(url, callback) {
var timeout = setTimeout(function() {
if (testing) alert('timeout');
callback();
}, waitSeconds * 1000 - 100);
var _callback = function() {
clearTimeout(timeout);
if (link)
link.onload = noop;
// for style querying, a short delay still seems necessary
setTimeout(callback, 7);
}
if (!hackLinks) {
var link = createLink(url);
link.onload = _callback;
head.appendChild(link);
}
// hacks
else {
if (engine == 'webkit') {
var link = createLink(url);
webkitLoadCheck(link, _callback);
head.appendChild(link);
}
else if (engine == 'gecko') {
var style = document.createElement('style');
style.textContent = '@import "' + url + '"';
mozillaLoadCheck(style, _callback);
head.appendChild(style);
}
else if (engine == 'trident')
ieLoad(url, _callback);
}
}
/* injection api */
var progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
var fileCache = {};
var get = function(url, callback, errback) {
if (fileCache[url]) {
callback(fileCache[url]);
return;
}
var xhr, i, progId;
if (typeof XMLHttpRequest !== 'undefined')
xhr = new XMLHttpRequest();
else if (typeof ActiveXObject !== 'undefined')
for (i = 0; i < 3; i += 1) {
progId = progIds[i];
try {
xhr = new ActiveXObject(progId);
}
catch (e) {}
if (xhr) {
progIds = [progId]; // so faster next time
break;
}
}
xhr.open('GET', url, requirejs.inlineRequire ? false : true);
xhr.onreadystatechange = function (evt) {
var status, err;
//Do not explicitly handle errors, those should be
//visible via console output in the browser.
if (xhr.readyState === 4) {
status = xhr.status;
if (status > 399 && status < 600) {
//An http 4xx or 5xx error. Signal an error.
err = new Error(url + ' HTTP status: ' + status);
err.xhr = xhr;
errback(err);
}
else {
fileCache[url] = xhr.responseText;
callback(xhr.responseText);
}
}
};
xhr.send(null);
}
//uses the <style> load method
var styleCnt = 0;
var curStyle;
cssAPI.inject = function(css) {
if (styleCnt < 31) {
curStyle = document.createElement('style');
curStyle.type = 'text/css';
head.appendChild(curStyle);
styleCnt++;
}
if (curStyle.styleSheet)
curStyle.styleSheet.cssText += css;
else
curStyle.appendChild(document.createTextNode(css));
}
// NB add @media query support for media imports
var importRegEx = /@import\s*(url)?\s*(('([^']*)'|"([^"]*)")|\(('([^']*)'|"([^"]*)"|([^\)]*))\))\s*;?/g;
var pathname = window.location.pathname.split('/');
pathname.pop();
pathname = pathname.join('/') + '/';
var loadCSS = function(fileUrl, callback, errback) {
//make file url absolute
if (!fileUrl.match(absUrlRegEx))
fileUrl = '/' + normalize.convertURIBase(fileUrl, pathname, '/');
get(fileUrl, function(css) {
// normalize the css (except import statements)
css = normalize(css, fileUrl, pathname);
// detect all import statements in the css and normalize
var importUrls = [];
var importIndex = [];
var importLength = [];
var match;
while (match = importRegEx.exec(css)) {
var importUrl = match[4] || match[5] || match[7] || match[8] || match[9];
importUrls.push(importUrl);
importIndex.push(importRegEx.lastIndex - match[0].length);
importLength.push(match[0].length);
}
// load the import stylesheets and substitute into the css
var completeCnt = 0;
for (var i = 0; i < importUrls.length; i++)
(function(i) {
loadCSS(importUrls[i], function(importCSS) {
css = css.substr(0, importIndex[i]) + importCSS + css.substr(importIndex[i] + importLength[i]);
var lenDiff = importCSS.length - importLength[i];
for (var j = i + 1; j < importUrls.length; j++)
importIndex[j] += lenDiff;
completeCnt++;
if (completeCnt == importUrls.length) {
callback(css);
}
}, errback);
})(i);
if (importUrls.length == 0)
callback(css);
}, errback);
}
cssAPI.normalize = function(name, normalize) {
if (name.substr(name.length - 4, 4) == '.css')
name = name.substr(0, name.length - 4);
return normalize(name);
}
var waitSeconds;
var alerted = false;
cssAPI.load = function(cssId, req, load, config, parse) {
waitSeconds = waitSeconds || config.waitSeconds || 7;
var resourceId = cssId + (!parse ? '.css' : '.less');
// attach the load function to a buffer if there is one in registration
// if not, we do a full injection load
if (cssAPI.attachBuffer(resourceId, load))
return;
var fileUrl = req.toUrl(resourceId);
if (!alerted && testing) {
alert(hackLinks ? 'hacking links' : 'not hacking');
alerted = true;
}
if (!parse) {
cssAPI.linkLoad(fileUrl, load);
}
else {
loadCSS(fileUrl, function(css) {
// run parsing after normalization - since less is a CSS subset this works fine
if (parse)
css = parse(css, function(css) {
cssAPI.inject(css);
setTimeout(load, 7);
});
});
}
}
if (testing)
cssAPI.inspect = function() {
if (stylesheet.styleSheet)
return stylesheet.styleSheet.cssText;
else if (stylesheet.innerHTML)
return stylesheet.innerHTML;
}
return cssAPI;
});