dev4web
12/25/2016 - 11:34 PM

CSS Variable Polyfill

CSS Variable Polyfill

var p={t:function(){window.CSS&&window.CSS.supports&&window.CSS.supports("(--foo: red)")||(p.v={},p.b={},p.o={},p.f(),p.u())},f:function(){var a=document.querySelectorAll('style:not([id*="inserted"]),link[type="text/css"],link[rel="stylesheet"]'),b=1;[].forEach.call(a,function(a){var c;"STYLE"===a.nodeName?(c=a.innerHTML,p.i(a.innerHTML,function(c){a.innerHTML=c,p.s(c,b)})):"LINK"===a.nodeName&&(p.g(a.getAttribute("href"),b,function(a,b){p.i(b.responseText,function(b){p.s(b,a),p.o[a]=b,p.u()})}),c=""),p.o[b]=c,b++})},s:function(a,b){p.b[b]=a.match(/([^var\/\*])--[a-zA-Z0-9\-]+:(\s?)(.+?);/gim)},u:function(){p.r(p.b);for(var a in p.o){var b=p.c(p.o[a],p.v);if(document.querySelector("#inserted"+a))document.querySelector("#inserted"+a).innerHTML=b;else{var c=document.createElement("style");c.innerHTML=b,c.id="inserted"+a,document.getElementsByTagName("head")[0].appendChild(c)}}},c:function(a,b){for(var c in b){var d=new RegExp("var\\(\\s*?"+c.trim()+"(\\)|,([\\s\\,\\w]*\\)))","g");a=a.replace(d,b[c])}for(var e=/var\(\s*?([^)^,\.]*)(?:[\s\,])*([^)\.]*)\)/g;match=e.exec(a);)a=a.replace(match[0],match[2]);return a},r:function(a){for(var b in a){var c=a[b];c.forEach(function(a){var b=a.split(/:\s*/);p.v[b[0]]=b[1].replace(/;/,"")})}},g:function(a,b,c){var d=new XMLHttpRequest;d.open("GET",a,!0),d.overrideMimeType("text/css;"),d.onload=function(){d.status>=200&&d.status<400?"function"==typeof c&&c(b,d,a):console.warn("an error was returned from:",a)},d.onerror=function(){console.warn("we could not get anything from:",a)},d.send()},i:function(a,b){var d,c=/^(?![\/\*])@import\s.*(?:url\(["'])([a-zA-Z0-9\.:\/_-]*)["']\)?([^;]*);/gim,e=0,f=0,g={};for(a.search(c)===-1&&b(a);d=c.exec(a);)g[d[1]]=d,e++,p.g(d[1],null,function(c,d,h){a=a.replace(g[h][0],(g[h][2].trim()?"@media "+g[h][2].trim()+" {":"")+d.responseText+(g[h][2].trim()?"}":"")),f++,f===e&&b(a)})}};p.t();
/*
TODO:
- Option to wait to apply anything until all <link>s are parsed or inject what we have and update as each <link> returns
*/
var cssVarPoly = {
  init: function() {
    if (window.CSS && window.CSS.supports && window.CSS.supports('(--foo: red)')) {
      return;
    }
    cssVarPoly.ratifiedVars = {};
    cssVarPoly.varsByBlock = {};
    cssVarPoly.oldCSS = {};

    // start things off
    cssVarPoly.findCSS();
    cssVarPoly.updateCSS();
  },

  // find all the css blocks, save off the content, and look for variables
  findCSS: function() {
    var styleBlocks = document.querySelectorAll('style:not([id*="inserted"]),link[type="text/css"],link[rel="stylesheet"]');

    // we need to track the order of the style/link elements when we save off the CSS, set a counter
    var counter = 1;

    // loop through all CSS blocks looking for CSS variables being set
    [].forEach.call(styleBlocks, function(block) {
      var theCSS;
      if (block.nodeName === 'STYLE') {
        theCSS = block.innerHTML;
        cssVarPoly.getImports(block.innerHTML, function(theCSS){
          block.innerHTML = theCSS;
          cssVarPoly.findSetters(theCSS, counter);
        });
      } else if (block.nodeName === 'LINK') {
        cssVarPoly.getLink(block.getAttribute('href'), counter, function(counter, request) {
          cssVarPoly.getImports(request.responseText, function(respCSS){
            cssVarPoly.findSetters(respCSS, counter);
            cssVarPoly.oldCSS[counter] = respCSS;
            cssVarPoly.updateCSS();
          });
        });
        theCSS = '';
      }
      // save off the CSS to parse through again later. the value may be empty for links that are waiting for their ajax return, but this will maintain the order
      cssVarPoly.oldCSS[counter] = theCSS;
      counter++;
    });
  },

  // find all the "--variable: value" matches in a provided block of CSS and add them to the master list
  findSetters: function(theCSS, counter) {
    cssVarPoly.varsByBlock[counter] = theCSS.match(/([^var\/\*])--[a-zA-Z0-9\-]+:(\s?)(.+?);/gmi);
  },

  // run through all the CSS blocks to update the variables and then inject on the page
  updateCSS: function() {
    // first lets loop through all the variables to make sure later vars trump earlier vars
    cssVarPoly.ratifySetters(cssVarPoly.varsByBlock);
    // loop through the css blocks (styles and links)
    for (var curCSSID in cssVarPoly.oldCSS) {
      var newCSS = cssVarPoly.replaceGetters(cssVarPoly.oldCSS[curCSSID], cssVarPoly.ratifiedVars);
      // put it back into the page
      // first check to see if this block exists already
      if (document.querySelector('#inserted' + curCSSID)) {
        document.querySelector('#inserted' + curCSSID).innerHTML = newCSS;
      } else {
        var style = document.createElement('style');
        style.innerHTML = newCSS;
        style.id = 'inserted' + curCSSID;
        document.getElementsByTagName('head')[0].appendChild(style);
      }
    };
  },

  // parse a provided block of CSS looking for a provided list of variables and replace the --var-name with the correct value
  replaceGetters: function(curCSS, varList) {
    for (var theVar in varList) {
      // match the variable with the actual variable name
      var getterRegex = new RegExp('var\\(\\s*?' + theVar.trim() + '(\\)|,([\\s\\,\\w]*\\)))', 'g');
      curCSS = curCSS.replace(getterRegex, varList[theVar]);
    };
    // now check for any getters that are left that have fallbacks
    var getterRegex2 = /var\(\s*?([^)^,\.]*)(?:[\s\,])*([^)\.]*)\)/g;
    while (match = getterRegex2.exec(curCSS)) {
      // find the fallback within the getter
      curCSS = curCSS.replace(match[0], match[2]);
    }
    return curCSS;
  },

  // determine the css variable name value pair and track the latest
  ratifySetters: function(varList) {
    // loop through each block in order, to maintain order specificity
    for (var curBlock in varList) {
      var curVars = varList[curBlock];
      // loop through each var in the block
      curVars.forEach(function(theVar) {
        // split on the name value pair separator
        var matches = theVar.split(/:\s*/);
        // put it in an object based on the varName. Each time we do this it will override a previous use and so will always have the last set be the winner
        // 0 = the name, 1 = the value, strip off the ; if it is there
        cssVarPoly.ratifiedVars[matches[0]] = matches[1].replace(/;/, '');
      });
    };
  },

  // get the CSS file
  getLink: function(url, counter, success) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.overrideMimeType('text/css;');
    request.onload = function() {
      if (request.status >= 200 && request.status < 400) {
        if (typeof success === 'function') {
          success(counter, request, url);
        }
      } else {
        // We reached our target server, but it returned an error
        console.warn('an error was returned from:', url);
      }
    };
    request.onerror = function() {
      // There was a connection error of some sort
      console.warn('we could not get anything from:', url);
    };
    request.send();
  },

  getImports: function(css, callback){
    var reg = /^(?![\/\*])@import\s.*(?:url\(["'])([a-zA-Z0-9\.:\/_-]*)["']\)?([^;]*);/gim,
      matches,
      todo = 0,
      done = 0,
      replacements = {};
    if(css.search(reg) === -1){callback(css)}
    while (matches = reg.exec(css)) {
      replacements[matches[1]] = matches;
      todo++;
      cssVarPoly.getLink(matches[1], null, function(counter, request, url){
        css = css.replace(replacements[url][0], (replacements[url][2].trim() ? ('@media ' + replacements[url][2].trim() + ' {') : '') + request.responseText + (replacements[url][2].trim() ? '}' : ''));
        done++;
        if(done === todo){callback(css)}
      });
    }
  }
};
cssVarPoly.init();