Just playing around. It won't be bulletproof, but enough to do some basics.
A Pen by Aaron Barker on CodePen.
Just playing around. It won't be bulletproof, but enough to do some basics.
A Pen by Aaron Barker on CodePen.
Just playing around. It won't be bulletproof, but enough to do some basics.
A Pen by Aaron Barker on CodePen.
| <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> |
| /* | |
| 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'); | |
| }); |
| :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; | |
| } |
| :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; | |
| } |
Found another bug when you're splitting the "--variable: value" here. The RegExp fails to capture external URLs due to splitting on the all colons.
I fixed it by making the RegExp split on just the the first colon
/:(.+)/as per this StackOverflow answer.