diff --git a/ui/src/branchCollapse.js b/ui/src/branchCollapse.js index 02159af..6eeb83a 100644 --- a/ui/src/branchCollapse.js +++ b/ui/src/branchCollapse.js @@ -37,6 +37,26 @@ export function levelsOfHeader(columnHeader, splitLen) { return out } +// How many consecutive columns from x share this cell's first chy+1 levels — +// the span the group header would have if nothing were hidden. +// +// This has to be derived from the column paths on every draw. The obvious +// shortcut, stashing the original span on the cell the first time we see it, is +// wrong: regular-table recycles header cells between draws, so the stashed value +// reappears on an unrelated group and silently truncates it. +function fullSpan(columnPaths, x, chy) { + const levels = levelsOf(columnPaths[x]) + if (levels.length < chy + 1) return 1 // a rollup placeholder covers only itself + const prefix = levels.slice(0, chy + 1) + let n = 0 + for (let i = x; i < columnPaths.length; i++) { + const l = levelsOf(columnPaths[i]) + if (l.length < prefix.length || !prefix.every((v, k) => l[k] === v)) break + n++ + } + return n +} + function isDescendant(levels, prefix) { return levels.length > prefix.length && prefix.every((v, i) => levels[i] === v) } @@ -82,21 +102,26 @@ export function attachBranchCollapse(grid, { onChange } = {}) { if (!len) return const hidden = hiddenColumns(paths(), collapsed) + const columnPaths = paths() + for (const cell of rt.querySelectorAll('tbody td, tbody th, thead th')) { const meta = rt.getMeta(cell) if (!meta) continue - // A spanning group header covers [x, x + colSpan); it stays visible but - // must shrink to the columns still showing under it, or the header row - // and the body stop lining up. - const span = cell.colSpan || 1 - if (meta.type === 'column_header' && span > 1 && meta.x != null) { - let visible = 0 - for (let i = meta.x; i < meta.x + (cell._pfFullSpan || span); i++) { - if (!hidden.has(i)) visible++ + // A group header stays visible but must shrink to the columns still + // showing under it, or the header row and the body stop lining up. + // + // Only while something is actually collapsed. With nothing hidden this + // must not touch colSpan at all — regular-table computes it each draw by + // merging adjacent equal header values, and that answer is the right one. + if (hidden.size && meta.type === 'column_header' && meta.x != null) { + const chy = meta.column_header_y ?? 0 + if (chy < len) { + const full = fullSpan(columnPaths, meta.x, chy) + let visible = 0 + for (let i = meta.x; i < meta.x + full; i++) if (!hidden.has(i)) visible++ + cell.colSpan = Math.max(1, visible) } - cell._pfFullSpan = cell._pfFullSpan || span - cell.colSpan = Math.max(1, visible) } const hide = meta.x != null && hidden.has(meta.x) @@ -104,7 +129,7 @@ export function attachBranchCollapse(grid, { onChange } = {}) { // mark what can be clicked to collapse, and which way it would go if (meta.type === 'column_header' && meta.x != null && !hide) { - const branch = branchAt(paths(), meta.x, meta.column_header_y ?? 0, len) + const branch = branchAt(columnPaths, meta.x, meta.column_header_y ?? 0, len) if (branch) { cell.dataset.pfBranch = branch cell.style.cursor = 'pointer'