/ Technique

Une galerie pour Ghost

Un petit article "tutoriel" pour expliquer l'implémentation (perfectible) des galeries sur ce blog.


Objectif

J'ai beau critiquer WordPress à tour de bras, il faut avouer que ce CMS proposait tout de même de nombreuses fonctionnalités intéressantes, soit par défaut, soit via JetPack, soit via les nombreuses extensions plus ou moins stables proposées par la communauté. Quiconque a utilisé les "tiled galleries" de WordPress avant de passer à Ghost ressentira probablement un méchant sentiment de manque face aux maigres options du second.

Caption
Ça a un minimum de classe, il faut avouer.

Tout comme pour le texte en marge, les citations ou les références, j'ai créé un petit snippet pour ce thème Ghost qui permet d'afficher une série d'images sous la forme d'une galerie. Ce code, un peu plus long que les précédents, n'a pas la prétention d'être mature, professionnel ni même particulièrement optimisé : je le décortique ici pour offrir quelques pistes à qui en aurait besoin.

Fonctionnalités et syntaxe souhaitées

Ce script de galerie permet l'affichage d'une série d'images de largeur variable (potentiellement rognées), sur des lignes de hauteur similaire. Lorsqu'une personne clique sur une image, celle-ci apparaît en plein écran. Il est possible de naviguer entre les éléments de la galerie soit avec les flèches du clavier, soit en swipant avec son doigt.

La syntaxe idéale ne nécessite pas de code ou de balise particulière. Les rédacteur/trice/s n'ont qu'à uploader des images à la suite, et spécifier "Gallery" dans l'attribut alt. La syntaxe Markdown est donc la suivante : ![Gallery](url-de-l'image). La légende attribuée à chaque image de la galerie peut être rédigée en-dessous. Toutes les images doivent se suivre, séparées par des retours de ligne (de sorte à générer des paragraphes pour chacune d'entre elles).

Caption
En résumé...

Implémentation

À l'heure actuelle, Ghost ne permet pas encore de créer des "applications" pour étendre ses fonctionnalités : cette possibilité devrait apparaître avec la prochaine mise à jour majeure. Nous sommes donc contraints d'appliquer les changements du côté client. Ce code va fonctionner en deux temps :

  1. Construire les éléments du DOM nécessaire à la galerie
  2. Appliquer les contrôles javascripts aux éléments préalablement créés

Pour garantir que les éléments de la galerie remplissent intégralement la ligne tout en étant d'une largeur aléatoire, on utilise la propriété CSS3 flex, qui définit la taille des éléments d'un conteneur par des rapports entre les uns et les autres. Ces valeurs sont modifiées par Javascript, de sorte à ce qu'un à trois éléments puissent être aléatoirement présents sur chaque ligne. Dans la galerie, les images sont affichées sous la forme de background-image, plus facile à positionniez au sein d'un div : la véritable image n'apparaît qu'après un click.

Prérequis

J'utilise jQuery pour me faciliter la vie et KnockoutJS pour détecter la présence d'une galerie sur la page. Enfin, HammerJS et son plugin jQuery sont utilisés pour la prise en charge de la gestuelle pour mobile.
Enfin, pour ce qui est du CSS, je rédige en SASS.

1. Construction des éléments

On commence par sélectionner tous les paragraphes de l'article pour y vérifier la présence d'images dotées de l'attribut alt "Gallery". Dans ce cas, on ajoute au paragraphe la classe "gallery-item", qui - comme son nom l'indique - identifie un objet de la galerie.

var par = 'article p'; // Sélection des paragraphes
$(par + ':has(img[alt*=Gallery])').addClass('gallery-item'); // Détection d'une image, ajout de la classe

À la fin du scan, si ne serait-ce qu'un élément "gallery-item" existe, on "emballe" le tout dans un nouvel élément du DOM, une div avec la classe "Gallery". Pour chaque "gallery-item", on :

  1. crée un span dont on extrait ensuite l'image.
  2. attribue un background-image dont l'URL est équivalente à la source de l'image
  3. attache un événement qui ajoute ou enlève la classe "clicked" lorsqu'un click est détecté

Nous nous retrouvons donc avec :

  • une div "Gallery" qui contient
    • des p "Gallery-Item", cliquable et composés
      • d'une img (l'image) et
      • d'un span (la légende).

L'apparence de ces éléments est définie par du CSS, dont la propriété flex est modifiée pour rendre la largeur de chaque élément aléatoire. On vise un minimum d'environ 33% de largeur pour chaque image, de sorte à ne pas se retrouver avec 4-5 images d'affilée sur une seule ligne.

Code JS :

$(par + '.gallery-item').wrapAll('<div class="gallery" />').each(function (e, p) {
    self.gallery(1); // Signifie à Knockout la présence d'une galerie

    $(p).wrapInner("<span></span>"); 
    var wrapper = $(p).children()[0];
    var img = $(wrapper).children()[0];
    var imgSrc = $(img).attr('src');
    $(p).css('background-image', 'url("'+imgSrc+'")');
    $(wrapper).before(img);

    $(this).click(function(event){
        $(this).toggleClass('clicked');
    });

    width = Math.ceil(Math.random() * 2) + 3;
    $(p).css('flex', width + ' ' + width * 10 + '%');
});

Le code CSS suivant est donné à titre indicatif. En commentaire, vous trouverez des remarques quant aux propriétés réellement nécessaires.

Code CSS (SASS):

.gallery
  display: flex // Nécessaire, défini un conteneur dont les objets sont flexibles. 
  flex-flow: row wrap // Nécessaire, indique que les éléments qui excèdent la taille d'une ligne peuvent déborder sur une autre ligne

  .gallery-item
    flex: 1 30% // Valeur par défaut, avant randomisation
    margin: 5px
    height: 400px
    overflow: hidden
    position: relative
    background-size: cover // Important pour afficher l'image dans toute la surface du .gallery-item
    background-position: 50% 20%
    filter: grayscale(0.5)
    transition: filter 0.2s ease-in-out
    z-index: 10

    span
      text-align: center
      text-shadow: 1px 1px 0 $shadow
      font-size: 0 // Cache la légende, tout en permettant une animation lorsqu'elle apparaît.
      position: absolute
      width: 100%
      bottom: 1em
      left: 0
      color: white

    img
      display: none // Cache l'image, au profit du background-image de .gallery-item défini via javascript

    &:hover
      cursor: pointer
      font-size: medium // Affiche la légende au passage de la souris
      filter: grayscale(0)

    &.clicked
      position: fixed // Nécessaire pour affichage en plein écran
      height: 100% // Nécessaire pour affichage en plein écran
      width: 100% // Nécessaire pour affichage en plein écran
      z-index: 1000 // Nécessaire pour affichage en plein écran
      left: 0 // Nécessaire pour affichage en plein écran
      top: 0 // Nécessaire pour affichage en plein écran
      background-image: none !important // cache le background-image, une fois le .gallery-item en plein écran
      margin: 0
      background-color: rgba(0,0,0, 0.9)
      filter: grayscale(0)

      span
        font-size: medium // Affiche la légende

      img
        max-width: 80%
        max-height: 80%
        position: absolute
        top: 50% // Centre l'image
        left: 50% // Centre l'image
        transform: translateX(-50%) translateY(-50%) // Centre l'image

2. Attribution des contrôles

Dans un premier temps, on définit une variable observable "gallery" qui définit si oui ou non, la page en cours dispose d'une galerie. Par défaut, ce n'est pas le cas. Lorsque le code de l'étape 1 signifie à Knockout qu'une galerie est détectée, "gallery" passe à "true". Une seconde variable, "galleryBehaviour", observe automatiquement le changement de statut et effectue les actions suivantes :

  • Attribution des contrôles de la galerie aux touches "esc" (quitte le plein écran), "flèche gauche" (retourne à l'image précédente) et "flèche droite" (avancer vers l'image suivante)
  • "Lazy load" de la libraire "HammerJS" pour la prise en charge des mouvements du doigt et attribution des contrôles de la galerie. Il est effectivement préférable de ne charger ce genre de librairies assez lourdes (plus de 10Ko) qu'à partir du moment où elles sont vraiment utiles. Notez également que j'ai fusionné le plugin jQuery et la librarie HammerJS en un seul fichier pour limiter le nombre de ressources externes.

C'est tout ce dont vous avez besoin pour attribuer les contrôles au clavier et au doigt pour votre galerie !

self.gallery = ko.observable(false);
self.galleryBehaviour = ko.computed(function(){
    if(self.gallery())
    {
        $(document).keydown(function(event){
            switch(event.which) {
                case 27: // Esc
                    $('.gallery-item.clicked').removeClass('clicked');
                    break;
                case 39: // Flèche droite
                    $('.gallery-item.clicked').removeClass('clicked').next().addClass('clicked');
                    break;
                case 37: // Flèche gauche
                    $('.gallery-item.clicked').removeClass('clicked').prev().addClass('clicked');
                    break;
            }
        });

        $.getScript('/assets/js/vendors/hammer.min.js', function(){
            $('.gallery-item').hammer().bind('swipeleft', function(e){
                $('.gallery-item.clicked').removeClass('clicked').next().addClass('clicked');
            });
            $('.gallery-item').hammer().bind('swiperight', function(e){
                $('.gallery-item.clicked').removeClass('clicked').prev().addClass('clicked');
            });
        });
    }
});

Résultat, limitations et futur

Un petit exemple de galerie, avec l'indécrottable Boris en guise de cobaye.

Gallery
Boris, gribouillage

Gallery
Boris, gribouillage de référence

Gallery
Boris, Second Life

Gallery
Boris, Champions Online

Gallery
Boris, Elder Scrolls Online

Gallery
Boris, Aion

Gallery
Boris, Guild Wars 2

Gallery
Boris, Dragon's Dogma

Ce script souffre de certaines limitations qui doivent être prises en compte.

  • En l'état, cette galerie retire les éléments "gallery-item" de leur contexte, pour les afficher en position:fixed. Ça signifie qu'en arrière-plan, l'élément disparaît de la galerie. Rien de vraiment gênant, mais il serait sans doute judicieux d'afficher uniquement l'image en fixed, plutôt que le paragraphe tout entier.
  • Seule une galerie peut être affichée par page : si vous répartissez les images de par l'article, elles seront néanmoins toutes rassemblées au même endroit.
  • Dans la galerie, les images sont potentiellement rognées. C'est un choix de facilité qui peut déplaire.
  • Bien que ça ne soit pas réellement un problème pour des navigateurs et des ordinateurs modernes, les performances de ce script sont faibles. Par exemple, le scan de l'intégralité des paragraphes de l'article et l'utilisation de pseudo-sélecteur jQuery (:has et compagnie) sont deux points qui pourraient facilement être améliorés.
  • Pour des raisons que je ne saisis pas encore, Edge plante méchamment avec ce script de galerie : l'image sélectionnée passe au second plan et n'apparaît que si vous scrollez le long de la page. Étant donné que tout fonctionne (plus ou moins) bien avec Internet Explorer 11, je présume qu'il s'agit là d'un bug de Edge qui finira par être corrigé à terme.
  • Problème récurrent avec Ghost, toutes les images n'ont qu'une seule taille. Autrement dit, il n'est actuellement pas possible d'afficher des images miniatures et compressées dans la galerie, puis de charger la version complète au moment du click.

J'espère que cet article sera utile à quelques personnes. Si parmi les lecteur/trice/s, quelqu'un est motivé à m'aider, il serait envisageable de passer aux choses sérieuses en développant une application pour Ghost, dès que la mise à jour censée en apporter le support sera publiée.

Pierre Vanhulst

Pierre Vanhulst

PhD et Assistant de recherche au sein de l'institut Human-IST (Fribourg) et du World Trade Institute (Berne). Fervent rôliste depuis 1996.

Read More