"[-prefix-free is] fantastic, top-notch work! Thank you for creating and sharing it." - Eric Meyer

Grâce à -prefix-free, seules les propriétés standards suffisent. Pas étonnant que tous les Code Playgrounds se sont rués pour nous proposer cette librairie devenue incontournable. Prenons par exemple le pen suivant proposé par amos.

See the Pen cblAm by Julien Sobczak (@julien-sobczak) on CodePen.

Aucun préfixe -moz ou -webkit, un code simple d’autant plus grâce à l’utilisation de Sass. Derrière le rideau, -prefix-free ajoute les propriétés préfixées mais seulement si nécessaire. Comment opère cette magie ? C’est ce que nous allons découvrir.

-prefix-free nous est offert par Lea Verou et est disponible sur Github. Le code présenté dans cet article a été simplifié pour des raisons évidentes et n’a pas pour vocation à être utilisé en dehors de ce contexte d’apprentissage. Cet article est basé sur la dernière version du code au moment de la publication de cet article.

Un premier exemple

/* demo.css */
h1 {
  background: orange;
  border-radius: 10px;
}
<!-- demo.html -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Démo PrefixFree</title>

    <link rel="stylesheet" href="demo.css">
  </head>
  <body>

    <h1>Hello World!</h1>

    <script src="http://cssdeck.com/assets/js/prefixfree.min.js"></script>

  </body>
</html>

Si on inspecte le source dans notre navigateur Firefox 3.6, on constate quelques différences :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- demo.html (résultat avec Firefox 3.6) -->
<!doctype html>
<html class="-moz-">
  <head>
    <meta charset="utf-8">
    <title>Démo PrefixFree</title>

    <style media="" data-href="demo.css">
      h1 {
        background: orange;
        -moz-border-radius: 10px;
      }
    </style>
  </head>
  <body>

    <h1>Hello World!</h1>

    <script src="http://cssdeck.com/assets/js/prefixfree.min.js"></script>

  </body>
</html>
8
Notre feuille de style a disparu et a été remplacée par une feuille de style inline.
8
Le contenu est identique à une exception près : l'utilisation du préfixe -moz pour la propriété non supportée border-radius.

C’est parti !

-prefix-free ajoute 2 variables globales (StyleFix et PrefixFree), reflet d’une librairie divisée en deux parties bien distinctes :

  • StyleFix est un framework qui permet d’appliquer des corrections à du CSS.
  • PrefixFree repose dessus et enregistre un correcteur qui vient remplacer les propriétés CSS non supportés par les équivalents des navigateurs.

Note : Nous allons continuer sur notre exemple du border-radius. Cette propriété est disponible (avec préfixe) depuis la version 2 de Firefox. Les exemples qui suivent ont été testés avec la version Firefox 3.6.

StyleFix : corrigeons nos CSS !

StyleFix applique une série de corrections apportées par ce qu’on va appeler des fixers. Un fixer est simplement une fonction qui respecte la signature suivante :

var css = fix(css, raw, element);

où :

  • css est une chaine de caractères contenant le code CSS à corriger.
  • raw qui vaut false lorsque le CSS est directement présent sur une balise HTML.
  • element qui correspond à l’élement associé au code (la balise link, style ou la balise HTML avec un attribut style).

La fonction retourne le CSS modifié.

L’enregistrement des fixers se fait grâce à la fonction register.

var self = window.StyleFix = { // Définition de l’objet global

  register : function(fixer) {
    self.fixers = (self.fixers || []).push(fixer);
  },

};

Les fixers sont ensuite déclenchés pour chaque élément contenant du code CSS grâce à la fonction fix:

fix : function(css, raw, element) {
 for (var i = 0; i < self.fixers.length; i++) {
  css = self.fixers[i](css, raw, element);
 }

 return css;
}

Rien d’insurmontable jusqu’à présent.

Intéressons nous maintenant à ce qui se passe au chargement de la page. Une fois le DOM chargé, StyleFix recherche les balises link, style et celles ayant un attribut style. Ici, nous nous intéresserons uniquement aux balises style mais le principe reste le même pour les autres balises.

var self = window.StyleFix = {

 styleElement : function(style) {
  style.textContent = self.fix(style.textContent, true, style);
 },

 process : function() {
  [].forEach.call(document.querySelectorAll('style'), StyleFix.styleElement);
 },

};

document.addEventListener('DOMContentLoaded', StyleFix.process, false);

Zoom sur querySelectorAll

La petite subtilité de ce code provient de la méthode querySelectorAll qui retourne un object NodeList. Cet object propose une propriété length et peut être itérer avec un for, de quoi nous laisser croire qu’on peut utiliser la méthode forEach. Mais non. Il ne s’agit pas d’un tableau, d’où la subtilité décrite plus en détail dans la documentation de l’objet.

Nous en avons fini avec l’objet StyleFix. Voici le résultat final :

(function() {

  var self = window.StyleFix = {

   styleElement : function(style) {
    style.textContent = self.fix(style.textContent, true, style);
   },

   process : function() {
    [].forEach.call(document.querySelectorAll('style'), StyleFix.styleElement);
   },

   register : function(fixer) {
    (self.fixers = self.fixers || []).push(fixer);
   },

   fix : function(css, raw, element) {
    for (var i = 0; i < self.fixers.length; i++) {
     css = self.fixers[i](css, raw, element);
    }

    return css;
   }

  };

  document.addEventListener('DOMContentLoaded', StyleFix.process, false);

})();

Avant de passer à la suite, voici un bref exemple d’utilisation de la librairie qui convertit les feuilles de style sur une seule ligne :

StyleFix.register(function(css, raw, element) {
 return css.replace(/\n/gm, '');
});

PrefixFree, les choses sérieuses commencent !

En prenant quelques raccourcis, on arrive à une première version opérationnelle :

1
2
3
4
5
6
7
8
9
10
11
StyleFix.register(function(css, raw, element) {
 var prefix = '-moz-', // TODO
     properties = ['border-radius']; // TODO

 for (var i = 0; i < properties.length; i++) {
  var regex = RegExp(properties[i], 'gi');
  css = css.replace(regex, prefix + properties[i]);
 }

 return css;
});
2
On se concentre unique sur notre Firefox 3.6 pour le moment.
3
On considère uniquement la propriété border-radius.
6
On recherche chaque propriété à remplacer pour la remplacer par son équivalent préfixé.

On retrouve logiquement StyleFix qui nous sert à enregistrer un fixer. Ce fixer, pour chaque propriété non supportée, remplace par la propriété équivalente. L’expression régulière permet de faire un remplacement global, la méthode replace ne remplaçant que la première occurrence (un flag peut être défini en 3ème argument mais n’est pas supporté par le moteur V8).

Pour que le code fonctionne sur d’autres exemples, deux points restent à élucider :

  • Comment connaître le préfixe du navigateur de l’utilisateur ?
  • Comment identifier les propriétés à remplacer ?

Commençons par répondre à la première question.

Plusieurs solutions sont envisageables. On pourrait utiliser Modernizr mais -prefix-free utilise une solution toute simple qui consiste à créer un élément dans le DOM et inspecter son attribut style représenté en JavaScript par l’objet CSSStyleDeclaration. Cet objet contient les valeurs de toutes les propriétés CSS supportées par le navigateur. On va se contenter de mémoriser la liste des propriétés qui commencent par - (synomyme d’extension) afin d’extraire le préfixe et de répondre par la même occasion à la deuxième question.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var prefix = undefined,
  properties = [],
  dummy = document.createElement('div').style;

for (var property in dummy) {
 property = deCamelCase(property);

 if (property.charAt(0) === '-') {
  properties.push(property);

  prefix = prefix || property.split('-')[1];
 }
}

self.prefix = '-' + prefix + '-';
6
Cette ligne est nécessaire pour retrouver le nom de la propriété telle que nous la connaissons en CSS. En effet, côté JavaScript les propriétés CSS sont définies comme des propriétés de l’objet CSSStyleDeclaration et se conforment à la définition du langage (caractère - interdit dans un identifiant).

Deux fonctions utilitaires sont définies qui permettent de passer d’une notation à l’autre :

function camelCase(str) {
 return str.replace(/-([a-z])/g, function($0, $1) {
  return $1.toUpperCase();
 }).replace('-', '');
}

function deCamelCase(str) {
 return str.replace(/[A-Z]/g, function($0) {
  return '-' + $0.toLowerCase()
 });
}

Si on revient à l’exemple précédent, nous étions arriver à un tableau contenant les propriétés avec préfixe supportées par notre navigateur. Il nous un cas reste à gérer : les navigateurs évoluent et tôt ou tard les propriétés standard deviennent supportées (ex : Firefox >= 4 supporte à la fois -moz-border-radius et border-radius). Inutile dans ces cas d’effectuer les remplacements.

// (suite)
// var properties = [/* toutes les propriétés avec préfixe supportées */]

self.properties = [];

supported = function(property) {
 return camelCase(property) in dummy;
}

// Get properties ONLY supported with a prefix
for (var i = 0; i < properties.length; i++) {
 var property = properties[i];
 var unprefixed = property.slice(self.prefix.length);

 if (!supported(unprefixed)) {
  self.properties.push(unprefixed);
 }
}

Notre version de PrefixFree est désormais complète :

(function(root) {

 function camelCase(str) {
  return str.replace(/-([a-z])/g, function($0, $1) {
   return $1.toUpperCase();
  }).replace('-', '');
 }

 function deCamelCase(str) {
  return str.replace(/[A-Z]/g, function($0) {
   return '-' + $0.toLowerCase()
  });
 }

 var self = window.PrefixFree = {
  prefixCSS : function(css, raw, element) {
   var prefix = self.prefix;

   for (var i = 0; i < self.properties.length; i++) {
    var regex = RegExp(self.properties[i], 'gi');
    css = css.replace(regex, prefix + self.properties[i]);
   }

   return css;
  }

 };

 (function() {
  var prefix = undefined,
    properties = [],
    dummy = document.createElement('div').style;

  supported = function(property) {
   return camelCase(property) in dummy;
  }

  for ( var property in dummy) {
   property = deCamelCase(property);

   if (property.charAt(0) === '-') {
    properties.push(property);

    prefix = prefix || property.split('-')[1];
   }
  }

  self.prefix = '-' + prefix + '-';

  self.properties = [];

  // Get properties ONLY supported with a prefix
  for (var i = 0; i < properties.length; i++) {
   var property = properties[i];
   var unprefixed = property.slice(self.prefix.length);

   if (!supported(unprefixed)) {
    self.properties.push(unprefixed);
   }
  }

 })();

 StyleFix.register(self.prefixCSS);

})(document.documentElement);

Terminé !

Cela termine la découverte de -prefix-free. Moins de 100 lignes auront été nécessaires pour proposer une première version. L’exemple complet est disponible ici.

A vous de forker

  • Supporter les balises link et attribut style. Indice : récupérer le contenu des feuilles de style externes en AJAX. Quelles sont les limites ?
  • Les changements CSS après chargement de la page (en JavaScript) ne sont pas supportés. Indice : écouter les événements DOMAttrModified et DOMNodeInserted (voir plugin prefixfree.dynamic-dom.js).
  • Supporter les @rules comme keyframe. Indice : utiliser des expressions régulières plus poussées en reprenant le même principe.

A Retenir

  • StyleFix/PrefixFree : un bon exemple de Divide-and-Conquer !
  • querySelectorAll ne retourne pas un objet Array mais NodeList.
  • L'objet CSSStyleDeclaration permet de connaître les propriétés supportées par un navigateur.