Last active
April 20, 2026 12:58
-
-
Save theinvensi/e1aacc43bb5a3d852e2e85b08cf85c8a to your computer and use it in GitHub Desktop.
pagedjs-repeat-table-header
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class RepeatTableHeadersHandler extends Paged.Handler { | |
| constructor(chunker, polisher, caller) { | |
| super(chunker, polisher, caller) | |
| this.splitTablesRefs = [] | |
| } | |
| afterPageLayout(pageElement, page, breakToken, chunker) { | |
| this.chunker = chunker | |
| this.splitTablesRefs = [] | |
| if (breakToken) { | |
| const node = breakToken.node | |
| const tables = this.findAllAncestors(node, "table") | |
| if (node.tagName === "TABLE") tables.push(node) | |
| if (tables.length > 0) { | |
| this.splitTablesRefs = tables.map(t => t.dataset.ref) | |
| let thead = node.tagName === "THEAD" ? node : this.findFirstAncestor(node, "thead") | |
| if (thead) { | |
| let lastTheadNode = thead.hasChildNodes() ? thead.lastChild : thead | |
| breakToken.node = this.nodeAfter(lastTheadNode, chunker.source) | |
| } | |
| this.hideEmptyTables(pageElement, node) | |
| } | |
| } | |
| } | |
| hideEmptyTables(pageElement, breakTokenNode) { | |
| this.splitTablesRefs.forEach(ref => { | |
| let table = pageElement.querySelector("[data-ref='" + ref + "']") | |
| if (table) { | |
| let sourceBody = table.querySelector("tbody > tr") | |
| if (!sourceBody || this.refEquals(sourceBody.firstElementChild, breakTokenNode)) { | |
| table.style.visibility = "hidden" | |
| table.style.position = "absolute" | |
| let lineSpacer = table.nextSibling | |
| if (lineSpacer) { | |
| lineSpacer.style.visibility = "hidden" | |
| lineSpacer.style.position = "absolute" | |
| } | |
| } | |
| } | |
| }) | |
| } | |
| refEquals(a, b) { | |
| return a && a.dataset && b && b.dataset && a.dataset.ref === b.dataset.ref | |
| } | |
| findFirstAncestor(element, selector) { | |
| while (element.parentNode && element.parentNode.nodeType === 1) { | |
| if (element.parentNode.matches(selector)) return element.parentNode | |
| element = element.parentNode | |
| } | |
| return null | |
| } | |
| findAllAncestors(element, selector) { | |
| const ancestors = [] | |
| while (element.parentNode && element.parentNode.nodeType === 1) { | |
| if (element.parentNode.matches(selector)) ancestors.unshift(element.parentNode) | |
| element = element.parentNode | |
| } | |
| return ancestors | |
| } | |
| layout(rendered, layout) { | |
| this.splitTablesRefs.forEach(ref => { | |
| const renderedTable = rendered.querySelector("[data-ref='" + ref + "']") | |
| if (renderedTable) { | |
| if (!renderedTable.getAttribute("repeated-headers")) { | |
| const sourceTable = this.chunker.source.querySelector("[data-ref='" + ref + "']") | |
| this.repeatColgroup(sourceTable, renderedTable) | |
| this.repeatTHead(sourceTable, renderedTable) | |
| renderedTable.setAttribute("repeated-headers", true) | |
| } | |
| } | |
| }) | |
| } | |
| repeatColgroup(sourceTable, renderedTable) { | |
| let colgroup = sourceTable.querySelectorAll("colgroup") | |
| let firstChild = renderedTable.firstChild | |
| colgroup.forEach((colgroup) => { | |
| let clonedColgroup = colgroup.cloneNode(true) | |
| renderedTable.insertBefore(clonedColgroup, firstChild) | |
| }) | |
| } | |
| repeatTHead(sourceTable, renderedTable) { | |
| let thead = sourceTable.querySelector("thead") | |
| if (thead) { | |
| let clonedThead = thead.cloneNode(true) | |
| renderedTable.insertBefore(clonedThead, renderedTable.firstChild) | |
| } | |
| } | |
| nodeAfter(node, limiter) { | |
| if (limiter && node === limiter) return | |
| let significantNode = this.nextSignificantNode(node) | |
| if (significantNode) return significantNode | |
| if (node.parentNode) { | |
| while ((node = node.parentNode)) { | |
| if (limiter && node === limiter) return | |
| significantNode = this.nextSignificantNode(node) | |
| if (significantNode) return significantNode | |
| } | |
| } | |
| } | |
| nextSignificantNode(sib) { | |
| while ((sib = sib.nextSibling)) { if (!this.isIgnorable(sib)) return sib } | |
| return null | |
| } | |
| isIgnorable(node) { | |
| return ( | |
| (node.nodeType === 8) | |
| || ((node.nodeType === 3) && this.isAllWhitespace(node)) | |
| ) | |
| } | |
| isAllWhitespace(node) { | |
| return !(/[^\t\n\r ]/.test(node.textContent)) | |
| } | |
| } | |
| Paged.registerHandlers(RepeatTableHeadersHandler) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Helps a lot. Weirdly, sometimes, I get an empty "tr", at the end of a page. E.g
<tr class="row" data-ref="fcb6e999-bcd9-4b67-b222-b63a66ade7bc" data-split-to="fcb6e999-bcd9-4b67-b222-b63a66ade7bc" data-split-original="true"><td class="col-1" data-ref="5593dbbe-154d-46d4-a198-451722029860" data-split-to="5593dbbe-154d-46d4-a198-451722029860" data-split-original="true" data-last-split-element="true" data-align-last-split-element="auto"></td><td class="col-2" data-ref="72970368-d3b2-4b11-8450-b1a693988bc9"></td></tr>No data lost, as the row is actually split on the next page, it's just weird sometimes this extra empty row is being created. I ended up just hiding with CSS these specific cases, viatr[data-split-to] { display: none; }