{# Gemessener Seitenumbruch fuer Fluss-Dokumente (docs/formblatt_druck_plan.md §2d, Scheibe 4). Nimmt den fertig gerenderten Fluss aus #quelle und verteilt ihn auf Blaetter in #ausgabe. Gemessen wird an einem echten Blatt auf dem #messplatz, nicht gerechnet: Schriftmasse, Bildhoehen und Umbrueche im Text kennt nur der Browser. Der Vertrag steht im Markup, nicht hier — so kann die Maschinerie nichts ueber das Dokument wissen muessen: .umbruch-neu leerer Block: erzwingt ein neues Blatt data-teilbar an einer : ihre Zeilen duerfen auf mehrere Blaetter; der wiederholt sich data-fort-marke="..." Text, den die Fortsetzung an data-fort-ziel anhaengt data-fort-ziel="sel" wohin die Marke gehoert (Vorgabe: erstes Kind) data-fort-leeren="sel" was auf der Fortsetzung leer bleibt (z.B. ein Foto, das nicht auf jedem Blatt wiederholt gehoert) data-mit-naechstem darf nicht allein unten stehen: eine Ueberschrift wandert mit, wenn der naechste Block nicht mehr passt Was nicht passt, wird NICHT verkleinert. Passt ein unteilbarer Block auch auf einem leeren Blatt nicht, bleibt er stehen und wird sichtbar abgeschnitten — und body.dataset.umbruchUeberlauf sagt, wie oft das passiert ist. Ein stiller Verlust waere das Schlimmere: eine Bestandsliste, der drei Positionen fehlen, sieht vollstaendig aus. #} const otUmbruch = (function () { function ueberlaeuft(koerper) { // +1 gegen die Rundungsreste, die der Browser bei mm-Massen liefert. return koerper.scrollHeight > koerper.clientHeight + 1; } /* Erst messen, wenn Schriften UND Bilder stehen — sonst bricht die Seite gegen die falschen Hoehen um, und zwar unsichtbar, weil das Ergebnis plausibel aussieht. Ein Bild, das nie laedt, darf den Druck nicht aufhalten. */ async function warteAufDarstellung(wurzel) { const warten = []; if (document.fonts && document.fonts.ready) warten.push(document.fonts.ready); wurzel.querySelectorAll('img').forEach(bild => { if (bild.complete) return; warten.push(new Promise(fertig => { bild.addEventListener('load', fertig, { once: true }); bild.addEventListener('error', fertig, { once: true }); })); }); const deckel = new Promise(fertig => setTimeout(fertig, 8000)); await Promise.race([Promise.all(warten), deckel]); } function teilbareTabelle(block) { if (block.matches && block.matches('table[data-teilbar]')) return block; return block.querySelector ? block.querySelector('table[data-teilbar]') : null; } /* Die Fortsetzung eines Blocks: dieselbe Huelle, leerer Tabellenkoerper, Marke dran, und was sich nicht wiederholen soll, ist leer. */ function fortsetzung(block) { const klon = block.cloneNode(true); const tab = teilbareTabelle(klon); if (tab && tab.tBodies[0]) tab.tBodies[0].innerHTML = ''; (block.dataset.fortLeeren || '').split(',').filter(Boolean).forEach(sel => { klon.querySelectorAll(sel.trim()).forEach(e => e.remove()); }); const marke = block.dataset.fortMarke; if (marke) { const ziel = block.dataset.fortZiel ? klon.querySelector(block.dataset.fortZiel) : (klon.matches('table') ? null : klon.firstElementChild); if (ziel) ziel.insertAdjacentHTML('beforeend', '' + marke + ''); } return klon; } /* Wieviele Zeilen mindestens stehen bleiben muessen, damit sich das Teilen eines Abschnitts lohnt. Darunter wandert er ganz auf das naechste Blatt: zwei Zeilen unter einer Ueberschrift und der Rest woanders liest sich schlechter als eine halb leere Seite. */ const MIN_ZEILEN = 3; async function zeichne(opt) { const quelle = document.getElementById(opt.quelle || 'quelle'); const ausgabe = document.getElementById(opt.ausgabe || 'ausgabe'); const messplatz = document.getElementById(opt.messplatz || 'messplatz'); await warteAufDarstellung(quelle); const zeilenVorher = quelle.querySelectorAll('tbody tr').length; const blaetter = []; let koerper = null; let ueberlauf = 0; function neuesBlatt() { const blatt = document.createElement('div'); blatt.className = 'blatt'; const nr = blaetter.length + 1; blatt.innerHTML = (opt.kopf ? opt.kopf(nr) : '') + '
' + (opt.fuss ? opt.fuss(nr) : ''); messplatz.appendChild(blatt); blaetter.push(blatt); koerper = blatt.querySelector('.blatt-koerper'); return koerper; } /* Eine Ueberschrift, die als letztes auf dem Blatt steht und laut Markup nicht allein bleiben darf, wandert auf das naechste Blatt mit. */ function blattwechsel() { let vorlaeufer = null; const letzter = koerper && koerper.lastElementChild; if (letzter && letzter.dataset && letzter.dataset.mitNaechstem !== undefined) { koerper.removeChild(letzter); vorlaeufer = letzter; } neuesBlatt(); if (vorlaeufer) koerper.appendChild(vorlaeufer); } function zeilenRaus(tab) { const raus = Array.from(tab.tBodies[0].rows); tab.tBodies[0].innerHTML = ''; return raus; } /* Fuellt den Tabellenkoerper aus `rest`, solange das Blatt es traegt. Nimmt die untergebrachten Zeilen aus `rest` heraus und liefert ihre Anzahl. */ function fuelleZeilen(tab, rest) { const tbody = tab.tBodies[0]; let gesetzt = 0; while (rest.length) { const zeile = rest[0]; tbody.appendChild(zeile); if (ueberlaeuft(koerper)) { tbody.removeChild(zeile); break; } rest.shift(); gesetzt += 1; } return gesetzt; } /* Der Rest eines geteilten Blocks: je ein Fortsetzungsblock pro Blatt. */ function teileRest(block, rest) { while (rest.length) { blattwechsel(); const huelle = fortsetzung(block); koerper.appendChild(huelle); const tab = teilbareTabelle(huelle); if (fuelleZeilen(tab, rest) === 0) { // Nicht eine Zeile passt — sonst liefe das hier ewig. tab.tBodies[0].appendChild(rest.shift()); ueberlauf += 1; } } } function platziere(block) { koerper.appendChild(block); if (!ueberlaeuft(koerper)) return; koerper.removeChild(block); const tab = teilbareTabelle(block); const teilbar = !!(tab && tab.tBodies[0]); /* Steht schon etwas auf dem Blatt, wird zuerst HIER geteilt statt den ganzen Block zu schieben: sonst bleibt der Rest des Blattes leer, nur weil der naechste Abschnitt lang ist. */ if (teilbar && koerper.children.length) { const alle = zeilenRaus(tab); const rest = alle.slice(); koerper.appendChild(block); const gesetzt = fuelleZeilen(tab, rest); if (gesetzt >= MIN_ZEILEN) { teileRest(block, rest); return; } // Zu wenig: Zustand zurueckbauen und den Block ganz verschieben. koerper.removeChild(block); alle.forEach(z => tab.tBodies[0].appendChild(z)); } if (koerper.children.length) blattwechsel(); koerper.appendChild(block); if (!ueberlaeuft(koerper)) return; // Passt auch auf einem leeren Blatt nicht. if (teilbar) { koerper.removeChild(block); const rest = zeilenRaus(tab); koerper.appendChild(block); if (fuelleZeilen(tab, rest) === 0 && rest.length) { tab.tBodies[0].appendChild(rest.shift()); ueberlauf += 1; } teileRest(block, rest); return; } // Unteilbar und zu gross: sichtbar abschneiden statt still schlucken. ueberlauf += 1; } neuesBlatt(); for (const block of Array.from(quelle.children)) { if (block.classList.contains('umbruch-neu')) { if (koerper.children.length) neuesBlatt(); continue; } platziere(block); } // Ein leeres letztes Blatt entsteht, wenn der Fluss genau aufgeht. if (blaetter.length > 1 && !koerper.children.length) { messplatz.removeChild(blaetter.pop()); } blaetter.forEach((blatt, i) => { blatt.querySelectorAll('.seite-nr').forEach(e => { e.textContent = i + 1; }); blatt.querySelectorAll('.seiten-gesamt').forEach(e => { e.textContent = blaetter.length; }); if (opt.marke) { const marke = document.createElement('div'); marke.className = 'blattnummer nicht-drucken'; marke.textContent = opt.marke(i + 1, blaetter.length); ausgabe.appendChild(marke); } ausgabe.appendChild(blatt); }); document.querySelectorAll('.leiste .seiten-gesamt') .forEach(e => { e.textContent = blaetter.length; }); // Fuer den Test: keine Zeile darf beim Umbrechen verlorengehen. document.body.dataset.umbruchZeilen = String(zeilenVorher); document.body.dataset.umbruchUeberlauf = String(ueberlauf); document.body.dataset.umbruchFertig = String(blaetter.length); return blaetter.length; } return { zeichne }; })();