Feature Request: Pagination extension (A4 format content) #5719
Replies: 11 comments 49 replies
-
I have already done some work for my Nextjs app, but it still a bit buggy: import { Extension } from '@tiptap/core';
import { Node, mergeAttributes } from '@tiptap/core';
import { Node as PMNode } from '@tiptap/pm/model';
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state';
export const PageNode = Node.create({
name: 'page',
group: 'block',
content: 'block+',
defining: true,
isolating: true,
parseHTML() {
return [
{
tag: 'div[data-page]',
},
];
},
renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(HTMLAttributes, { 'data-page': true, class: 'page' }), 0];
},
addNodeView() {
return () => {
const dom = document.createElement('div');
dom.setAttribute('data-page', 'true');
dom.classList.add('page');
dom.style.height = '297mm'; // A4 height in mm
dom.style.width = '210mm'; // A4 width in mm
dom.style.padding = '25.4mm';
dom.style.border = '1px solid #ccc';
dom.style.background = 'white';
dom.style.overflow = 'hidden';
dom.style.position = 'relative';
const contentDOM = document.createElement('div');
dom.appendChild(contentDOM);
return {
dom,
contentDOM,
};
};
},
});
export const PaginationExtension = Extension.create({
name: 'pagination',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('pagination'),
appendTransaction: (transactions, oldState, newState) => {
const paginationMeta = 'pagination';
const lastTransaction = transactions[transactions.length - 1];
const isPaginationTransaction = lastTransaction.getMeta(paginationMeta);
// Avoid infinite loops and unnecessary processing
if (isPaginationTransaction || !lastTransaction.docChanged) {
return null;
}
const { schema } = newState;
const pageType = schema.nodes.page;
if (!pageType) {
return null;
}
// Collect content nodes, flattening existing pages
const contentNodes: PMNode[] = [];
newState.doc.forEach((node) => {
if (node.type === pageType) {
node.forEach((child) => contentNodes.push(child));
} else {
contentNodes.push(node);
}
});
// Partition content nodes into pages
const pages = [];
let currentPageContent: PMNode[] = [];
let currentHeight = 0;
const pageHeight = (297 - 25.4 * 2) * 3.77953; // A4 height in mm minus padding, converted to px (1 mm = 3.77953 px)
const lineHeight = 24; // Line height in px
for (const node of contentNodes) {
const nodeHeight = estimateNodeHeight(node, lineHeight);
if (currentHeight + nodeHeight > pageHeight && currentPageContent.length > 0) {
// Start a new page
pages.push(pageType.create({}, currentPageContent));
currentPageContent = [node];
currentHeight = nodeHeight;
} else {
currentPageContent.push(node);
currentHeight += nodeHeight;
}
}
if (currentPageContent.length > 0) {
pages.push(pageType.create({}, currentPageContent));
}
const newDoc = schema.topNodeType.create(null, pages);
// Compare the content of the documents
if (newDoc.content.eq(newState.doc.content)) {
return null; // No changes, skip transaction
}
const tr = newState.tr.replaceWith(0, newState.doc.content.size, newDoc.content);
tr.setMeta(paginationMeta, true);
// Map the selection from oldState to the new document
const { selection } = oldState;
const mappedSelection = selection.map(tr.doc, tr.mapping);
if (mappedSelection) {
tr.setSelection(mappedSelection);
} else {
// Fallback to a safe selection at the end of the document
tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size));
}
return tr;
},
}),
];
},
});
function estimateNodeHeight(node: PMNode, lineHeight: number): number {
if (node.isTextblock) {
const lines = node.textContent.split('\n').length || 1;
return lines * lineHeight;
} else if (node.type.name === 'image') {
return 200;
} else {
return lineHeight;
}
} |
Beta Was this translation helpful? Give feedback.
-
@bdbch yes. Right now the content split into nodes is working super good, but only for paragraphs, lists, and other simple nodes. The big issue comes with more complex HTML elements such as tables. It's not an easy job to traverse the table element and make a split. Something similar with images. There are also some buggy behaviors when deleting content and passing to previous page nodes, which I still cant figure out how to solve |
Beta Was this translation helpful? Give feedback.
-
Great job, this is the first well-documented pagination solution that I've seen online. I echo your need for a comprehensive pagination plugin. As you said, there are so many sectors in which being able to see editable content in pages is essential. Have you tested cases in which the number of pages reaches 100, 200 or higher? That's generally where the existing solutions begin to slow down. |
Beta Was this translation helpful? Give feedback.
-
Hi guys. Any progress by your side? For me has been a hell, and right now trying out a Collabora Online server to just embedded it to the app |
Beta Was this translation helpful? Give feedback.
-
guys, i asked for the feature on #5793 (comment) :) hope the team consider it!!! |
Beta Was this translation helpful? Give feedback.
-
Hi guys, need same implementation for me also, using tiptap with automatically page-breaks (like in Google-Docs) any demo or updates ? |
Beta Was this translation helpful? Give feedback.
-
I took a swing at this https://pagination-demo-ashy.vercel.app/ |
Beta Was this translation helpful? Give feedback.
-
Hello everyone Screen.Recording.2025-05-22.at.1.04.20.PM.movThank you |
Beta Was this translation helpful? Give feedback.
-
Hi @RomikMakavana this is cool thanks for submitting this - looks like you've went for the
Just thought I'd leave my two cents here as I've spent well over 500 hours on ProseMirror + TipTap pagination at this point, and I can confidently say it is one of, if not the most, difficult programming tasks i've ever worked on. The ultimate solution for pagination needs to be based in the PM schema and take into account nodes themselves, ie. a page needs to be a node with attributes. There are a few community solutions that go down this route but they are extremely unperformant for large documents, and I have yet to see one that is feature complete (handling all the "standard" schema nodes) |
Beta Was this translation helpful? Give feedback.
-
Not that I can share unfortunately, our solution is probably closest to the
one posted here by Romik
His is actually much simpler (in a good way) think we over engineered a
little to support coloured pages and horizontal pages
I’m going to try with a node based approach one last time next month, if
that fails I am thinking about starting a new open source project for these
page based word processors based on canvas (like MS word or Google docs)
…On Thu, 19 Jun 2025 at 15:55, Clemente ***@***.***> wrote:
BTW @dan-cooke <https://github.com/dan-cooke> , do you have any demo of
your solution?
—
Reply to this email directly, view it on GitHub
<#5719 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AFOCQ53QC4K4ZVDQ7UJO4G33ELFOJAVCNFSM6AAAAABPW2X3TKVHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTGNJSGEZTCOI>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
Hello, |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Description
Extension which purpose is to emulate the content into an A4 page format, and automatically split the content for the user when the max height is exceeded.
Until now, Tiptap has no extensions that allow the editor to enter to this "page" format.
Use Case
Many apps with more conventional users (mainly in industries such as lawyers, legal guardians, any anyone which has to work with lots of paper) still need online editors that emulate the content into an A4 page format. Many need to print it out or export it to PDF to be sent to recipients that need to document in A4 format.
Type
New extension
Beta Was this translation helpful? Give feedback.
All reactions