feat: styles minimaux, fusions et volets figes dans la visionneuse xlsx #153
A15 — le rendu tableur lit les metadonnees du classeur (une charge en mode
normal par feuille) et les restitue : styles inline data-driven (couleur de
police en premier, fond, gras/italique, format numerique signale en mono),
alignements non-defaut, plages fusionnees (rowSpan/colSpan cote client) et
ancrage des volets figes (sticky rows/cols). Le chargement lazy replie les
metadonnees de chaque fenetre. Contrat API : styles/aligns = {ref: css},
merges = [ranges], freeze = ancre. 9 tests backend + contre-preuve (5 echecs
sur neutralisation), JSDOM 33/33.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
This commit is contained in:
@@ -1047,6 +1047,59 @@ export function renderXlsxViewer(area, data) {
|
||||
`<button class="xlsx-tab${i === 0 ? " active" : ""}" data-sheet="${i}">${escapeHtml(s.name)}</button>`
|
||||
).join("")}</div>`
|
||||
: "";
|
||||
// #153 A15 — inline styles (bold/italic/colors from the file itself), merged
|
||||
// ranges and the freeze anchor are applied AFTER the panels are in the DOM
|
||||
// (the HTML string alone cannot carry them: col/row spans need the table).
|
||||
const applySheetMeta = (panel) => {
|
||||
const meta = sheets[Number(panel.dataset.sheet)] || {};
|
||||
const styleMap = meta.styles || {};
|
||||
const alignMap = meta.aligns || {};
|
||||
panel.querySelectorAll(".xlsx-table td[data-cell]").forEach((td) => {
|
||||
const ref = td.dataset.cell;
|
||||
const style = styleMap[ref];
|
||||
if (style) td.setAttribute("style", style);
|
||||
const align = alignMap[ref];
|
||||
if (align) td.style.textAlign = align;
|
||||
});
|
||||
// Merged ranges: span the anchor cell over the range and hide the covered
|
||||
// cells (rowSpan/colSpan survive a re-render; hidden cells are skipped by
|
||||
// the save collector because their text was already blank in openpyxl).
|
||||
(meta.merges || []).forEach((range) => {
|
||||
const [from, to] = range.split(":");
|
||||
const anchor = panel.querySelector(`td[data-cell="${from}"]`);
|
||||
const last = panel.querySelector(`td[data-cell="${to}"]`);
|
||||
if (!anchor || !last || anchor === last) return;
|
||||
const a = parseRef(from);
|
||||
const b = parseRef(to);
|
||||
if (!a || !b) return;
|
||||
anchor.rowSpan = Math.max(1, b.row - a.row + 1);
|
||||
anchor.colSpan = Math.max(1, b.col - a.col + 1);
|
||||
// Hide every covered cell of the block.
|
||||
for (let r = a.row; r <= b.row; r++) {
|
||||
for (let c = a.col; c <= b.col; c++) {
|
||||
if (r === a.row && c === a.col) continue;
|
||||
const covered = panel.querySelector(`td[data-cell="${columnName(c)}${r}"]`);
|
||||
if (covered) covered.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
// Freeze panes: sticky the rows/cols left of and above the anchor.
|
||||
const freeze = meta.freeze;
|
||||
if (freeze && freeze !== "A1") {
|
||||
const f = parseRef(freeze);
|
||||
if (f) {
|
||||
panel.querySelectorAll(".xlsx-table tbody tr").forEach((tr) => {
|
||||
const ref = tr.querySelector("td")?.dataset.cell || "";
|
||||
const p = parseRef(ref);
|
||||
if (p && p.row < f.row) tr.classList.add("xlsx-frozen-row");
|
||||
});
|
||||
panel.querySelectorAll(".xlsx-table td[data-cell]").forEach((td) => {
|
||||
const p = parseRef(td.dataset.cell);
|
||||
if (p && p.col < f.col) td.classList.add("xlsx-frozen-col");
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const panels = sheets.map((s, i) =>
|
||||
`<div class="xlsx-panel" data-sheet="${i}"${i === 0 ? "" : ' style="display:none"'}>${truncationNote(s)}${s.html}</div>`
|
||||
).join("");
|
||||
@@ -1151,6 +1204,20 @@ export function renderXlsxViewer(area, data) {
|
||||
panel.querySelectorAll("tbody tr").forEach((tr) => tr.setAttribute("data-wired", "1"));
|
||||
const cachedEls = panel.querySelectorAll(".xlsx-cached[data-cached-value]");
|
||||
cachedEls.forEach((el) => { if (!el.title) el.title = t("xlsx.cached_value_title"); });
|
||||
// #153 A15 — the window carries its own style/merge metadata: fold it
|
||||
// into the sheet meta and re-apply, so lazy rows look like the initial
|
||||
// render (applySheetMeta is idempotent over already-processed cells).
|
||||
const sheetMeta = sheets[Number(panel.dataset.sheet)];
|
||||
if (sheetMeta) {
|
||||
Object.assign(sheetMeta.styles || (sheetMeta.styles = {}), win.styles || {});
|
||||
Object.assign(sheetMeta.aligns || (sheetMeta.aligns = {}), win.aligns || {});
|
||||
if (!sheetMeta.merges) sheetMeta.merges = [];
|
||||
(win.merges || []).forEach((m) => {
|
||||
if (!sheetMeta.merges.includes(m)) sheetMeta.merges.push(m);
|
||||
});
|
||||
if (win.freeze && !sheetMeta.freeze) sheetMeta.freeze = win.freeze;
|
||||
applySheetMeta(panel);
|
||||
}
|
||||
safeCreateIcons();
|
||||
};
|
||||
|
||||
@@ -1327,6 +1394,7 @@ export function renderXlsxViewer(area, data) {
|
||||
// Editable cells: Enter blurs, Escape reverts, arrows/Tab navigate.
|
||||
area.querySelectorAll(".xlsx-table td").forEach(setupCell);
|
||||
panelEls.forEach(wireLazyRows);
|
||||
panelEls.forEach(applySheetMeta);
|
||||
// The formula bar starts disabled: nothing is selected yet.
|
||||
setActiveCell(null);
|
||||
|
||||
|
||||
@@ -11092,6 +11092,21 @@ body.desktop-mode .editor-container {
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* #153 A15 — freeze panes: frozen rows sit UNDER the sticky thead, frozen
|
||||
columns stay left. z-index mirrors thead (3) without covering it. */
|
||||
.xlsx-table tr.xlsx-frozen-row td,
|
||||
.xlsx-table tr.xlsx-frozen-row th {
|
||||
position: sticky;
|
||||
top: 33px; /* thead height — keeps the frozen row below the header */
|
||||
z-index: 2;
|
||||
background: var(--surface);
|
||||
}
|
||||
.xlsx-table td.xlsx-frozen-col {
|
||||
position: sticky;
|
||||
left: 44px; /* the row-number column width */
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* #153 A14 — structure menu (sheets / rows / columns) */
|
||||
.xlsx-structure-menu {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user