Petit TP du week end : un effet de loupe en Javascript et CSS3 que vous pouvez essayer ici. Vous pouvez changer le style de la loupe grâce au menu à gauche.
J'ai codé ça à l'aide de YUI3. C'est l'occasion de découvrir ensemble les qualités de ce framework magique de Yahoo. Regardons ça d'un peu plus près.
Head
Dans le header de ma page html, je charge deux scripts : http://yui.yahooapis.com/3.2.0/build/yui/yui-min.js et js/magnifier.js.
Le premier est la base de YUI. Cette classe s'occupera de charger toutes les dépendances des scripts utilisés dans la page.
Le second est mon script dans lequel est déclarée une classe qui hérite de Y.Plugin.Base.
Body
Dans le body, le script suivant :
<script type="text/javascript" charset="utf-8"> //<![CDATA[ YUI().use('magnifier', function(Y) { Y.all('img[longdesc]').each( function(img) { img.plug(Y.Magnifier); } ); }); //]]> </script>
On récupère tous les éléments <img /> de la page qui possèdent un attribut longdesc, et pour chacun d'eux, on leur plugue la classe Y.Magnifier, justement décrite dans le fichier js/magnifier.js. Observons la magie de YUI3.
Dans ce bout de script, img n'est pas qu'un simple element. C'est un objet de classe Y.Node construit autour de l'élément <img />. C'est ainsi qu'on peut lui faire exécuter des méthodes, telles que img.one('.selecteur .css p') qui récupère un élément enfant en suivant un sélecteur CSS par exemple, ou lui attacher des plugins, qui sont des classes héritant de Y.Plugin.Base.
C'est donc ce que fait ma classe Y.Magnifier. C'est un plugin qui va rajouter des fonctionnalités à cette pauvre image sans défense. Regardons de quoi elle est faite.
YUI().add('magnifier', function(Y) { … }, '1.0', { requires: ['plugin', 'node', 'overlay'] });
Tout va se passer dans la méthode add() de l'objet global YUI(). Le premier paramètre est le nom du module que nous ajoutons à YUI (différent du nom de la classe). Le second paramètre est la code qui sera éxécuté quand ce module sera chargé. Le troisième indique le numéro de version et ne sert pas à grand chose. Le quatrième est nettement plus intéressant car il permet de spécifier les dépendances. Ma classe aura besoin des modules de YUI : plugin, node et overlay et seront chargés automatiquement sans avoir à les inclure dans mon head : la magie du YUI3 Loader.
Regardons maintenant le code éxécuté par notre module magnifier :
Y.Magnifier = function(config) { Y.Magnifier.superclass.constructor.apply(this, arguments); } Y.Magnifier.NAME = 'Magnifier'; Y.Magnifier.NS = 'magnifier'; Y.Magnifier.ATTRS = { /** * Original picture's url */ original: { valueFn: function() { … } }, /** * Container where is displayed the magnifier */ container: { valueFn: function() { … } } }; Y.extend(Y.Magnifier, Y.Plugin.Base, { … });
La première fonction permet de déclarer le nom de ma classe et de lui faire adopter le même comportement que l'ensemble des classes de YUI : un unique argument sous forme d'objet de paramètres nommés, qui servira pour la gestion des attributs. Je l'ai appellée Y.Magnifier, mais j'aurais aussi pu l'appeler Y.eu.hadrien.Magnifier ou encore Loupe. Cette variable ne sera de toute façon présente que dans la sandbox qui aura demandé à faire usage de ce module (YUI().use('magnifier', function(Y) {})).
Les attributs NAME et NS permettent de nommer notre classe de façon générique. Le plus important pour un plugin est NS car c'est par ce nom que la factory deviendra accessible depuis l'objet hote. Ainsi, dans notre exemple, je pourrais accéder à l'objet Magnifier depuis img.magnifier.
ATTRS est une fonctionnalité particulièrement pratique de YUI. Dans cet objet, nous allons déclarer tous les paramètres que notre classe peut avoir. Nous pouvons pour chacun d'eux spécifier une valeur par défaut, une valeur par défaut calculée par une fonction (c'est le cas ici grâce à valueFn), des méthode de getter, de setter, ou de controle de typage. Tout ces attributs feront partie des arguments que l'ont pourra passer lors de l'instanciation de la classe. Petit exemple :
… MaClasse.ATTRS = { truc: { value: 1, setter: function(v) { return v * 10; } }, machin: { valueFn: function() { return this.get('truc') + 1; } }, bidule: { value: null, getter: function() { if (this.get('truc') > 10) { return true; } return false; } }, unEntier: { value: 0, setter: function(v) { if (!Y.Lang.isNumber(v)) { throw 'unEntier must be a Number'; } return v; } } } … var monObj1 = new MaClasse(); // monObj1.get('truc') === 1; // monObj1.get('machin') === 2; // monObj1.get('bidule') === false; monObj1.set('truc', 20); // monObj1.get('truc') === 200; var monObj2 = new MaClasse({ truc: 2, machin: 10, graaaa: 'test' }); // monObj2.get('truc') === 20 // monObj2.get('machin') === 10 // monObj2.get('bidule') === true // monObj2.get('graaaa') === undefined // monObj2.set('unEntier', 'yo') => Exception
Cette façon de déclarer les propriétés d'une classe est particulièrement puissant et permet de ranger plus clairement son code en séparant les méthodes de formatage des variables du code propre à la classe elle même.
La dernière partie concerne la définitions des méthodes de la classe. Cela se passe à travers la méthode Y.extend qui va se charger de faire hériter notre classe d'une précédente, et d'y ajouter ses propres méthodes. Le premier argument est notre classe. Le second est la classe dont elle hérite. Le troisième est le prototype de notre classe : un objet contenant ses méthodes.
Des méthodes spéciales sont proposées : initializer et destructor. initializer est le constructeur et sera appelée automatiquement lors de l'instanciation de la classe. desctructor sera appelé lors la destruction de l'objet grâce à la méthode destroy() dont notre classe a hérité de Y.Base (dont hérite Y.Plugin.Base).
Il ne nous reste plus qu'à coder les méthodes de notre classe, en sachant que nous accédons toujours aux attributs renseignés dans ATTRS grâce à la méthode get() (monObj.get('maPropery'), que nous les modifions grâce à set() (monObj.set('maProperty', 'maValue')). Et spécifiquement aux classes qui héritent de plugin, l'objet auquel nous avons pluggé notre classe est implicitement mis dans la propriété host. Nous accédons dans notre classe Magnifier à notre Node <img /> grâce à this.get('host') à l'intérieur de la classe, ou maLoupe.get('host') depuis l'instance.
Y.extend(Y.Magnifier, Y.Plugin.Base, { initializer: function() { … }, /** * destruct */ destructor: function() { … }, /** * Show and positionate glasse and its background image */ move: function(x, y) { … }, /** * hide glasses */ hide: function(x, y) { … } });
Concernant la css et la loupe ronde, il s'agit d'une bidouille à l'aide de border-radius qui consiste à spécifier un radius de la même taille que les dimensions du container :
.magnifier { width: 200px; height: 200px; -moz-border-radius: 200px; /* FF1+ */ -webkit-border-radius: 200px; /* Saf3+, Chrome */ -khtml-border-radius: 200px; /* Konqueror */ border-radius: 200px; /* Standard. IE9 */ }
Évidemment, ça marche pas sous IE < 9, mais osef :D
Commentaires