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)
@axessweb
Copy link

great !

@zackwong97
Copy link

thanks bro you are the best

@anteeek
Copy link

anteeek commented Aug 19, 2023

Very epic, thanks

@TheCardinalSystem
Copy link

Is there support for tfoot as well?

@Zarky2k2
Copy link

greattttttttttttttttttt

@cathyhax
Copy link

Works like a charm! Thank you!!!

@kevinguto
Copy link

You just don't know how much of a savior you are sir! Thank you!!!!

@adepranaya
Copy link

You save my life, Thank u very much

@DaisukiDaYo
Copy link

Thanks a lot!!! you're my life saver!!! Kudos 🙏🙏🙏

@RobertBouillon
Copy link

RobertBouillon commented Mar 4, 2024

Works great most of the time. Nested repeated headers causes some sections to be missing from both pages, though :(

EDIT: nevermind - it's a bug with pagedJS, not this script!

@DiesuaY
Copy link

DiesuaY commented Mar 11, 2024

Thanks alot

@MoamenAbdelsattar
Copy link

Thanks, I haven't tested yet but I don't understand where the function layout is called. It's not a part of the documented API of hooks.

@mersa2024
Copy link

How do I get this to work? I add it to my code, but it doesn't do anything??? Is there something I need to add to the CSS?

@pawelcwiek
Copy link

pawelcwiek commented May 23, 2024

Thanks!
one note: When the table is moved to a new page (because the table does not fit on the previous page), the header appears twice.
you can avoid this by adding a condition.

  repeatTHead(sourceTable, renderedTable) {
    let thead = sourceTable.querySelector('thead');
    if (thead && renderedTable.firstChild.tagName !== 'THEAD') {
      let clonedThead = thead.cloneNode(true);
      renderedTable.insertBefore(clonedThead, renderedTable.firstChild);
    }
  }

@0110wdj
Copy link

0110wdj commented Aug 9, 2024

So great ! Thanks a lot !

@jkbgbr
Copy link

jkbgbr commented Oct 20, 2024

Dear all,

Seeing everybody thanking for this I think it works, but I have absolutely no experience with js and so far no success in using this code.

What I tried is: simply putting it in the section right after the paged.polyfill.js section like this:

<script src="js/paged.polyfill.js"></script>
<script type="text/javascript">

class RepeatTableHeadersHandler extends Paged.Handler {
    constructor(chunker, polisher, caller) {
.
.
.
</script>

And of course I added a table in the html with a header like this but longer:

<table>
  <tr>
    <th>Firstname</th>
    <th>Lastname</th>
    <th>Age</th>
  </tr>
  <tr>
    <td>Jill</td>
    <td>Smith</td>
    <td>50</td>
  </tr>
</table>

Could anybody point me in the right direction please?

@TheCardinalSystem
Copy link

TheCardinalSystem commented Oct 21, 2024

Dear all,

Seeing everybody thanking for this I think it works, but I have absolutely no experience with js and so far no success in using this code.

What I tried is: simply putting it in the section right after the paged.polyfill.js section like this:

<script src="js/paged.polyfill.js"></script>
<script type="text/javascript">

class RepeatTableHeadersHandler extends Paged.Handler {
    constructor(chunker, polisher, caller) {
.
.
.
</script>

And of course I added a table in the html with a header like this but longer:

<table>
  <tr>
    <th>Firstname</th>
    <th>Lastname</th>
    <th>Age</th>
  </tr>
  <tr>
    <td>Jill</td>
    <td>Smith</td>
    <td>50</td>
  </tr>
</table>

Could anybody point me in the right direction please?

Add me on Discord and I can walk you through it. My username is cardinalsystem.

@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