CM3: Exécuter du code dans une page Web (côté client)

Du code sur le navigateur ?

HTML permet de définir la structure de la page Web, et CSS son affichage.

Cependant, pour générer du contenu dynamique et gérer les interactions avec l'utilisateur, il est nécessaire de pouvoir exécuter du code arbitraire sur le navigateur.

Pour cela, le navigateur est capable d'interpréter nativement deux langages :

Pour utiliser d'autres langages, il faut alors les convertir soit en JavaScript, soit en WebAssembly (e.g. avec emscriptem).

⚠ Contrairement à JavaScript, le WebAssembly n'a pas un accès direct au DOM.

Afin de vous éviter d'avoir à apprendre un nouveau langage, nous utiliserons Brython qui permet d'exécuter du code Python sur le navigateur. L'un des avantages de Brython est qu'il permet d'utiliser les API natives du navigateur.

Pour les VCOD, nous verrons par la suite TypeScript en Programmation Web (S4) et Programmation Web pour la Visualisation (S5). Vous apprendrez donc un nouveau langage, mais serez déjà familiarisés avec les API utilisées.

Exécuter du code dans la page Web

Dans l'en-tête de la page Web, permet d'exécuter du code sur la page :

💡 indique le type du script (ici un module JavaScript).

💡 permet d'indiquer quand exécuter le code inclus :

⚠ Il recommandé d'attendre que la page ai finie de se charger avant de la manipuler. En effet, si le code HTML n'a pas fini d'être interprété lors de l'exécution du script, certains éléments ne seront pas encore chargés dans le DOM.

La console de la page Web

Les erreurs survenant durant l'exécution du code sont affichées dans la console de la page Web. [TODO: ouvrir la console de la page Web]

La console gère plusieurs niveau d'affichages dont vous pouvez filtrer l'affichage :

💡 Vous pouvez formatter l'affichage dans la console en préfixant le texte par %c, e.g. console.log("%cOK", "color: red")

💡 Vous pouvez manipuler la console via :

💡 console offre d'autres fonctions d'affichages :

💡 Cette console permet aussi d'entrer du code JavaScript à exécuter grâce à une interface REPL (Read–eval–print loop).

Spécificités de Brython

L'API du navigateur est accessible à partir du module browser.window : from browser import window

En JavaScript, représente la "fenêtre" de la page Web actuelle. Il contient l'ensemble des variables/classes/fonctions globales de la page Web. En particulier, représente l'arbre DOM de la page Web.

💡 En JavaScript, est implicite, i.e. on peut écrire au lieu de . En Brython, il faudra obligatoirement expliciter .

⚠ Afin que les objets Python s'affichent correctement, il est important d'utiliser la version Brython de en l'important via : from browser import console

⚠ Brython a aussi du mal à distinguer les classes des fonctions JavaScripts. Pour instancier une nouvelle classe il faudra ainsi écrire window.URL.new() au lieu de URL(). [TODO: corrigé par SBrython]

Manipulation du DOM

Sélecteurs

Pour interagir avec le DOM, il convient de sélectionner les éléments HTML sur lesquels on souhaite interagir.

Pour cela on utilise qui retourne le (ou les) descendants de correspondant au sélecteur :

💡 retourne le premier ancêtre correspondant au sélecteur .

💡 retourne un booléen indiquant si l'élément correspond au sélecteur .

💡 Dans un sélecteur, permet de désigner l'élément à partir du lequel on appelle ces fonctions.

💡 Vous pouvez utiliser ces fonctions sur , mais aussi sur :

💡 Il existe aussi les sélecteurs XPath, plus puissants, mais plus complexes.

Créer un élément HTML

Vous pouvez aussi créer un élément HTML via :

Manipuler les attributs et classes

En premier lieu il est possible de modifier les attributs et les classes d'un élément.

Opération Classes Attributs
Lister
Obtenir
Ajouter
Supprimer
Basculer
Contient ?

💡 permet d'obtenir le nom de la balise HTML.

Manipuler les attributs data-*

Les attributs sont un peu particuliers. En effet, ils peuvent être manipulés via , mais aussi via qui se manipule comme un dictionnaire associatif. Chaque clé de correspondant à un attribut de l'élément HTML.

Ils sont utilisés afin que les développeurs puissent définir des attributs personnalisés, sans entrer en conflit avec les attributs définis par HTML, e.g. .

Manipuler le style

La modification de l'affichage d'un élément se fait usuellement via la modification de ses classes. Il est cependant tout à fait possible de manipuler directement ses propriétés CSS. Dans ce cas de figure, on préfère généralement se limiter à la manipulation des propriétés personnalisées.

Les styles peuvent être accédés de deux manières distinctes :

Il est alors possible d'accéder ou de modifier les propriétés CSS via :

Opération
Obtenir
Définir
Supprimer

⚠ Pour obtenir des positions d'éléments aisément comparables, utilisez qui retourne la position relativement à la zone d'affichage (i.e. la fenêtre).

Manipuler le contenu

Il existe beaucoup de manière d'obtenir un (ou des) élément(s) à partir d'un autre. Nous verrons les plus génériques :

Il existe beaucoup de manière de modifier le contenu d'un élément. Nous verrons les plus génériques :

⚠ Un élément ne peut être qu'à un seul endroit à la fois. L'insérer dans un élément le retirera donc de son précédent père.

Événements

-> [zone d'affichage, comparer des positions sinon infernal] -> .scroll()

Principe

-> boucle d'events & promesses async (différent en Python) (corrigé avec SBrython)

-> trigger

Types d'évents

TODO...

Delegated events

Debounce/Throttle/setAnimationFrameRequest/hoist.

Signaux

Formulaires et manipulation d'URL

Autres éléments d'intéractions

-> attrs -> dialog/popover/details-summary -> CSS aussi... / caroussel / etc.

Autres choses possibles

-> profile/profileEnd() => profiler (perfs) -> CM5 ?

⚠ Pour des raisons de sécurité, ce code arbitraire n'a pas accès aux ressources locales (i.e. sur votre poste de travail), ou aux autres page Web. Imaginez en effet qu'à la simple visite d'une page Web, votre navigateur se mette à exécuter un code supprimant tous vos fichiers !

old

-> order of deps => dev log.

@starting-style for animations

:(user-)valid :(user-)invalid :has()

document. body/head. rootElement.

activeElement (qui a le focus). documentElement

Événements

Bubble vs Capture

Dans les navigateurs, les interactions sont gérées via des événements, qui se composent de 2 phases : Capture : La phase de capture est descendante, elle part du noeud racine et descend vers le noeud cible (target) final. Par exemple, lorsque vous cliquez sur un élément, l'événement de clic va d'abord partir du document, puis par le body. Le navigateur va propager l'événement en se demandant quel fils de l'élément courant est la cible de l'événement, afin de lui transmettre l'événement, puis recommence de manière récursive, l'élément fils cible devenant l'élément courant. Bubble : La phase de buble est montante, elle part du noeud cible (target) final et remonte vers le noeud racine. Si l'événement se propage dans le DOM (bubble: true), le navigateur va transmettre l'événement à l'élément père de l'élément courant, puis recommence de manière récursive, l'élément père devant l'élément courant.

Écouter un événement

// [JS] Javascript

function handler(ev) {
    // ev.currentTarget : $ELEM
    // ev.target : source of the event.
    // ev.type   : $EVENT_TYPE
    // ev.detail : $EVENT_DATA (si CustomEvent, cf plus bas)
    // ev.preventDefault() : annuler l'action par défaut du navigateur
    // e.g. lorsqu'on clique sur un lien, empêcher d'aller vers la page.
    // ev.stopImmediatePropagation() : ne pas appeler les autres
    // handleurs pour cet événement.
}

$ELEM.addEventListener($EVENT_TYPE, handler);
// ou
$ELEM.addEventListener($EVENT_TYPE, ev => ... );
# [🐍] Python

def handler(ev):
    # ev.currentTarget : $ELEM
    # ev.target : source of the event.
    # ev.type   : $EVENT_TYPE
    # ev.detail : $EVENT_DATA (si CustomEvent, cf plus bas)
    # ev.preventDefault() : annuler l'action par défaut du navigateur
    # e.g. lorsqu'on clique sur un lien, empêcher d'aller vers la page.
    # ev.stopImmediatePropagation() : ne pas appeler les autres
    # handleurs pour cet événement.

$ELEM.addEventListener($EVENT_TYPE, handler)

📖 Plus d'informations dans la documentation.

Écouteur délégué

Il arrive qu'on souhaite écouter des événements sur les descendants d'un élément, qui peuvent être ajoutés, déplacés, supprimés, etc. Le problème est que cela nécessiterait de créer un écouteur pour chaque éléments et de les supprimer/ajouter à chaque modifications du DOM.

Heureusement, il est possible d'utiliser un écouteur délégué, i.e. d'écouter l'événement sur le descendant (nécessite que l'événement soit bubble, i.e. se propage dans le DOM).

// [JS] Javascript

function handler(ev) {
    if( ev.target.matches($CSS_SELECTOR) ) {
        // ...
    }
    // or
    let target;
    if( target = ev.target.closest($CSS_SELECTOR) ) {
        // ...
    }
}

$ELEM.addEventListener($EVENT_TYPE, handler);
# [🐍] Python

def handler(ev):
    if ev.target.matches($CSS_SELECTOR):
        # ...
    # or
    if target := ev.target.closest($CSS_SELECTOR):
        # ...

$ELEM.addEventListener($EVENT_TYPE, handler)

Créer un événement

// [JS] Javascript

$ELEM.dispatchEvent( new Event($EVENT_TYPE) );
// ou
$ELEM.dispatchEvent( new CustomEvent($EVENT_TYPE, {detail: $EVENT_DATA}) );
# [🐍] Python

$ELEM.dispatchEvent( Event.new($EVENT_TYPE) )
# ou
$ELEM.dispatchEvent( CustomEvent.new($EVENT_TYPE, {"detail": $EVENT_DATA}) )

💡 Vous pouvez aussi ajouter, au 2ème argument, l'option bubble: true pour faire en sorte que l'événement soit bubble, i.e. se propage dans le DOM (par default bubble: false).