Skip to content

Instantly share code, notes, and snippets.

@theinvensi
Last active March 29, 2025 19:43
Show Gist options
  • Save theinvensi/e1aacc43bb5a3d852e2e85b08cf85c8a to your computer and use it in GitHub Desktop.
Save theinvensi/e1aacc43bb5a3d852e2e85b08cf85c8a to your computer and use it in GitHub Desktop.
pagedjs-repeat-table-header
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)
@tobias-stein
Copy link

tobias-stein commented Oct 25, 2024

Great work, thanks for sharing.

It's repeating the the first table header twice for me.
I think it does that when you have

    thead {
      display: table-header-group; /* Repeat header */
    }

    tbody {
      display: table-row-group;
    }

in your css

@abjardim
Copy link

abjardim commented Jan 9, 2025

Works great, thank you!

@kudaboh
Copy link

kudaboh commented Jan 23, 2025

Thank you

@Ami777
Copy link

Ami777 commented Mar 29, 2025

Thank you! For me in the current 0.5.0-beta2 version it wans't working when tables weren't in one specific layout (in code it says table should be parent of the element). So here is the version that looks for tables everywhere (any layout):

            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 = []

                    // Check all the tables on the current page
                    const tablesOnPage = pageElement.querySelectorAll("table");

                    // Check which tables have sources
                    for (let table of tablesOnPage) {
                        if (table.dataset.ref) {
                            const sourceTable = chunker.source.querySelector(`[data-ref='${table.dataset.ref}']`);
                            if (sourceTable) {
                                this.splitTablesRefs.push(table.dataset.ref);
                            }
                        }
                    }

                    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