tiptap-pagination-breaks
Version:
A Tiptap extension for pagination
175 lines (172 loc) • 7.99 kB
JavaScript
import { Extension } from '@tiptap/core';
import { PluginKey, Plugin } from 'prosemirror-state';
import { DecorationSet, Decoration } from 'prosemirror-view';
const Pagination = Extension.create({
name: 'pagination',
addOptions() {
return {
pageHeight: 1056,
pageWidth: 816,
pageMargin: 96,
label: 'Page',
showPageNumber: true,
};
},
addCommands() {
return {
setPaginationOptions: (options) => ({ tr, dispatch }) => {
if (dispatch) {
tr.setMeta('paginationOptions', options);
}
return true;
},
};
},
onUpdate() {
// Apply the page width and margin to the editor's container
const editorContainer = this.editor.view.dom.closest('.ProseMirror');
if (editorContainer) {
const htmlElement = editorContainer; // Cast to HTMLElement
htmlElement.style.width = `${this.options.pageWidth}px`;
htmlElement.style.margin = `${this.options.pageMargin}px auto`;
}
},
addProseMirrorPlugins() {
const pluginKey = new PluginKey('pagination');
return [
new Plugin({
key: pluginKey,
state: {
init: () => ({ ...this.options }),
apply: (tr, value) => {
const newOptions = tr.getMeta('paginationOptions');
return newOptions ? { ...value, ...newOptions } : value;
},
},
props: {
decorations: (state) => {
const { doc } = state;
const decorations = [];
let currentPageHeight = 0;
let pageNumber = 1;
let lastNodeWasList = false;
let currentListHeight = 0;
let listStartPos = 0;
const options = pluginKey.getState(state);
const { pageHeight, pageMargin, showPageNumber, label } = options;
const effectivePageHeight = pageHeight - 2 * pageMargin;
const createPageBreak = (pos) => {
return Decoration.widget(pos, () => {
const pageBreak = document.createElement('div');
pageBreak.className = 'page-break';
pageBreak.style.cssText = `
height: 20px;
width: 100%;
border-top: 1px dashed #ccc;
margin: 10px 0;
position: relative;
`;
pageBreak.setAttribute('data-page-number', String(pageNumber));
if (showPageNumber) {
const pageIndicator = document.createElement('span');
pageIndicator.className = 'page-number';
pageIndicator.textContent = `${label || 'Page'} ${pageNumber}`;
pageIndicator.style.cssText = `
position: absolute;
right: 0;
top: -10px;
font-size: 12px;
color: #666;
background: white;
padding: 0 4px;
`;
pageBreak.appendChild(pageIndicator);
}
pageNumber++;
return pageBreak;
});
};
doc.descendants((node, pos) => {
if (!node.isBlock)
return;
const nodeDOM = this.editor.view.nodeDOM(pos);
if (!(nodeDOM instanceof HTMLElement))
return;
const isList = node.type.name === 'bulletList' ||
node.type.name === 'orderedList';
const isListItem = node.type.name === 'listItem';
// Calculate node height
const nodeHeight = isListItem
? calculateListItemHeight(nodeDOM)
: nodeDOM.offsetHeight;
if (nodeHeight === 0)
return;
// Handle list items
if (isList || isListItem) {
if (!lastNodeWasList) {
listStartPos = pos;
currentListHeight = 0;
}
currentListHeight += nodeHeight;
lastNodeWasList = true;
// Check if next node is not a list
const nextNode = doc.nodeAt(pos + node.nodeSize);
const isLastListItem = !nextNode ||
(nextNode.type.name !== 'listItem' &&
nextNode.type.name !== 'bulletList' &&
nextNode.type.name !== 'orderedList');
if (isLastListItem) {
// Process the entire list
if (currentPageHeight + currentListHeight >
effectivePageHeight) {
decorations.push(createPageBreak(listStartPos));
currentPageHeight = currentListHeight;
}
else {
currentPageHeight += currentListHeight;
}
lastNodeWasList = false;
}
return;
}
// Handle non-list blocks
lastNodeWasList = false;
if (currentPageHeight + nodeHeight > effectivePageHeight) {
decorations.push(createPageBreak(pos));
currentPageHeight = nodeHeight;
}
else {
currentPageHeight += nodeHeight;
}
});
return DecorationSet.create(doc, decorations);
},
},
}),
];
},
addGlobalAttributes() {
return [
{
types: ['textStyle'],
attributes: {
class: {
default: null,
parseHTML: (element) => element.getAttribute('class'),
renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {},
},
},
},
];
},
});
function calculateListItemHeight(element) {
const style = window.getComputedStyle(element);
const marginTop = parseFloat(style.marginTop) || 0;
const marginBottom = parseFloat(style.marginBottom) || 0;
const paddingTop = parseFloat(style.paddingTop) || 0;
const paddingBottom = parseFloat(style.paddingBottom) || 0;
return (element.offsetHeight + marginTop + marginBottom + paddingTop + paddingBottom);
}
export { Pagination };
//# sourceMappingURL=index.esm.js.map