diff --git a/src/lib/editor/extensions/tableOfContents.ts b/src/lib/editor/extensions/tableOfContents.ts index 1d7b047..879e9eb 100644 --- a/src/lib/editor/extensions/tableOfContents.ts +++ b/src/lib/editor/extensions/tableOfContents.ts @@ -14,8 +14,10 @@ export type TocEntry = { text: string; level: number; page: number }; // ("Inhalt", "Sommaire", …), or none at all where the file put its heading in a // separate paragraph (`''`); a freshly inserted one takes this. const TITLE = 'Table of Contents'; -// Enough leader dots to cross the widest gap a page can offer; the row clips the rest. +// Enough leader dots to cross the widest gap a page can offer; fillLeaders cuts each +// row's back to what its own gap holds, measuring one dot with this sample. const LEADER_DOTS = 200; +const LEADER_PROBE = '..........'; declare module '@tiptap/core' { interface Commands { @@ -235,6 +237,22 @@ class TocView { } this.dom.appendChild(row); }); + this.fillLeaders(); + } + + // As many leader dots as the gap holds. The row clips the rest on screen, but nothing + // else does: 200 of them reach the PDF, the clipboard and every measurement as text. + private fillLeaders(): void { + const leaders = Array.from(this.dom.querySelectorAll('.toc-leader')); + if (!leaders.length) return; + leaders[0].textContent = LEADER_PROBE; + const range = document.createRange(); + range.selectNodeContents(leaders[0]); + const dot = range.getBoundingClientRect().width / LEADER_PROBE.length; + if (!(dot > 0)) return; + for (const el of leaders) { + el.textContent = '.'.repeat(Math.max(0, Math.floor(el.getBoundingClientRect().width / dot))); + } } // Scroll the heading into view and drop the cursor into it. diff --git a/src/styles/editor.css b/src/styles/editor.css index 738b9a0..bf967f7 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -219,10 +219,11 @@ .paper .tiptap .toc-entry:hover .toc-text { text-decoration: underline; } -.paper .tiptap .toc-level-2 { padding-left: 0.6cm; } -.paper .tiptap .toc-level-3 { padding-left: 1.2cm; } -.paper .tiptap .toc-level-4 { padding-left: 1.8cm; } -.paper .tiptap .toc-level-5 { padding-left: 2.4cm; } +/* One level = 0.5cm, the indent LibreOffice's own Contents 1…5 styles carry. */ +.paper .tiptap .toc-level-2 { padding-left: 0.5cm; } +.paper .tiptap .toc-level-3 { padding-left: 1cm; } +.paper .tiptap .toc-level-4 { padding-left: 1.5cm; } +.paper .tiptap .toc-level-5 { padding-left: 2cm; } .paper .tiptap .toc-text { white-space: nowrap; overflow: hidden; diff --git a/tests/render-parity/run.mjs b/tests/render-parity/run.mjs index 107529b..5738f09 100644 --- a/tests/render-parity/run.mjs +++ b/tests/render-parity/run.mjs @@ -98,8 +98,11 @@ async function settle(page) { // The spacers' heights too, not just their count: they go on settling for a while // after the last one is placed, and the page a line lands on moves with them. const spacers = Array.from(document.querySelectorAll('[data-page-break-spacer]')); + // The index's page numbers settle after the spacers do — a stale one is a line of + // text that still changes. + const toc = Array.from(document.querySelectorAll('.toc-page')).map((t) => t.textContent).join(','); const key = el.style.minHeight + '|' + el.children.length + '|' + spacers.length + '|' - + spacers.reduce((sum, s) => sum + s.offsetHeight, 0); + + spacers.reduce((sum, s) => sum + s.offsetHeight, 0) + '|' + toc; const w = window; if (w.__parityKey !== key) { w.__parityKey = key; w.__paritySince = performance.now(); return false; } return performance.now() - (w.__paritySince ?? 0) > 3000;