Google analytics script

Latest jQuery CDN with code tiggling.

Saturday, 30 March 2013

LESS gradient mixin with fallback for IE

As you could read in my last post (long time ago) I've rather used SCSS over LESS but when I upgraded my Visual Studio to version 2012 I decided not to install Mindscape's addin as VS already comes with support for LESS, CoffeeScript and TypeScript via Web Essentials addin. So I started writing my usual set of mixins in LESS. It seemed simple and straight forward at first until I started writing .gradient mixin that should somewhat also support older non-CSS3 browsers like outdated IE8.

Gradient mixin requirements

CSS3 gradient support is great but when creating a public facing webapp I usually want to support old(er) browsers so they'd display something in place of those nifty looking gradients. I opt for a flat background colour that is a mix between first and last gradient colour. But that's not all. Here are my gradient mixin requirements:

  1. support CSS3 gradients using background-image style property
  2. support prefixed variations i.e. -webkit
  3. graceful degradation for older non-CSS3 browsers by providing a flat colour calculated from first and last gradient definition colour as vast majority of gradients are two coloured
  4. provide mixin parameters in the same way as we provide them for actual CSS so without resorting to escaped ~"..." LESS notation
  5. all fallbacks for flat colours have to be automatically calculated by the mixin instead of providing it manually

LESS parametrised mixins and built-in functions

LESS is comparable to SCSS although latter seems much more feature rich as I've already learned some time ago. SCSS also provides list manipulation functions which are helpful exactly in this situation as an arbitrary number of mixin parameters are a list. LESS on the other hand is limited but I should also mention that next version (1.4) will have some support for this by providing extract function that will be able to extract a particular mixin parameter from @arguments variable. But for now we're left empty handed.

Ok so let's go through upper list of requirements and see how I solved them. I'm not going to detail #1 and #2 as they're perfectly trivial when you resolve #4. Hence I'll spend some time around #4 and a bit more around #3 which is the most complex of all. And I hope that especially my solution to #3 will help you as I haven't found anything similar on the internet. Now my article can help others.

Problem of providing several arguments to LESS mixin

LESS has a strange way of treating arbitrary number of mixin parameters. Let's take this simplified example:

   1:  .shadow (...) {
   2:      box-shadow: @arguments;
   3:  }
When we wanted a shadow on an element we'd simply call it this way: .shadow(0 0 5px #000); The output would be as expected. But shadows do allow defining several shadows at once separated by commas. That was also the reason why mixin uses variadic argument definition (...).

Now we create a double shadow: .shadow(0 0 5px #000, 0 0 2px #fff); and result wouldn't be ok as comma separating both shadow definitions is being removed from result. Translated property would therefore look like this: box-shadow: 0 0 5px #000 0 0 2px #fff; There are basically two ways of mitigating this problem:

  1. Provide all definitions as a single escaped string parameter .shadow(~"0 0 5px #000, 0 0 2px #fff"); with which we trick LESS compiler that we're seemingly providing a single parameter and LESS would just keep it as is and strip away delimiting quotes
  2. Provide all definitions as you would in CSS and then use javascript evaluation and some plain old Javascript string manipulation using regular expressions

#4: Using usual CSS syntax to provide gradient property values

Javascript evaluation in LESS is provided by using back-ticks (a.k.a. grave accent). As gradients usually require at least two parameters we have a bit simplified Javascript expression. @all: ~`"@{arguments}".replace(/[\[\]]/g,"")`; We'll use this variable later on in our final mixin. First part of converting arguments to string renders them with square brackets, similar to how Arrays are being presented in Javascript. Regular expression in the second part therefore replaces those brackets with empty string, so we end up with parameters only. These parameters can then safely be assigned to CSS3 gradient properties.

Advanced LESS Javascript evaluations

As LESS is a client-side compiler (executed by Javascript) it also supports Javascript evaluations. This is one of the main differences between LESS and SCSS as SCSS is a server-side compiler implemented in whatever language we require. But even though LESS supports Javascript evaluation it may seem tricky to execute complex block of statements. If you're versed in Javascript you've likely heard about immediately executing anonymous functions in Javascript. The only requirement is that thiese functions have to provide a result that will be converted to a string. The best thing is to return a string then. And that's exactly what we can use in LESS to provide complex features. (function(args) { /* do whatever */ return "string value"; })("@{arguments}")

#3: Graceful degradation for non-CSS3 browsers

My graceful degradation for older browsers has two steps:

  1. first background-color is an everyday flat colour provided with 6 hex values as #XXXXXX; this is supported basically since ever in all browsers
  2. second background-color is for a more advanced browsers that may support rgba colour definition
That's why I'll provide two very similar functions (as they can't be reused in LESS) one that returns the first simple colour definition and the second one that returns the rgba one. But both of them parse arguments, take the first and last one, parses colours out of them and calculates a mix between the two.

Simple colour:

   1:  (function(args) {
   2:      args = args.replace(/,\s+(?=[\d ,.]+\))/gi, ";").split(/,\s*/g);
   3:      var first = args[0].split(/\s+/g)[0];
   4:      var last = args.pop().split(/\s+/g)[0];
   5:      
   6:      var calculateValues = function(color) {
   7:          var result = [];
   8:          // is colour provided as RGBA?
   9:          /rgb/i.test(color) && color.replace(/[\d.]+/g, function(i) {
  10:              result.push(1*i);
  11:              return "";
  12:          });
  13:          // is colour a short #XXX
  14:          /#.{3}$/.test(color) && color.replace(/[\da-f]/ig, function(i) {
  15:              result.push(parseInt(i+i, 16));
  16:              return "";
  17:          });
  18:          // is colour a long #XXXXXX
  19:          /#.{6}/.test(color) && color.replace(/[\da-f]{2}/ig, function(i) {
  20:              result.push(parseInt(i, 16));
  21:              return "";
  22:          });
  23:          // colour provided and parsed
  24:          if (result.length) return result;
  25:          // not recognised as colour, provide RED
  26:          return [100,0,0];
  27:      };
  28:      
  29:      var padZero = function(val) {
  30:          // just last two hex digits
  31:          return ("0" + val.toString(16)).match(/.{2}$/)[0];
  32:      };
  33:      
  34:      first = calculateValues(first);
  35:      last = calculateValues(last);
  36:      
  37:      var result = {
  38:          r: ((first.shift() + last.shift()) / 2) | 0,
  39:          g: ((first.shift() + last.shift()) / 2) | 0,
  40:          b: ((first.shift() + last.shift()) / 2) | 0
  41:      };
  42:      return "#"+ padZero(result.r) + padZero(result.g) + padZero(result.b);
  43:  })("@{arguments}")

RGBA colour:

   1:  (function(args) {
   2:      args = args.replace(/,\s+(?=[\d ,.]+\))/gi, ";").split(/,\s*/g);
   3:      var first = args[0].split(/\s+/g)[0];
   4:      var last = args.pop().split(/\s+/g)[0];
   5:      
   6:      var calculateValues = function(color) {
   7:          var result = [];
   8:          // is colour provided as RGBA?
   9:          /rgb/i.test(color) && color.replace(/[\d.]+/g, function(i) {
  10:              result.push(1*i);
  11:              return "";
  12:          });
  13:          // is colour a short #XXX
  14:          /#.{3}$/.test(color) && color.replace(/[\da-f]/ig, function(i) {
  15:              result.push(parseInt(i+i, 16));
  16:              return "";
  17:          });
  18:          // is colour a long #XXXXXX
  19:          /#.{6}/.test(color) && color.replace(/[\da-f]{2}/ig, function(i) {
  20:              result.push(parseInt(i, 16));
  21:              return "";
  22:          });
  23:          // colour provided and parsed
  24:          if (result.length) {
  25:              // push alpha for colours that have none (others that do don't matter)
  26:              result.push(1);
  27:              return result;
  28:          }
  29:          // not recognised as colour, provide RED
  30:          return [100,0,0,1];
  31:      };
  32:      
  33:      first = calculateValues(first);
  34:      last = calculateValues(last);
  35:      
  36:      var result = {
  37:          r: ((first.shift() + last.shift()) / 2) | 0,
  38:          g: ((first.shift() + last.shift()) / 2) | 0,
  39:          b: ((first.shift() + last.shift()) / 2) | 0,
  40:          a: (first.shift() + last.shift()) / 2
  41:      };
  42:      return "rgba("+ result.r +","+ result.g +","+ result.b +","+ result.a +")";
  43:  })("@{arguments}")

All we have to do now is to minify these two functions and put them in our gradient LESS mixin

   1:  .gradient (...) {
   2:      @all: ~`"@{arguments}".replace(/[\[\]]/g,"")`;
   3:      @mix1: ~`(function(a){a=a.replace(/,\s+(?=[\d ,.]+\))/gi,";").split(/,\s*/g);var f=a[0].split(/\s+/g)[0];var l=a.pop().split(/\s+/g)[0];var c=function(c){var r=[];/rgb/i.test(c)&&c.replace(/[\d.]+/g,function(i){r.push(1*i);return"";});/#.{3}$/.test(c)&&c.replace(/[\da-f]/ig,function(i){r.push(parseInt(i+i,16));return"";});/#.{6}/.test(c)&&c.replace(/[\da-f]{2}/ig,function(i){r.push(parseInt(i,16));return"";});if(r.length)return r;return[100,0,0];};var p=function(v){return("0"+v.toString(16)).match(/.{2}$/)[0];};f=c(f);l=c(l);var r={r:((f.shift()+l.shift())/2)|0,g:((f.shift()+l.shift())/2)|0,b:((f.shift()+l.shift())/2)|0};return"#"+p(r.r)+p(r.g)+p(r.b);})("@{arguments}")`;
   4:      @mix2: ~`(function(a){a=a.replace(/,\s+(?=[\d ,.]+\))/gi,";").split(/,\s*/g);var f=a[0].split(/\s+/g)[0];var l=a.pop().split(/\s+/g)[0];var c=function(c){var r=[];/rgb/i.test(c)&&c.replace(/[\d.]+/g,function(i){r.push(1*i);return"";});/#.{3}$/.test(c)&&c.replace(/[\da-f]/ig,function(i){r.push(parseInt(i+i,16));return"";});/#.{6}/.test(c)&&c.replace(/[\da-f]{2}/ig,function(i){r.push(parseInt(i,16));return"";});if(r.length){r.push(1);return r;}return[100,0,0,1];};f=c(f);l=c(l);var r={r:((f.shift()+l.shift())/2)|0,g:((f.shift()+l.shift())/2)|0,b:((f.shift()+l.shift())/2)|0,a:(f.shift()+l.shift())/2};return"rgba("+r.r+","+r.g+","+r.b+","+r.a+")";})("@{arguments}")`;
   5:      background-color: @mix1;
   6:      background-color: @mix2;
   7:      background-image: -webkit-linear-gradient(top, @all);
   8:      background-image: -moz-linear-gradient(top, @all);
   9:      background-image: -o-linear-gradient(top, @all);
  10:      background-image: linear-gradient(to bottom, @all);
  11:  }

Special trick with shadow mixin (1..N parameters)

Gradients have 2..N parameters hence we only have to cover such scenario. Shadows on the other hand have 1..N possible parameters (shadow definitions). And as seen previously LESS understands single parameter differently than several. Several parameters are compiled stripping out commas that delimit them. Using the same trick as we used in gradient also won't work because providing a single shadow and manipulating them with Javascript will render individual values within same shadow definition as an array hence have too many commas. box-shadow: 0, 0, 5px, #000;

We use a different trick with such mixins which ensure that we always have at least two parameters. What it does is it provides a second parameter with default (invalid) value that we can later strip out using regular expression. This will of course get stripped out only in case of a single provided parameter. Without any further explanation as it's not too complicated, let me just provide the .shadow mixin.

   1:  .shadow (@a1, @a2:_, ...) {
   2:      @all: ~`"@{arguments}".replace(/[\[\]]|\,\s_/g,"")`;
   3:      -webkit-box-shadow: @all;
   4:      box-shadow: @all;
   5:  }
As you can see second argument has a default value "_" that is not a valid shadow definition so we can easily strip it out using regular expression.

I apologise for a longer post but I wanted to explain this into much detail.

No comments:

Post a Comment