Audit complet de mise en forme — Image (2e élément du tableau)

Implémente toutes les options manquantes identifiées pour l'élément
Image : dimensionnement/ratio/object-fit, filtres CSS, upload de
fichier (en plus de l'URL), lien/plein écran au clic, chargement
différé, légende, et tous les attributs de boîte partagés déjà créés
pour Titre/Paragraphe (padding/margin/fond/bordure/ombre/opacité/
position du bloc).

Système de pages : un support peut désormais avoir 0 page (un nouveau
support démarre vide), suppression de toutes les pages en un clic, et
la pagination automatique insère intelligemment la nouvelle page juste
après celle qui déborde plutôt qu'en toute fin de liste.

Bugs réels trouvés et corrigés en cours de route : le style de bloc
(dont align-self) ciblait l'élément interne au lieu de son enveloppe
(légende/lien) ; une image à sa taille native pouvait déclencher une
pagination infinie ; upload/mise à jour d'attribut ne déclenchaient
jamais le contrôle de débordement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-26 12:06:44 +02:00
co-authored by Claude Sonnet 5
parent 6c7675fad0
commit 9ad50c58b8
27 changed files with 1299 additions and 174 deletions
+166 -1
View File
@@ -64,6 +64,12 @@ image de fond, `overflow`/`z-index`/position absolue, transitions au
survol, réglages différents par taille d'écran (responsive), arrondi
par coin (un seul rayon pour les 4 coins, comme l'étiquette).
**Changement rétroactif (au moment de l'Image, ci-dessous)** : `width`
(largeur fixe) a été ajouté à `box_style.py` partagé — Titre/Paragraphe
gagne donc aussi ce réglage a posteriori (en plus de la largeur MAXIMALE
déjà là), sans repasser par une validation dédiée puisque c'est un ajout
pur (aucun comportement existant modifié).
Bugs transversaux trouvés et corrigés pendant ce chantier (concernent
TOUT l'éditeur, pas seulement Titre/Paragraphe) :
- Panneau Propriétés jamais reconstruit après un clic (boutons/segments
@@ -74,7 +80,166 @@ TOUT l'éditeur, pas seulement Titre/Paragraphe) :
à tort par les cases à cocher et curseurs, cassant leur apparence
native.
### 2. Image — à faire (prochain élément)
### 2. Image — ✅ audité et validé (commité)
Implémenté : légende (`caption`, enveloppe dans `<figure>/<figcaption>`),
ajustement dans son cadre (`object_fit` : taille réelle/couvrir/
contenir/étirer), format/ratio (`aspect_ratio` : libre/carré/4:3/16:9),
filtre (3 préréglages : noir et blanc/sépia/flouté), chargement différé
(`loading="lazy"`), comportement au clic mutuellement exclusif (aucun /
lien externe dans un nouvel onglet / plein écran — un vrai overlay
plein écran côté client, `forgeDocOpenImageLightbox`), tous les
attributs de boîte partagés (dont la nouvelle **largeur fixe**, ajoutée
à `box_style.py` à cette occasion).
Comme pour les mini-jeux et la pièce jointe d'un bouton, le lien et le
plein écran ne sont réellement cliquables qu'en Mode Aperçu — en
édition, le clic sélectionne l'élément (la navigation native du lien
est bloquée pour ne pas quitter l'éditeur par accident).
**Ajout (retour utilisateur : "il manque la possibilité d'uploader une
image")** : téléversement d'un fichier depuis l'ordinateur (PNG/JPG/
GIF/WEBP/SVG), en plus du champ URL externe déjà là — mirroir exact du
mécanisme déjà en place pour la pièce jointe d'un bouton :
`routes/document/document_element_upload_image.py` (stocke sous
`uploads/`, jamais `attachments/` — pas de `as_attachment`, l'image doit
s'afficher, pas se télécharger) et `routes/document/
document_uploaded_file.py` (route de service dédiée). Le fichier
téléversé vide `svg_markup` au passage (les deux modes ne coexistent
jamais). Le panneau propose maintenant les deux : sélecteur de fichier
(natif, pas de champ texte) en premier, champ URL externe en second
pour une image déjà hébergée ailleurs.
Volontairement laissé de côté (complexité/valeur douteuse pour du
contenu de formation) : `mix-blend-mode`, découpe `clip-path`, overlay
au survol, arrondi par coin, réglages responsive par taille d'écran.
Bug pré-existant corrigé au passage : `docImageSrc`/`docImageAlt`
n'étaient pas échappés avant insertion dans l'attribut `value` du
panneau Propriétés (mineur, mais corrigé puisque cette fonction était
déjà réécrite).
**Bug réel corrigé (retour utilisateur : "la position de bloc ne
fonctionne pas sur l'image")** : `render_box_style` (padding/margin/
fond/bordure/largeur/position du bloc, dont `align_self`) était
appliqué à l'`<img>`/`<div>` INTERNE, jamais à son enveloppe
(`<figure>`/`<a class="docImageLink">`/`<div class=
"docImageLightboxTrigger">`) quand une légende ou un comportement au
clic en ajoutait une — `align-self` posé sur un simple descendant du
flex-item n'a aucun effet CSS, d'où le bouton "position du bloc" sans
effet visible dès qu'une légende ou un lien était configuré. Corrigé
dans `_render_image` (`document_engine/rendering/
render_document_element.py`) : le style de bloc cible désormais
toujours l'élément réellement top-niveau (enfant direct de
`.docPageContent`), quel que soit l'emboîtement. Changement de
comportement réel sur du contenu existant : une image avec légende/lien
et un fond/une bordure/un padding déjà réglés les verra désormais
appliqués à TOUT le bloc (image + légende), pas seulement à l'image —
c'est le comportement correct/attendu, mais je le signale car ça change
le rendu visuel d'éléments déjà créés.
**Bug réel corrigé (retour utilisateur : "quand j'ajoute une image ça
crée des pages à l'infini")** : une photo importée à sa taille native
pouvait dépasser une page entière à elle seule ; la pagination
automatique (`forgeDocCheckPageOverflow`, static/document/js/
document-editor.js) la déplaçait alors sans fin vers une page neuve,
qui débordait identiquement. Corrigé une première fois par un garde-fou générique (si tout le
contenu de la page déborde déjà, rien à répartir, on arrête) + une
hauteur maximale par défaut sur `img.docImage` (plafonnée à la hauteur
intérieure de la page, `static/document/document-editor.css`).
**Affiné ensuite (retour utilisateur : "si l'image uploadée est trop
grande je préfère qu'elle soit redimensionnée plutôt que bouger sur une
autre page")** : `forgeDocCheckPageOverflow` (static/document/js/
document-editor.js) évite de paginer une image qui déborde quand il
reste assez de place pour un résultat encore utilisable — elle est
rétrécie SUR PLACE (`max-height` posé en style inline,
calculé à partir de l'espace réellement disponible sous elle sur SA
page actuelle, légende comprise) à chaque rafraîchissement du canevas.
Purement visuel, jamais persisté en attribut (l'espace disponible
dépend du contenu au-dessus, qui change en éditant). Un réglage
explicite de hauteur maximale via le panneau reste prioritaire (une
valeur inline posée par un attribut serait recalculée par-dessus à
l'affichage suivant si elle déborde encore). Le garde-fou anti-boucle
et le défaut CSS restent en place pour les AUTRES kinds et comme filet
de sécurité au tout premier rendu (avant que le JS n'ait tourné).
**Changement de conception (retour utilisateur : "ce cadre ne devrait
pas changer de taille en fonction de la taille de l'image mais être
fixe et contraindre l'image dedans")** : plutôt que de rétrécir
dynamiquement une image trop grande selon l'espace disponible (approche
fragile, source des deux bugs ci-dessus), un nouvel élément image a
maintenant un cadre FIXE dès sa création — `object_fit="cover"` +
`height="220px"` par défaut au lieu de vides (`document_engine/labels/
element_kind_labels.py`) — une photo importée est donc TOUJOURS rognée
pour remplir ce cadre, quelle que soit sa résolution native. "Taille
réelle" reste un choix explicite possible dans le panneau (segmented
"Ajustement dans son cadre"). **Ne s'applique qu'aux NOUVEAUX éléments
image** — un élément déjà créé avant ce changement garde ses attributs
`object_fit`/`height` existants (jamais re-migré automatiquement,
`sanitize_element_attributes` ne touche pas au kind "image", voir sa
docstring) ; pour en faire bénéficier une image déjà présente, régler
manuellement "Couvrir" + une hauteur via le panneau. Le rétrécissement
dynamique (garde-fou anti-boucle + `forgeDocCheckPageOverflow` appelé
après upload/mise à jour d'attribut) reste en place comme filet de
sécurité pour une hauteur explicite déraisonnablement grande.
**Bug réel corrigé une seconde fois (retour utilisateur : "ce n'est pas
redimensionner")** : le rétrécissement ci-dessus vit dans
`forgeDocCheckPageOverflow`, appelée uniquement par
`forgeDocRefreshCanvas` (ajout/déplacement/suppression/Annuler-
Rétablir) — mais **téléverser un fichier dans une image existante**
(`forgeDocApiUploadImage`) et **changer un attribut quelconque**
(`forgeDocUpdateAttributes`, le point d'entrée central de tous les
panneaux) patchent chacun le DOM directement, SANS jamais passer par
ce chemin : le rétrécissement ne se déclenchait donc jamais après un
upload. Corrigé en appelant explicitement `forgeDocCheckPageOverflow()`
à la fin de ces deux fonctions. Pas de test automatisé possible ici :
aucune suite de tests n'existe pour `document-editor.js`
(`package.json` ne couvre que `static/game/js/`), vérification
manuelle uniquement.
**Affiné une troisième fois (retour utilisateur : "l'ajout de page
quand le contenu déborde dois rester et même être plus intelligent, la
nouvelle page dois etre sous la page qui deborde meme si ya d'autre
page")** : la pagination automatique (pour tout kind, pas seulement les
images désormais épargnées ci-dessus) insérait toujours la page neuve
en toute fin de la bande d'onglets (`add_document_page` l'ajoute
toujours à la fin), même si d'autres pages existaient déjà après celle
qui déborde — déborder sur la page 2 d'un support qui en compte 5
ajoutait la nouvelle page en position 6 au lieu de 3. Corrigé dans
`forgeDocCheckPageOverflow` : la page est créée puis immédiatement
déplacée (`forgeDocApiPageMove`, mécanisme déjà existant pour le
glisser-déposer du panneau Pages) juste après la page active, décalant
les pages suivantes d'un cran. Extraction d'un helper partagé
`forgeDocReorderLocalPage` (état client après un déplacement) réutilisé
par le glisser-déposer ET par ce nouveau cas, pour ne pas dupliquer ce
calcul. Vérification côté serveur déjà couverte par les tests existants
de `move_document_page`/`document_page_move` (l'insertion "au milieu"
de la bande d'onglets y est déjà testée) ; le déclenchement côté client
reste manuel faute de suite de tests JS.
**Affiné une quatrième fois** : sous un seuil de place restante
(`FORGE_DOC_MIN_IMAGE_HEIGHT`, 60px) il n'y a plus de place RÉELLE sur
la page (pas seulement pour cette image) — l'image bascule alors dans
la pagination normale ci-dessus au lieu d'être rétrécie à une taille
inutilisable, ce qui empêchait sinon une page pleine d'images
d'enchaîner sur une nouvelle page. Vérifié par un test de bout en bout
en conditions réelles (navigateur automatisé Playwright contre le
serveur local, sur un support jetable créé puis supprimé pour
l'occasion) : 5 images vides ajoutées à la suite produisent bien 3
pages, chacune avec le cadre fixe "Couvrir" actif par défaut.
**Audit final** : chaque ligne du tableau d'audit initial pour
l'élément Image est couverte — implémentée (dimensionnement/ratio/
object-fit, bordure+ombre via `box_style.py`, filtre, lien/lightbox/
lazy-load/légende, position du bloc dont l'alignement, upload de
fichier) ou explicitement différée ci-dessus avec sa raison
(`object-position`, `mix-blend-mode`, `clip-path`, overlay au survol,
arrondi par coin, responsive par taille d'écran — mêmes exclusions que
Titre/Paragraphe, valeur douteuse pour du contenu de formation). Rien
d'oublié constaté à cette relecture. Validé par l'utilisateur, prêt à
committer.
### 3. Bouton — à faire