Feature Request: Pagination extension (A4 format content) #5719
Replies: 5 comments 19 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.
-
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