Restaure et termine le lot tableur resté non committé. Il n'existait que dans un `git stash` (24 fichiers suivis) et en fichiers non suivis (menus.js, formula.js, color-picker.js, tests, fiches) : la 2.53.3 livrée ne le contenait donc pas. Le stash, créé avant le commit BUG-108, n'a jamais été restauré. #180 Redimensionnement au curseur (en-tête de bord miroité + classe de repaint), grille thémée sur les 15 thèmes et les modes contraste élevé / sépia, menu Fichier au niveau des onglets, barre épinglée pleine largeur. #181 Modèle de saisie Google Sheets : sélection ≠ édition (caret masqué sans quitter contenteditable, double-clic / F2 / première frappe qui remplace, Entrée contextuelle, Échap qui restaure) + inventaire priorisé des écarts. #182 Désélection fiable (clic simple sans dépendre du focus), contours de plage non empilés (sélecteur de classes sans point), couleurs texte/fond sur une plage, clic droit qui préserve la sélection multiple. #183 Panneau de couleurs façon Google Sheets : palette 8 × 10, STANDARD, PERSONNALISÉ, coche selon la luminance, sortie `#rrggbb` (une valeur HSL était rejetée par normHex). #184 Peinture de format complète (toutes propriétés, source sans format = réinitialisation), sélection multi-lignes/colonnes depuis les marges, grille étendue : colonnes A → Z d'emblée, lignes ajoutées PAR BLOCS DE 100 au défilement jusqu'à 1000 — matérialiser 1000 lignes d'un coup = ~26 000 cellules câblées par feuille, ce qui épuisait le tas de la suite JSDOM ; index de cellules `ref → td`, court-circuits formule/styles, marqueur data-wired. #185 Barre de menus : les 10 menus Google Sheets (161 entrées, 125 câblées, 36 annoncées indisponibles), ruban façon Sheets, grille unie 1 px dérivée du thème. #186 « Créer un fichier » propose .xlsx et construit un vrai classeur OPC (openpyxl) au lieu d'une charge utile texte illisible. Corrections trouvées en restaurant et en exerçant le lot : - fuite mémoire : écouteur `click` anonyme posé sur #content-area à chaque rendu, jamais retiré — sa fermeture retenait la grille précédente en entier ; - sorties Markdown / HTML / Imprimer de la barre de menus inertes (`data-xlsx-export` jamais réparti, seul `data-xlsx-action` l'était) ; - collision de classe `.xlsx-structure-menu` entre la barre de menus et le menu Structure de la barre d'outils (toute requête tombait sur un nœud masqué) ; - curseur col/row-resize absent quand le pointeur est sur la table elle-même ; - l'export emportait les lignes et colonnes vides du quadrillage (CSV, Markdown, HTML, impression) ; - clic extérieur avalé par la grâce de 250 ms du menu contextuel (destinée au seul appui long tactile) ; - une entrée indisponible laissait la barre de menus ouverte. Tests : pytest 1595 passés / 2 ignorés ; ruff et mypy 0 erreur ; bandit 0 ; 33 suites frontend vertes (xlsx-viewer 163/163, xlsx-menus 19/19, xlsx-formula 14/14) ; E2E complet 132 passés / 12 ignorés ; E2E xlsx-viewer 19/19.
17 KiB
#181 — Éditeur tableur : modèle de saisie Google Sheets & inventaire des écarts
Statut : ✅ livré le 2026-10-08 (v2.54.0) — ouvert le 2026-10-05 | Impact : 🟡 Références : Roadmap · Changelog · #154 · #155 · #179 · #180
1. Constat
L'éditeur ne distinguait pas sélectionner une cellule et l'éditer :
- le curseur de texte clignotait en permanence dans la cellule focalisée
(tout est
contenteditable,caret-colorpar défaut) ; Entréevalidait puis faisaitblur()— le focus quittait la grille ;- l'indicateur de plage était un simple lavis, sans contour, et les en-têtes de la plage sélectionnée n'étaient pas reflétés ;
- la première frappe ajoutait au contenu existant au lieu de le remplacer.
Un utilisateur habitué à Google Sheets ou Excel perd le repère : il ne sait plus s'il est en train de naviguer ou de saisir.
2. Livré (A1-A3)
A1 — Deux états distincts : sélection et édition
Toutes les cellules portent la classe xlsx-cell et démarrent en
sélection (data-editing="") : contour visible, aucun curseur. Le
curseur est masqué via caret-color: transparent — donc sans laisser
contenteditable, ce qui préserve le collage, le clic et la frappe native.
L'édition s'atteint par trois voies, comme dans Google Sheets :
| Geste | Effet |
|---|---|
| Clic / flèches | sélectionne (pas de curseur) |
| Double-clic | édite sur place — caretRangeFromPoint place le curseur sous le pointeur |
| Première frappe | édite et remplace le contenu (jamais d'ajout) |
F2 |
édite, curseur en fin de texte |
Alt+flèche |
saut de ligne dans la cellule |
Échap |
quitte l'édition et restaure la valeur |
enterEditing() / exitEditing() centralisent la bascule. Un garde
editingRequested empêche le handler focus de la cellule d'annuler l'état
d'édition que enterEditing() vient d'installer (il se déclenche au moment du
focus).
A2 — Sélection multiple et indicateur visuel
Maj+flèches étend depuis l'ancre, glisser avec la souris aussi, plusMaj+clic — le mécanisme existait déjà, il est désormais complet et lisible.- Les cellules portent
.xlsx-selectedet quatre classes de bord (.xlsx-sel-top/bottom/left/right) : le bloc est contouré exactement comme dans Excel/Sheets, au lieu d'un lavis uniforme. Chaque cellule ne reçoit que les bords qu'elle porte réellement (les coins n'héritent pas d'un bord intérieur). - Les en-têtes couverts (lettres
A,B… et numéros1,2…) reçoivent.xlsx-hdr-selected: la plage se lit aussi dans les marges.
A3 — Entrée contextuelle
| État | Entrée |
|---|---|
| édition | valide, puis le focus passe à la cellule du dessous |
| sélection | bascule la cellule courante en mode édition |
Tab suit la même logique (validation + déplacement latéral en édition,
déplacement simple en sélection), et un clic simple après une édition abandonne
l'état d'édition sans laisser de curseur.
3. A4 — Inventaire des écarts vs Google Sheets / Excel
Rapprochement exhaustif impossible en une tranche. L'éditeur couvre déjà l'essentiel ; l'inventaire ci-dessous est l'état réel vérifié dans le code au 2026-10-05.
3.1 Déjà couvert
Édition de cellules · types et formats · recherche et remplacement ·
trier / filtrer · gel des volets · fusions · annuler / rétablir · presse-papiers
de plage · insertion / suppression de lignes, colonnes et feuilles ·
graphiques KPI du tableau de bord · commentaires · liens hypertexte (formule
=LIEN) · export (CSV, Markdown, HTML, impression) · impression ·
collaboration temps réel · import de formats · lecture seule .xls / .ods.
3.2 Manquants, par priorité
P1 — usage quotidien
- Poignée de recopie (fill handle) : le petit carré en bas à droite de la plage, glisser pour remplir séries et formules. Très attendu, absent.
- Remplacer (
Ctrl+H) : seule la recherche existe. - Validation de données : listes déroulantes, bornes numériques.
- Filtre automatique : l filtrage texte existe, pas les entêtes de filtre par colonne.
- Tri multi-critères et options de tri (couleur, taille, valeurs personnalisées).
- Déplacer lignes / colonnes par glisser-déposer (l'insertion existe).
P2 — présentation et analyse
- Mise en forme conditionnelle (couleurs, barres de données, jeu d'icônes).
- Graphiques dans le tableur : KPI et relevé de graphiques existants sont lus, mais on ne crée pas de graphique.
- Tableaux croisés : lus et affichés, non modifiables.
- Groupement / dégroupement de lignes et colonnes.
- Images et formes dans les cellules (ObsiGate a Excalidraw séparé).
- Protection d'une feuille ou de cellules (lecture seule).
- Masquer des lignes / colonnes.
P3 — avancé
- Analyse de données : supprimer les doublons, convertir du texte en colonnes, supprimer les espaces superflus.
- Fusion des cellules à l'identique, annotations, suivi des modifications, historique des révisions.
- Macros (les
.xlsmsont lus et réécrits aveckeep_vba). - Simulation de scénarios, recherche de cible (goal seek).
- Options de tri avancées (insensible à la casse, séparateur décimal personnalisé).
- Peintures de cellules, texte en art.
4. Tests
tests/frontend/xlsx-viewer.test.mjs — 146/146 (10 nouveaux cas #181) :
sélection sans curseur au clic et aux flèches · double-clic qui édite en place ·
première frappe qui remplace · deuxième frappe qui n'efface pas ·
Entrée qui passe en édition · Entrée qui valide et descend sous la cellule ·
extension Maj+flèches avec les quatre bords du contour et les en-têtes
reflétés · glisser qui peint le même contour · clic simple qui fait tomber
l'état d'édition · Échap qui restaure la valeur.
Suites frontend également vertes : xlsx-menus 12/12, unit 13, editor-inline
44, toolbar-order, mobile-editor 35, ai 100, pane-manager 9,
validate-imports 41 modules / 355 exports.
5. Point d'attention
Le caret-color: transparent repose sur la prise en charge navigateur
(caret-color est largement disponible). Sur un très ancien moteur, le curseur
resterait visible en sélection — l'état reste correct, seul l'indicateur
disparaît.
6. #182 — Désélection & couleurs sur une plage
6.1 La sélection « collée »
Symptôme : après un Maj+clic ou un glissement, la plage restait affichée et
un clic simple ne la supprimait pas.
Cause racine — la désélection reposait entièrement sur l'événement focus de
la cellule, qui appelle setActiveCell() et donc réduit la plage à la cellule
focalisée. Or le handler Maj+clic appelle e.preventDefault(), ce qui
supprime le déplacement de focus natif du navigateur. Résultat : après un
Maj+clic, activeTd valait la cellule cliquée mais document.activeElement
restait sur l'ancienne. Le clic suivant sur la cellule déjà focalisée ne
déclenchait donc aucun focus : la plage ne pouvait plus s'effacer.
Correctif
- le clic simple appelle
setActiveCell(td)dansmousedown: la plage est effacée de façon déterministe, sans dépendre du focus ; - le
Maj+clic déplace réellement le focus, soussuppressSelectionResetpour que le handlerfocusne rétrécisse pas la plage qui vient d'être étendue ; - le glissement ne vole pas le focus (comme dans un navigateur) — c'est précisément ce vol qui aurait fait disparaître la plage tracée ;
endDragrestaure la zone Nom sur la plage (A1:B2) au lieu de la cellule d'origine.
Le même correctif a guéri le symptôme connexe « la mini-barre de sélection ne se cachait plus après un glissement ».
6.2 Couleurs de texte et de fond
Les trois chemins ont été vérifiés (charge utile PUT …/xlsx/style puis
peinture locale) sur une plage A1:B2 obtenue par Maj+flèches et par
glissement :
| Chemin | Charge utile | Résultat |
|---|---|---|
| Menu Mise en forme → couleur | range: "A1:B2" |
4 cellules peintes |
| Nuancier du ruban (police) | range: "A1:B2" |
4 cellules peintes |
| Nuancier du ruban (fond) | range: "A1:B2" |
4 cellules peintes |
Le mécanisme d'écriture était donc correct ; ce qui manquait réellement, c'était
le retour visuel : les deux nuanciers du ruban n'affichaient rien, d'où
l'impression que la couleur n'était pas appliquée. Ils portent désormais une
pastille (.xlsx-color-swatch) qui prend la couleur choisie.
6.3 Tests
tests/frontend/xlsx-viewer.test.mjs — 149/149 (+3 cas #182) :
- un clic simple réduit toujours une plage « collée » (y compris sur la cellule qui porte déjà le focus) ;
- la zone Nom affiche la plage après un glissement ;
- couleurs de texte et de fond appliquées aux 4 cellules d'une plage, charge utile vérifiée, pastilles synchronisées.
Note d'implémentation : la peinture locale est asynchrone (elle suit l'écriture
PUT), un test qui lit les styles inline doit donc attendre le tour de boucle d'événements.
6.4 Les contours qui s'empilaient (le point manquant)
Symptôme : en déplaçant le focus de cellule en cellule, les encadrés de l'ancienne cellule restaient et s'accumulaient — la grille finit quadrillée.
Cause racine — le sélecteur de nettoyage était écrit sans le point :
// ✘ avant : en CSS, un nom nu est un sélecteur de BALISE (<xlsx-sel-top>)
panel.querySelectorAll("xlsx-sel-top,xlsx-sel-bottom,...")
// ✔ après
panel.querySelectorAll(".xlsx-sel-top,.xlsx-sel-bottom,...")
La requête ne renvoyait aucun élément, donc classList.remove(...) n'était
jamais appelé et aucun contour n'était effacé. La même famille de faute
touchait classList.remove(".xlsx-hdr-selected") (un point passé comme jeton
de classe ne correspond à rien) : les en-têtes reflétés n'étaient jamais effacés
non plus.
Le test de non-régression compte les contours dans toute la grille après une randonnée au clavier et exige qu'une seule cellule soit encadrée.
6.5 Flèches pendant la saisie
Dans Google Sheets comme dans Excel, une flèche pendant la saisie valide la
cellule et déplace la sélection ; le curseur n'est pas parcouru dans le texte
(Alt+flèche sert à insérer un saut de ligne). Notre implémentation laissait
le curseur se déplacer et la cellule restait figée sous l'encadré — ce qui
amplifiait la confusion. Corrigé.
7. #183 — Panneau de couleurs façon Google Sheets
7.1 Ce qui remplace quoi
Le <input type="color"> natif (ruban A / seau, et les deux lignes du menu
Mise en forme) est remplacé par un bouton ouvrant un panneau construit par
frontend/js/xlsx/color-picker.js :
| Section | Contenu |
|---|---|
| En-tête | icône + Réinitialiser (gomme pour le fond, pinceau pour le texte) |
| Palette | 8 × 10 = 80 pastilles — ligne 1 : 8 gris du noir au blanc ; lignes 2-10 : 8 teintes (rouge, orange, jaune, vert clair, cyan, bleu, violet, magenta) × 9 nuances, du clair au foncé |
| STANDARD | 8 couleurs prédéfinies + crayon |
| PERSONNALISÉ | pastille de la couleur personnalisée, bouton « + », crayon |
| Pied | Mise en forme conditionnelle, et Couleurs en alternance (n'apparaît qu'une fois une couleur choisie, comme sur la maquette) |
Un clic sur une pastille referme le panneau et applique la couleur à la
cellule ou à la plage sélectionnée. La coche de sélection est blanche ou noire
selon la luminance de la pastille (luminanceOf).
7.2 Le piège d'encodage
La palette est calculée en HSL (lisible) mais doit sortir en #rrggbb :
normHex() côté viewer n'accepte que l'hexadécimal à 6 chiffres, et une
valeur hsl(0, 0%, 1429%) était donc rejetée sans erreur — la pastille se
peignait à l'écran mais la couleur n'était jamais écrite sur la cellule.
hslToHex() est doncexporté et testé par le contrôle de fumée.
7.3 Choix assumés
- « + » et les crayons ouvrent le sélecteur natif : le panneau propose 88 couleurs curées, le natif reste le seul moyen d'atteindre une teinte arbitraire.
- Les deux entrées de pied de page sont annoncées, pas fonctionnelles :
« Mise en forme conditionnelle » et « Couleurs en alternance » ne sont pas
livrées (cf. §3.2). Elles appellent
onUnavailableet affichent un message explicite plutôt que d'être des boutons inertes. - Chrome thémé : uniquement des variables CSS, donc le panneau suit le thème actif ; seules les pastilles portent une couleur en ligne, ce sont des données.
8. #184 — Peinture de format, multi-lignes/colonnes, grille A → Z × 1000
8.1 Peindre le format
Symptôme : l'outil refusait toute cellule source sans mise en forme explicite (« Veuillez sélectionner une cellule formatée »). Peindre une cellule simple sur une cellule colorée et en grand corps ne produisait donc rien — c'est le cas le plus courant en pratique.
Deux défauts distincts
- Copie partielle : seuls gras/italique/souligné/barré, couleurs, taille, alignement et format de nombre étaient capturés. valign, retour à la ligne, rotation et bordures ne l'étaient pas — une cellule bordée perdait ses bordures en « peignant » la même mise en forme ailleurs.
- Source « neutre » rejetée : sans propriété capturée, l'arme était annulée. Dans Sheets/Excel, une cellule sans mise en forme est une source valide : elle réinitialise la cible.
Correctif — captureFormat() renvoie toujours un jeu complet de
propriétés (y compris border: "none", font_color: "", fill_color: "",
number_format: "General"), donc la cible finit toujours identique à la source,
et une source neutre vide la cible.
Fuite d'écouteur : Échap ou un second clic sur le bouton désarmaient
l'arme mais laissaient le gestionnaire « appliquer au prochain clic » en place —
le clic suivant dans la grille repeignait une vieille mise en forme. Le
gestionnaire est maintenant retiré par stopPaint().
8.2 Sélection de plusieurs lignes / colonnes
Cliquer un numéro de ligne sélectionnait déjà la ligne entière ; il manquait l'extension à plusieurs lignes. Le mécanisme :
mousedownsur un en-tête sélectionne la ligne / colonne entière ;- glisser au-dessus d'autres en-tête étend (
A1:Z5),Maj+clic aussi (A1:Z5) — l'ancre est mémorisée après le glisser ; - la sélection réutilise le modèle
{anchor, focus}d'une plage, donc copier, effacer, insérer/supprimer, le menu contextuel et la suppression de contenu fonctionnent sans code supplémentaire ; - le
clicksur en-tête a été retiré : il écrasait la multi-sélection juste après le glisser ; - une pression près du bord de l'en-tête reste le geste de
redimensionnement (partage explicite avec
wireResize).
8.3 Grille A → Z, et 1000 lignes à la demande
La grille de la feuille affichée est complétée jusqu'à 26 colonnes (A → Z)
d'emblée ; les lignes s'ajoutent par blocs de 100 quand le lecteur approche
du bas, jusqu'à 1000. Les cellules vides sont de vraies cellules éditables
(contentEditable, tabIndex, écouteurs, data-cell). L'extension a lieu à
l'activation d'un onglet, pas au montage : la faire pour toutes les feuilles
multiplierait les cellules par le nombre d'onglets.
Une feuille tronquée garde son chargement serveur (« Charger la suite », fenêtre par fenêtre) : ses lignes manquantes sont des données, pas du remplissage. Étendre la grille par-dessus aurait dupliqué les lignes et supprimait le pied de page — régression de #153-A9bis, corrigée en gardant les deux mécanismes séparés (les colonnes et les blocs vides pour une feuille complète, la fenêtre serveur pour une feuille tronquée).
Pourquoi pas les 1000 lignes d'un coup. Matérialiser A → Z × 1000 revient à
~26 000 cellules câblées pour la seule feuille visible. Mesuré : ~0,3 Go
retenus par rendu, non récupérables même après innerHTML = "" et GC forcé — la
suite JSDOM xlsx-viewer (163 cas) épuisait le tas vers le 45ᵉ montage. La
croissance par blocs offre la même étendue sans payer le coût d'un coup.
Trois goulots levés — toujours utiles sur la grille atteinte :
| Goulot | Avant | Après |
|---|---|---|
findTd |
un querySelector par appel → 26 000 requêtes pour une colonne |
index ref → td avec auto-réparation |
refreshPanelFormulas |
cloneNode sur chaque cellule, à chaque frappe |
court-circuit sur cellule vide sans formule |
applySheetMeta |
toutes les cellules touchées | court-circuit sur celles que le classeur ne style pas |
Sorties. L'export (CSV / Markdown / HTML / impression) écarte les lignes et colonnes vides de queue : sans cela un classeur de deux colonnes exportait les 26 colonnes du quadrillage.
Reste à faire. La virtualisation par fenêtre (ne garder que les lignes visibles et recycler au défilement) remplacerait la croissance par blocs si la grille de 1000 lignes redevenait lourde dans le navigateur.