Nouveau blog -- partie 3 : JavaScript
Dans la première partie je parlais du contenu en HTML, dans la seconde des outils CSS que j'ai utilisés pour vous présenter le contenu plus proprement, que reste-t-il à présenter ?
Vous êtes en train de lire un blog généré statiquement, il y a assez peu d'interaction possible à part des liens (logique pour un blog au fond), donc que manque-t-il ? Techniquement rien. En vrai j'aurai pu m'arrêter là, mais j'ai ajouté une fonctionnalité de zoom sur deux parties des articles : les images et les galeries d'images, et pour que ça fonctionne bien j'ai été obligé d'ajouter du JavaScript.
Sans JavaScript🔗
Pour moi c'était important que tout fonctionne sans JavaScript. Je suis peut-être vieux jeux sur ce coup-là, mais j'estime que si vous arrivez avec un grille-pain avec un navigateur textuel vous devriez pouvoir lire mon blog (si vous lisez ce blog sur un grille-pain via un navigateur textuel : envoyez-moi une photo, ça me fera un kiff de ouf !).
Donc voyons ce que ça donne sans JavaScript :
À priori rien de particulier à signaler. Mais si maintenant on ajoute le JavaScript ?
Vous noterez le changement de curseur au survol d'une image pour indiquer qu'on peut cliquer, au clic le zoom pour passer l'image en plein écran. Pareil pour les galeries mais en plus l'option de pouvoir défiler les images de la galerie en cours.
L'amélioration est petite (mais indispensable pour certains articles à mon sens), mais elle est là. Et je l'ai fait sans aucune librairie externe, avec deux Web Components très simple.
Web Component ? kézako ?🔗
Un Web Component c'est simplement le nom qu'on donne à une brique JavaScript qui est composé d'un Custom Element, de Shadow DOM et des éléments <template> et <slot>.
C'est assez réducteur comme définition à mon sens, j'ai par exemple bien créé deux Web Component mais en utilisé aucune des briques cités, mais c'est généralement comme ça qu'on crée un Web Component.
Mais restons sur la définition de base :
class BlogIcon extends HTMLElement {
constructor() {
super();
this.innerHTML = this.getAttribute('icon') + this.getAttribute('icon');
}
}
customElements.define('blog-icon', BlogIcon);JavaScript
Et à l'usage :
<blog-icon icon="🖌️"></blog-icon>HTML
C'est pas très utile mais ça donne une idée simple de comment tout s'articule. On crée une classe qui étend HTMLElement (ou n'importe quel autre élément, par exemple HTMLParagraphElement pour créer un paragraphe personnalisé), on lui ajoute un constructeur qui appelle le constructeur parent, puis on agit (l'idéal serait de passer par les lifecycles mais ce n'est pas obligatoire), ici je vais afficher dans le composant le paramètre deux fois.
Une fois la classe créée il faut indiquer cette classe comme étant un Web Component au navigateur en passant par customElements.define('nom-du-composant', ClassDuComposant);.
Pour l'utiliser on va ensuite ajouter l'élément personnalisé qu'on a choisi (un custom element ne peut pas être self-closed, donc <blog-icon icon="🖌️"/> aurait été invalide).
Note: vous devez avoir un nom de composant en 2 parties avec un
-sinon vous aurez l'erreurDOMException: CustomElementRegistry.define: 'blogicon' is not a valid custom element nameau moment de la déclaration. En général c'est une bonne pratique d'avoir un préfixe pour notre application. Pour mon exemple le préfixe seraitblog-.
Simple non ?
Là j'ai fait un Web Component sans shadowDOM mais le ShadowDOM permet d'avoir un cloisonnement du style pour éviter d'avoir à s'occuper du reste du style de page et d'éventuel conflit. C'est une bonne pratique d'utiliser du ShadowDOM, particulièrement si votre objectif c'est de partager le composant.
Composant de zoom d'image🔗
Bon maintenant qu'on sait faire un Web Component, on veut faire la même chose, mais sans custom element (en gardant juste la balise <img />), sans changer le comportement de base, juste "améliorer" le comportement de la balise <img/> pour ne pas avoir à attendre que le JS se charge pour avoir les images (comme toujours : progressive enhancement, pas de JS obligatoire, etc.).
Pas de ShadowDOM non plus parce que je ne veux pas avoir un style appliqué plusieurs fois sur la page, un style global ça me va bien (mais appliqué seulement si on charge de Web Component).
Donc comment on fait ?
Déjà, on va utiliser les options de customElements.define() :
customElements.define("img-zoom", ImageZoom, { extends: "img" });JavaScript
Ici on dit au navigateur que notre composant ne va pas s'appliquer sur <img-zoom></img-zoom> mais sur <img is="img-zoom" />. Du coup, il faut aussi changer le HTML pour que nos images aient l'attribut is="img-zoom".
Comme je voulais quelque chose de propre et éviter d'ajouter du JavaScript pour rien, j'ai choisi de le faire via mon plugin Marked en ouvrant la possibilité d'ajouter un paramètre is, donc il suffit d'ajouter le plugin avec le bon paramètre markedBetterImage({ is: "img-zoom" }), c'est particulièrement intéressant que ça permet de ne cibler que les images qui ne sont pas dans des galeries d'images. Pour les galeries je vais fonctionner de la même façon mais avec le plugin markedImageGallery({ is: "gallery-zoom" }).
On passe donc de :
<figure>
<img src="./img/print_1.min.jpg" alt="Des jetons"/>
<figcaption>Des jetons</figcaption>
</figure>HTML
à
<figure>
<img src="./img/print_1.min.jpg" alt="Des jetons" is="img-zoom"/>
<figcaption>Des jetons</figcaption>
</figure>HTML
Donc presque pas de différence dans le HTML mais beaucoup d'options possibles derrière !
La classe ImageZoom est finalement assez simple :
class ImageZoom extends HTMLImageElement {
constructor() {
super();
this.addEventListener("click", () => {
this.zoomIn = !this.zoomIn;
});
}
get zoomIn() {
return this.getAttribute("data-zoom") === "zoom";
}
set zoomIn(flag) {
if (flag) {
this.setAttribute("data-zoom", "zoom");
} else {
this.removeAttribute("data-zoom");
}
}
}JavaScript
L'idée étant juste d'ajouter ou enlever l'attribut data-zoom (en lui passant la valeur zoom dans le doute d'avoir besoin d'une autre valeur un jour) en fonction de si on zoom ou dé-zoom.
Et du coup comment ça marche ? Du CSS tout simplement !
Dans le même fichier que mon Web Component j'ai ça :
const style = document.createElement("style");
style.textContent = `
@media (width >= 700px) {
img[is="img-zoom"] {
cursor: zoom-in;
&[data-zoom=zoom] {
cursor: zoom-out;
background-color: var(--color--bg);
position: fixed;
padding: var(--size--1);
margin: auto;
inset: 0;
object-fit: contain;
width: 100%;
min-width: unset;
max-width: 100%;
height: 100%;
min-height: unset;
max-height: 100%;
z-index: 10000;
}
}
}
`;
document.body.appendChild(style);JavaScript
Uniquement sur un grand écran (supérieur à 700px, comme pour le seul break-point que j'ai dans mon style), si une balise <img/> a l'attribut is="img-zoom" alors je change le curseur, et si en plus on retrouve l'attribut data-zoom="zoom" alors je change à nouveau le curseur et je change la position et taille de l'image pour qu'elle prenne le maximum possible de taille de l'écran.
C'est finalement assez simple à faire, j'ai surtout joué avec le style pour que tout fonctionne.
Pour la galerie, c'est pratiquement la même chose mais en ajoutant des boutons supplémentaires, en ajoutant du style pour positionner un peu tout, en ajoutant un peu de mécanique pour appliquer l'attribut data-zoom="zoom" sur une seule image et qu'on puisse déplacer l'attribut d'image en image. C'est plus long mais pas tellement plus compliqué. Si vous êtes curieux, je vous laisse inspecter le script gallery-zoom.mjs qui se charge sur cette page.
Conclusion🔗
C'est clairement la partie la plus courte de toute la série. J'aurais pu mettre du JavaScript partout, faire des animations / interactions de fou ! Mais ça n'aurait pas été nécessaire…
J'ai fait quelque chose de simple. Ça me va parfaitement. J'aurai surement d'autres Web Components à ajouter pour quelques petites idées mais pas beaucoup non plus. Je reste sur l'idée que la page de mes articles doit se charger vite !
Crédit photo : Générée via Mistral AI avec le prompt suivant :
Une scène chaleureuse et immersive, inspirée par l’esthétique poétique de Studio Ghibli, représentant un laboratoire d’alchimie numérique en bois clair et vieilli, baigné d’une lumière dorée et bleutée tamisée, comme un crépuscule d’automne. Au centre, un panda roux anthropomorphe (Firefox), vêtu d’une blouse d’alchimiste ample et texturée, mélange des fioles remplies de lignes de code JavaScript qui s’illuminent de reflets dorés et bleutés. Les fioles sont disposées sur une table en bois couverte de schémas techniques dessinés à la main et de parchemins affichant des extraits de code.
Des bulles de code flottent autour de lui, s’animant et interagissant avec des éléments HTML/CSS stylisés, comme des particules magiques. Des écrans holographiques (avec des cadres en bois sculpté) affichent des animations discrètes : un bouton qui réagit au survol, une liste qui se trie dynamiquement, ou un graphique qui se met à jour en temps réel.
En arrière-plan, une grande fenêtre donne sur un paysage nocturne aux étoiles filantes et aux néons bleutés, reflétant des motifs de code binaire sur les murs en bois. Des feuilles d’érable flottent doucement dans l’air, ajoutant une touche magique à la scène.
À droite, en bas, le panda roux est légèrement tourné en 3/4 dos, lové sur un coussin, entouré d’un halo lumineux subtil. Il ne dépasse pas le tiers de la hauteur de l’image et observe ses expériences avec curiosité et satisfaction.
L’ambiance est cosy, onirique et high-tech : les couleurs sont douces (beiges, bleus pâles, oranges chauds), avec des jeux de lumière tamisée qui créent des ombres chaleureuses. Les détails sont soignés, avec des textures visibles (grain du bois, tissu de la blouse, pelage du panda roux), et une tasse de thé fumant posée près des fioles. Le tout respire la sérénité et l’inspiration, comme un moment suspendu entre la magie et la technologie.""Une scène chaleureuse et immersive, inspirée par l’esthétique poétique de Studio Ghibli, représentant un laboratoire d’alchimie numérique en bois clair et vieilli, baigné d’une lumière dorée et bleutée tamisée, comme un crépuscule d’automne. Au centre, un panda roux anthropomorphe (Firefox), vêtu d’une blouse d’alchimiste ample et texturée, mélange des fioles remplies de lignes de code JavaScript qui s’illuminent de reflets dorés et bleutés. Les fioles sont disposées sur une table en bois couverte de schémas techniques dessinés à la main et de parchemins affichant des extraits de code.
Des bulles de code flottent autour de lui, s’animant et interagissant avec des éléments HTML/CSS stylisés, comme des particules magiques. Des écrans holographiques (avec des cadres en bois sculpté) affichent des animations discrètes : un bouton qui réagit au survol, une liste qui se trie dynamiquement, ou un graphique qui se met à jour en temps réel.
En arrière-plan, une grande fenêtre donne sur un paysage nocturne aux étoiles filantes et aux néons bleutés, reflétant des motifs de code binaire sur les murs en bois. Des feuilles d’érable flottent doucement dans l’air, ajoutant une touche magique à la scène.
À droite, en bas, le panda roux est légèrement tourné en 3/4 dos, lové sur un coussin, entouré d’un halo lumineux subtil. Il ne dépasse pas le tiers de la hauteur de l’image et observe ses expériences avec curiosité et satisfaction.
L’ambiance est cosy, onirique et high-tech : les couleurs sont douces (beiges, bleus pâles, oranges chauds), avec des jeux de lumière tamisée qui créent des ombres chaleureuses. Les détails sont soignés, avec des textures visibles (grain du bois, tissu de la blouse, pelage du panda roux), et une tasse de thé fumant posée près des fioles. Le tout respire la sérénité et l’inspiration, comme un moment suspendu entre la magie et la technologie.