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.
73 lines
2.5 KiB
JavaScript
73 lines
2.5 KiB
JavaScript
/* ObsiGate — XLSX A1 reference helpers (#154-A5).
|
||
*
|
||
* Pure functions (no DOM, no i18n): safe to unit-test in Node and shared by the
|
||
* viewer. Extracted from frontend/js/viewer.js without behaviour change.
|
||
*/
|
||
|
||
/** Parse an A1 reference ("B12") into its 1-based (row, col) parts. */
|
||
export function parseRef(ref) {
|
||
const m = /^([A-Z]+)(\d+)$/.exec(ref || "");
|
||
if (!m) return null;
|
||
let col = 0;
|
||
for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64);
|
||
return { row: Number(m[2]), col };
|
||
}
|
||
|
||
/** Column number (1-based) → letters ("A", "Z", "AA"). */
|
||
export function columnName(col) {
|
||
let name = "";
|
||
while (col > 0) {
|
||
const rem = (col - 1) % 26;
|
||
name = String.fromCharCode(65 + rem) + name;
|
||
col = Math.floor((col - 1) / 26);
|
||
}
|
||
return name;
|
||
}
|
||
|
||
// #184 — cell lookup cache. `findTd` used to run a `querySelector` per call;
|
||
// painting the grid out to A → Z × 1000 rows means a single "select column"
|
||
// asks for 26 000 cells, which was quadratic on the DOM. The index is built
|
||
// once per panel and rebuilt on demand whenever an entry no longer matches its
|
||
// own `data-cell` (the signature of a renumbered grid after a structure op).
|
||
const _cellIndex = new WeakMap();
|
||
|
||
function buildCellIndex(panel) {
|
||
const map = new Map();
|
||
panel.querySelectorAll("td[data-cell]").forEach((td) => {
|
||
map.set(td.dataset.cell, td);
|
||
});
|
||
_cellIndex.set(panel, map);
|
||
return map;
|
||
}
|
||
|
||
/** Drop a panel's lookup cache (bulk DOM changes). */
|
||
export function invalidateCellIndex(panel) {
|
||
if (panel) _cellIndex.delete(panel);
|
||
}
|
||
|
||
/** Find a rendered cell inside a sheet panel by (row, col). */
|
||
export function findTd(panel, row, col) {
|
||
const ref = `${columnName(col)}${row}`;
|
||
const cached = _cellIndex.get(panel);
|
||
const hit = cached && cached.get(ref);
|
||
if (hit && hit.dataset.cell === ref) return hit;
|
||
// Miss, or a stale entry (the row was renumbered): rebuild once and retry.
|
||
const fresh = buildCellIndex(panel).get(ref);
|
||
return fresh && fresh.dataset.cell === ref ? fresh : null;
|
||
}
|
||
|
||
/** Sheet prefix of a range ref ("'Mon onglet'!$A$1" → "Mon onglet"), or null. */
|
||
export function sheetOfRef(ref) {
|
||
const s = String(ref || "");
|
||
if (!s.includes("!")) return null;
|
||
return s.split("!")[0].replace(/^'|'$/g, "");
|
||
}
|
||
|
||
/** First cell of a range ref ("Data!$A$1:$B$5" → "A1"), or null. */
|
||
export function firstCellOfRange(ref) {
|
||
const s = String(ref || "");
|
||
const body = s.includes("!") ? s.split("!").pop() : s;
|
||
const m = /([A-Za-z]+)\$?(\d+)/.exec(body);
|
||
return m ? `${m[1].toUpperCase()}${m[2]}` : null;
|
||
}
|