Falci
6/8/2016 - 10:45 PM

CSS Variables Polyfill

CSS Variables Polyfill

root {
  --externalcolor: red;
  --samename: orange;
  --samename: #0f0;
  --foo: green;
  --FOO: #0f0;
  --halfsuccess: orange;
  --success: green;
  --success2: #0f0;
}
html {
  font-family: var(--fontsans);
}
.success {
  color: green;
}

.fail {
  color: red;
}

span {
  display: inline-block;
  margin: 5px;
}

.samename {
  color: var(--samename);
}

.demo1 {
  color: #f00;
  color: var(--success);
}

.demo2 {
  color: #f00;
  color: var( --success2);
}

.demo3 {
  color: #f00;
  color: var(--halfsuccess);
  color: var(--success);
}

.demo4 {
  color: red;
  border-color: #f00;
}

.inlineoverlink {
  color: #f00;
}

p {
  padding: var(--spacing-l);
}

.lower {
  color: var(--foo);
}

.upper {
  color: var(--FOO);
}

.externalcolor {
  color: var(--externalcolor);
}

.fallback {
  color: #f00;
  color: var(--wrongname, green);
}

// for the top documentation
.supports {
  color: green;
  .no {
    display:none;
  }
}
.showforpolyfill {
  display:none;
}

.cssvars-polyfilled {
  .supports {
    color: red;
    .no {
      display:inline;
    }
  }
  .showforpolyfill {
    display:inline;
  }
  .hideforpolyfill {
    display:none;
  }
} 
.hide,
.hide-the-docs .documentation {
  display:none;
}
/* declare some font-family stuff at bottom of file to reflect on stuff above it*/

:root {
  --fontsans: arial;
}
root {  
  --externalcolor:red;
  --samename: red;
  --samename: #0f0;
  --foo:green;
  --FOO:#0f0;
  --halfsuccess:orange;
  --success:green;
  --success2:#0f0;
}
.success {
  color:green;
}
.fail {
  color:red;
}

span {
  display:inline-block;
  margin:5px;
}

.samename {
  color:var(--samename);
}
.demo1 {
  color: #f00;
  color:var(--success);
}
.demo2 {
  color: #f00;
  color:var(    --success2          );
}
.demo3 {
  color: #f00;
  color: var(--halfsuccess);
  color: var(--success);
}
.demo4 {
  color: red;
  border-color:#f00;
}
.inlineoverlink {
  color: #f00;
}
p {
  padding: var(--spacing-l);
}
.lower {
  color: var(--foo);
}
.upper {
  color: var(--FOO);
}
.externalcolor {
  color: var(--externalcolor);
}

.fallback {
  color: #f00;
  color: var(--wrongname, green);
}
/*
need to write test cases for:
- foo vs FOO (case sensitive)

*/
/* declare some font-family stuff at bottom of file*/
:root {  
  --fontsans: arial;
}
/*
TODO:
X Maybe account for defaults: color: var(--header-color, blue);
- Verify cross domain working or not (it is working from dropbox)
- Option to wait to apply anything until all <link>s are parsed or inject what we have and update as each <link> returns
- Need to test on a more complex CSS file
- Option to save parsed file in local/session storage so there isn't a delay on additional page loads. Could only do it for links (with URLs to use as keys) and style blocks with IDs of some sort
- Need to test more complex values like rgba(255,0,0,0.5); and something with !important
- Try multiple links
- Local links
- Ajax driven site, or CSS added later the top of the stack
*/
let cssVarPoly = {
  init() {
    // first lets see if the browser supports CSS variables
    // No version of IE supports window.CSS.supports, so if that isn't supported in the first place we know CSS variables is not supported
    // Edge supports supports, so check for actual variable support
    if (window.CSS && window.CSS.supports && window.CSS.supports('(--foo: red)')) {
      // this browser does support variables, abort
      console.log('your browser supports CSS variables, aborting and letting the native support handle things.');
      return;
    } else {
      // edge barfs on console statements if the console is not open... lame!
      console.log('no support for you! polyfill all (some of) the things!!');
      document.querySelector('body').classList.add('cssvars-polyfilled');
    }

    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() {
    let styleBlocks = document.querySelectorAll('style:not(.inserted),link[type="text/css"]');

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

    // loop through all CSS blocks looking for CSS variables being set
    [].forEach.call(styleBlocks, function(block) {
      // console.log(block.nodeName);
      let theCSS;
      if (block.nodeName === 'STYLE') {
        // console.log("style");
        theCSS = block.innerHTML;
        cssVarPoly.findSetters(theCSS, counter);
      } else if (block.nodeName === 'LINK') {
        // console.log("link");
        cssVarPoly.getLink(block.getAttribute('href'), counter, function(counter, request) {
          cssVarPoly.findSetters(request.responseText, counter);
          cssVarPoly.oldCSS[counter] = request.responseText;
          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(theCSS, counter) {
    // console.log(theCSS);
    cssVarPoly.varsByBlock[counter] = theCSS.match(/(--.+:.+;)/g);
  },

  // run through all the CSS blocks to update the variables and then inject on the page
  updateCSS() {
    // 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 (let curCSSID in cssVarPoly.oldCSS) {
      // console.log("curCSS:",oldCSS[curCSSID]);
      let 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)) {
        // console.log("updating")
        document.querySelector('#inserted' + curCSSID).innerHTML = newCSS;
      } else {
        // console.log("adding");
        var style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = newCSS;
        style.classList.add('inserted');
        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(curCSS, varList) {
    // console.log(varList);
    for (let theVar in varList) {
      // console.log(theVar);
      // match the variable with the actual variable name
      let getterRegex = new RegExp('var\\(\\s*' + theVar + '\\s*\\)', 'g');
      // console.log(getterRegex);
      // console.log(curCSS);
      curCSS = curCSS.replace(getterRegex, varList[theVar]);

      // now check for any getters that are left that have fallbacks
      let getterRegex2 = new RegExp('var\\(\\s*.+\\s*,\\s*(.+)\\)', 'g');
      // console.log(getterRegex);
      // console.log(curCSS);
      let matches = curCSS.match(getterRegex2);
      if (matches) {
        // console.log("matches",matches);
        matches.forEach(function(match) {
          // console.log(match.match(/var\(.+,\s*(.+)\)/))
          // find the fallback within the getter
          curCSS = curCSS.replace(match, match.match(/var\(.+,\s*(.+)\)/)[1]);
        });

      }

      // curCSS = curCSS.replace(getterRegex2,varList[theVar]);
    };
    // console.log(curCSS);
    return curCSS;
  },

  // determine the css variable name value pair and track the latest
  ratifySetters(varList) {
    // console.log("varList:",varList);
    // loop through each block in order, to maintain order specificity
    for (let curBlock in varList) {
      let curVars = varList[curBlock];
      // console.log("curVars:",curVars);
      // loop through each var in the block
      curVars.forEach(function(theVar) {
        // console.log(theVar);
        // split on the name value pair separator
        let matches = theVar.split(/:\s*/);
        // console.log(matches);
        // 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(/;/, '');
      });
    };
    // console.log(ratifiedVars);
  },

  // get the CSS file (same domain for now)
  getLink(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) {
        // Success!
        // console.log(request.responseText);
        if (typeof success === 'function') {
          success(counter, request);
        }
      } 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();
  }
  
};

// hash = function(s){
//   return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);              
// }
cssVarPoly.init();

// export default makeFit;

// stuff for hiding documentation for Opera Mini testing
document.querySelector('.hide-docs').addEventListener('click',function(event){
  event.preventDefault();
  document.querySelector('body').classList.toggle('hide-the-docs');
});
<link rel="stylesheet" type="text/css" media="all" href="http://aaronbarker.net/cssvars/vars.css?a">
<div class="documentation">
  <h1>CSS Variables Polyfill</h1>
  <p>
    This is an attempt at a very basic <a href="https://drafts.csswg.org/css-variables/">CSS variables (custom properties)</a> polyfil. In reality this is more of a <em>partial</em> polyfill as it will not cover variables inside of variables, DOM scoping or anything else "fancy". Just taking variables declared anywhere in the CSS and
    then re-parsing the CSS for var() statements and replacing them in browsers that don't natively support CSS variables.
  </p>
  <p>According to <a href="http://caniuse.com/#feat=css-variables">caniuse.com</a>, of current browsers only IE, Edge and Opera Mini do not support CSS variables. This polyfil appears to work on all three really well. I don't see why this wouldn't work on older browsers as well, but I haven't been able to test it on them yet.</p>

  <p>As far as we can tell your browser <span class="supports">does <span class="no">not</span> support</span> native CSS variables. <span class="showforpolyfill">That means if you see green tests results below, it is thanks to the polyfill :).</span> <span class="hideforpolyfill">All the green test results below are actually native CSS Variable support.  Good job using a good browser :)</span></p>
  
  <h3>Does this work on externally CSS files?</h3>
  <p>Yes!</p>
  <h3>Even ones loaded from another domain?</h3>
  <p>To go across domain, CSS needs to be served up with <code>Access-Control-Allow-Origin:*</code> headers.</p>


</div>
<a href="#d" class="hide-docs">Toggle documentation</a> (for Opera Mini vs Codepen issue)
<style>
  :root {
    --newcolor: #0f0;
  }
  
  .inlineoverlink {
    color: var(--success2);
  }
</style>
<h2>Tests</h2>
<p>On mosts tests (unless otherwise noted) success will be green text. We start with a <code>color:red;</code> and then override it with a <code>color:var(--success);</code> (or similar) which is green.</p>
<span class="samename">declare same variable over and over</span>
<span class="demo1">no whitespace on var() calls</span>
<span class="demo2">whitespace on var() calls</span>
<span class="demo3">Multiple variables in same call. orange means first var worked, green var worked</span>
<span class="inlineoverlink">orange if link won, green if style after link won</span>
<span class="lower">--foo: lowercase foo</span>
<span class="upper">--FOO: uppercase FOO</span>
<span class="fallback">uses fallback <code>--var(--wrongname, green)</code></span>

<h2>Tests on external, cross-domain file</h2>
<div class="documentation">
  <p><strong>Edge</strong> appears to be working well on Edge 13. Edge 12 was having some problems.</p>
  <p><strong>Opera mini</strong> seems to work well too. This demo fails because not all the page is displayed, but I think that is a codepen issue, not a polyfill issue.  When the upper documentation is removed, all tests display well.</p>
  <p><strong>IE 11</strong> seems to do fine.</p>
</div>

<span class="demo4">Gets stuff from external .css file. Should start red and change to green on LINK load. border proves the CSS loaded, missing colors means script didn't get parsed and reinserted</span>
<span class="externalcolor">--externalcolor: should start red and change to green on LINK load</span>
<span class="externalfallback">uses fallback. should be green</span>

<p>Another set of text under the test for Opera Mini testing.</p>

CSS Variables Polyfill

Just playing around. It won't be bulletproof, but enough to do some basics.

A Pen by Aaron Barker on CodePen.

License.

CSS Variables Polyfill

Just playing around. It won't be bulletproof, but enough to do some basics.

A Pen by Aaron Barker on CodePen.

License.