# #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](../ROADMAP.md) · [Changelog](../../CHANGELOG.md) · > [#154](./xlsx-ui-redesign.md) · [#155](./xlsx-context-menu.md) · > [#179](./xlsx-menus-179.md) · [#180](./xlsx-sheetbar-180.md) ## 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-color` par défaut) ; - `Entrée` validait puis faisait `blur()` — 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, plus `Maj`+clic — le mécanisme existait déjà, il est désormais complet et lisible. - Les cellules portent `.xlsx-selected` **et** 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éros `1`, `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 `.xlsm` sont lus et réécrits avec `keep_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)` dans `mousedown` : la plage est effacée de façon déterministe, sans dépendre du focus ; - le `Maj`+clic déplace réellement le focus, sous `suppressSelectionReset` pour que le handler `focus` ne 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 ; - `endDrag` restaure 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** : ```js // ✘ avant : en CSS, un nom nu est un sélecteur de BALISE () 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 `` 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 `onUnavailable` et 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** 1. **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. 2. **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 : - `mousedown` sur 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 `click` sur 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.