Replies: 6 comments
-
There isn’t a built-in pagination or A4 page format plugin in Plate, but the plugin system is flexible enough to support this. You can introduce a custom top-level "page" node (similar to how tables are implemented) and use normalization to ensure all content is wrapped inside these page nodes. When a page’s rendered height exceeds your A4 limit, you’d programmatically move overflowing content into a new page node below. Plate’s architecture supports custom node structures and normalization, so you can manage splitting, merging, and rendering pages using the same patterns as the table plugin. You’ll need to handle edge cases like orphan paragraphs and editing across page boundaries, but the hooks and rendering components ( If you want to discuss implementation details or edge cases, let me know! To reply, just mention @dosu. How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other |
Beta Was this translation helpful? Give feedback.
-
An extension here would be fantastic |
Beta Was this translation helpful? Give feedback.
-
@skynth I've got a really basic extension done that works for me. It's not really well-designed, but at least something to start off. here is the plugin: import type { PluginConfig } from "platejs"
import { createTSlatePlugin } from "platejs"
import { DOCUMENT_DEFAULTS, INCHES_TO_CM, PPI } from "./constants"
import { normalizeInitialValuePagination } from "./normalizeInitialValuePagination"
import { withPagination } from "./withPagination"
export type PaginationConfig = PluginConfig<
"pagination",
{
documentSettings: {
type: "A4"
sizes: {
width: number
height: number
}
margins: {
top: number
bottom: number
left: number
right: number
}
}
},
{
create: {}
},
{
insert: {}
},
{}
>
export const BasePaginationPlugin = createTSlatePlugin<PaginationConfig>({
key: "pagination",
node: {
isElement: true,
isContainer: true,
isSelectable: false,
isInline: true,
type: "page",
},
options: {
documentSettings: {
type: "A4",
sizes: DOCUMENT_DEFAULTS.A4.sizes,
margins: DOCUMENT_DEFAULTS.A4.margins,
},
},
normalizeInitialValue: normalizeInitialValuePagination,
})
.extendSelectors(({ getOption }) => ({
sizes: () => getOption("documentSettings").sizes,
sizesCm: () => {
const base = getOption("documentSettings").sizes
return {
width: INCHES_TO_CM(base.width / PPI),
height: INCHES_TO_CM(base.height / PPI),
}
},
margins: () => getOption("documentSettings").margins,
marginsCm: () => {
const base = getOption("documentSettings").margins
return {
top: INCHES_TO_CM(base.top / PPI),
bottom: INCHES_TO_CM(base.bottom / PPI),
left: INCHES_TO_CM(base.left / PPI),
right: INCHES_TO_CM(base.right / PPI),
}
},
}))
.overrideEditor(withPagination) import type { PaginationConfig } from "./BasePaginationPlugin"
import { ElementApi } from "platejs"
import { OverrideEditor } from "platejs/react"
export const withPagination: OverrideEditor<PaginationConfig> = ({
editor,
tf: { normalizeNode },
type,
}) => ({
transforms: {
normalizeNode([n, path]) {
if (ElementApi.isElement(n)) {
if (n.type === type) {
if (path.length > 1) {
editor.tf.unwrapNodes({
at: path,
})
}
}
}
normalizeNode([n, path])
},
},
})
I wasnt able to make elements move from page to page just by using normalization. For that, you need to know the size of each element within the page node. I had two options on how could I implement this:
with first option I had no success as paragraph nodes can wrap, and at which point they will wrap was kind of hard to estimate by just having the text Yet I needed the auto-moving between pages so I've decided to do it in a hacky and really bad way: import { useEffect, useRef } from "react"
import { PlateElementProps, useEditorRef, usePluginOption } from "platejs/react"
import { useMounted } from "@/shared/hooks/use-mounted"
import { PaginationPlugin } from "../editor/plugins/pagination/react"
export function PageElement(props: PlateElementProps) {
const sizes = usePluginOption(PaginationPlugin, "sizes")
const margins = usePluginOption(PaginationPlugin, "margins")
const containerRef = useOverflowDetection(props)
return (
<div
ref={containerRef}
className="relative outline outline-1 outline-neutral-400"
style={{
width: `${sizes.width}px`,
height: `${sizes.height}px`,
paddingTop: margins.top ? `${margins.top}px` : undefined,
paddingBottom: margins.bottom ? `${margins.bottom}px` : undefined,
paddingLeft: margins.left ? `${margins.left}px` : undefined,
paddingRight: margins.right ? `${margins.right}px` : undefined,
}}
>
{props.children}
</div>
)
}
function useOverflowDetection(props: PlateElementProps) {
const mounted = useMounted()
const editor = useEditorRef()
const currentPath = props.path
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const container = containerRef.current
if (!mounted || !container || !currentPath || !editor.api.node({ at: [] }))
return
const nextPagePath = [currentPath[0] + 1]
if (container.scrollHeight > container.clientHeight) {
editor.tf.withoutNormalizing(() => {
const nextPage = editor.api.node(nextPagePath)
if (!nextPage) {
editor.tf.wrapNodes(
{
type: "page",
children: [],
},
{
at: currentPath.concat([container.children.length - 1]),
},
)
editor.tf.moveNodes({
at: currentPath.concat([container.children.length - 1]),
to: nextPagePath,
})
} else {
const node1 = editor.api.node(
currentPath.concat([container.children.length - 1]),
)
const node2 = editor.api.node(nextPagePath.concat([0]))
if (!node1 || !node2 || !node1[0] || !node2[0]) return
editor.tf.moveNodes({
at: currentPath.concat([container.children.length - 1]),
to: nextPagePath.concat([0]),
mode: "highest",
})
}
})
}
}, [containerRef?.current?.scrollHeight])
return containerRef
} as you see the page node itself is responsible for this logic. Im really sure its possible to dig into two previous options I considered and find a better success with it. Also would be really cool to hear some minds behind platejs, how can I make a better extension and what to consider @zbeyens @felixfeng33 |
Beta Was this translation helpful? Give feedback.
-
It sounds different from #4007, as it still loads all the nodes at once, but it's still a good plugin. |
Beta Was this translation helpful? Give feedback.
-
@claude can you help? |
Beta Was this translation helpful? Give feedback.
-
I did a paging plugin in the past, unfortunately I won't have free time to open-source it. Here is a quick analysis from AI. Core ArchitectureThe plugin wraps all content inside page nodes, using normalization to ensure Normalization StrategyThe normalization system enforces several rules:
Soft Split/Merge ImplementationThis is the most complex feature - elements (especially tables) can span across
Edge Cases Handled
Performance Issue: DOM MeasuringThe major performance bottleneck is DOM measuring being too slow, especially for real-time page breaks:
This approach, while accurate, is too computationally expensive for smooth Canvas-Based Text MeasurementYour implementation uses a clever approach with the typometer module:
However, even with Canvas measuring, the performance bottleneck remains for
Architectural Solution: Virtual PagesA more scalable approach would involve rewriting slate-react to support virtual This would eliminate several performance bottlenecks:
The virtual page system would track element positions and render page |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I want to implement a plugin into the editor 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.
As far as I know, platejs has no extensions that allow the editor to enter to this "page" format. It's pageless
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.
Would love to hear ideas on how to potentially implement this extension
My first thought is to keep editor's top level children to be nodes that represent pages
if content goes out of page's height, create a new page below and move the content there
Beta Was this translation helpful? Give feedback.
All reactions