Feature: Is there a plugin that implements pagination similar to Google Docs?
Description
Is there a plugin that implements pagination similar to Google Docs?
I want to implement automatic pagination similar to Word and export the document as a Word file.
Impact
Does anyone have an implementation idea or an existing demo? I can try to implement it myself?
There isn't one, but in the playground, there is 'page break' widget that makes it print per page. If you customize that plugin CSS to look like two split pages and somehow anchor it to a certain height to refer to A4 size (that's the hard part), might get you closer to what you are after
Alternatively easier solution would be an editor per page and just concat those editors' contents
Thank you very much for your reply. I am familiar with 'page break.' However, I am looking for more advanced features, such as the ability to set page size, headers, and footers. When a document spans multiple pages, I would like the overflow content to automatically move to the next page when the user inputs content at the beginning.
Yeah, nothing out of the box for this.
I did some investing on the way to do it. First conclusion, is that we should not affect the text contents itself, which is the case for the page break solution.
Starting for this, I noticed the following approach to split:
- We can calculate the height of the elements and split them.
- We identify the element/paragraph that overflows the page. Once identified, we try to split by words, untill we have a sequence of strings fiting the page.
- We split the paragraph in 2.
- In between, we add a page break
Of course, this approch changes the HTML scructure and thus we need to do post processing of the lexical rendering.
is it possible to ovewrite the exportDOM of the rootnode?
I would need to get the exportedDOM for the whole document, traverse the HTML elements to calcule sizes, and split the HTML elements in correct points, and return it. It should not affect the editor state
No, RootNode can't be overridden. I'm not sure exportDOM would be the right solution here because that is only for export, not for rendering in the editor. You definitely won't be able to do it with createDOM/updateDOM because lexical requires a strict 1:1 lexical node to dom element mapping, you won't be able to split the dom without also splitting the nodes.
Ok, to automatically break and adjust paragraphs up or down the pages, we need to change the DOM, then the solution I proposed won't work.
A less powerfull solution is then to automatically add/remove page breaks in between paragraphs once the paragraph overflow the page size. This would leave empty spaces at the bottom of the page, because we will not split the paragraphs by words to occupy the full page space. Changing the paragraph nodes just for layout, for me, would be a misconception
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useEffect } from 'react'; import { $getSelection, $isRangeSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW, $isParagraphNode, } from 'lexical';
export function SelectionHighlightPlugin() { const [editor] = useLexicalComposerContext(); useEffect(() => { const removeHighlightCommand = editor.registerCommand( SELECTION_CHANGE_COMMAND, () => { editor.update(() => { const all = document.querySelectorAll('.lexical-highlight'); all.forEach((el) => el.classList.remove('lexical-highlight'));
const selection = $getSelection();
if (
!$isRangeSelection(selection) ||
selection.isCollapsed() ||
selection.getTextContent().length === 0
) {
return;
}
const selectedNodes = selection.getNodes();
selectedNodes.forEach((node) => {
if ($isParagraphNode(node) && node.getChildren().length === 0) {
const dom = editor.getElementByKey(node.getKey());
if (dom) {
dom.classList.add('lexical-highlight');
}
}
});
});
return false;
},
COMMAND_PRIORITY_LOW
);
const editable: any = document.querySelector('.content-editable-grid');
const blockHeight = 1123;
const colors = [''];
if (!editable) return;
function assignBackgroundsAndGaps() {
const children = Array.from(editable.children);
if (children.length === 0) return;
children.forEach((child: any) => {
child.classList.remove('block-end');
child.style.minHeight = '0px';
});
children.forEach((child: any) => {
if(child && child.tagName.toLowerCase() !== 'figure'){
child.classList.add('padding-elem');
}
})
const blocks = new Map();
let cumulativeHeight = 0;
children.forEach((child: any) => {
const childHeight = child.offsetHeight;
const blockIndex = Math.floor(cumulativeHeight / blockHeight);
const color = colors[blockIndex % colors.length];
child.style.backgroundColor = color;
if (!blocks.has(blockIndex)) {
blocks.set(blockIndex, []);
}
blocks.get(blockIndex).push(child);
cumulativeHeight += childHeight;
});
blocks.forEach((divs) => {
const totalHeight = divs.reduce((sum: any, div: any) => sum + div.offsetHeight, 0);
const deficit = blockHeight - totalHeight;
const lastDiv = divs[divs.length - 1];
if (lastDiv) {
lastDiv.classList.add('block-end');
if(lastDiv && lastDiv.tagName.toLowerCase() !== 'figure'){
lastDiv.classList.add('padding-elem')
}
if (deficit > 0) {
lastDiv.style.minHeight = (lastDiv.offsetHeight + deficit) + 'px';
}
}
});
}
// const handleKeyDown = (e: KeyboardEvent) => { // if (e.key === 'Enter') {
// const el :any= document.querySelector('.block-end') // if( el && el.offsetHeight < 1015){ // el.scrollIntoView({ // behavior: 'smooth', // block: 'center', // inline: 'center' // }); // window.scrollTo({ // top: 0, // or any fixed position // behavior: 'auto' // }); // } // // } // const selection = window.getSelection(); // if (!selection?.rangeCount) return;
// const range = selection.getRangeAt(0);
// const newDiv = document.createElement('div');
// newDiv.innerHTML = '
';
// let currentDiv = range.startContainer as HTMLElement; // while (currentDiv && currentDiv !== editable && currentDiv.nodeName !== 'DIV') { // currentDiv = currentDiv.parentNode as HTMLElement; // }
// if (currentDiv && currentDiv !== editable) { // if (currentDiv.nextSibling) { // editable.insertBefore(newDiv, currentDiv.nextSibling); // } else { // editable.appendChild(newDiv); // }
// const newRange = document.createRange(); // newRange.setStart(newDiv, 0); // newRange.collapse(true); // selection.removeAllRanges(); // selection.addRange(newRange); // } else { // editable.appendChild(newDiv); // }
// assignBackgroundsAndGaps(); // } // };
// editable.addEventListener('keydown', handleKeyDown);
editable.addEventListener('input', assignBackgroundsAndGaps);
const observer = new MutationObserver(assignBackgroundsAndGaps);
observer.observe(editable, { childList: true, subtree: true });
assignBackgroundsAndGaps();
document.addEventListener('paste', function () {
const children = Array.from(editable.children);
children.forEach((child: any) => {
if(child && child.tagName.toLowerCase() !== 'figure'){
child.classList.add('padding-elem');
}
})
const tableCells = document.querySelectorAll('td');
tableCells.forEach(cell => {
cell.classList.add('pasted-tables');
});
const contentMatches = document.querySelectorAll(".block-end");
const pageBreaks = document.querySelectorAll('figure[type="page-break"]');
const combined = Array.from(contentMatches).concat(Array.from(pageBreaks));
if (combined.length === 0) return;
const total = combined.length;
combined.forEach((el, index) => {
el.setAttribute('data-index', String(index + 1));
el.setAttribute('data-total', String(total));
});
});
return () => {
removeHighlightCommand();
// editable.removeEventListener('keydown', handleKeyDown);
editable.removeEventListener('input', assignBackgroundsAndGaps);
observer.disconnect();
};
}, [editor]);
return null; }
this will server the purpose
include in your Lexicalcomposer <SelectionHighlightPlugin/>