Skip to Content
Pagination

Pagination

Lays the document out as pages, the way Word does: page and section breaks, headers and footers, footnotes and endnotes, line numbers and page numbers. It is optional; the editor works without it.

Packages

PackagePurpose
@stesura/paginationPlugins, layout engine, stores (nanostores), helpers
@stesura/pagination/typesTypes only: PaginationState, PageLayout, PaginationCache, …
@stesura/pagination-reactpaginationUiExtension, node views, HeaderFooterPanel, hooks

Setup

Mount the plugins through your mount hook’s extraPlugins, and pass paginationUiExtension to the editor:

// StesuraEditor is the dynamic import from the getting-started example. import { useLocalEditor } from "@stesura/editor-react/hooks"; import { paginationPlugins } from "@stesura/pagination"; import { paginationUiExtension } from "@stesura/pagination-react"; const uiExtensions = [paginationUiExtension]; const Editor = () => { const { editorState, dispatch, pluginFactory, schema } = useLocalEditor({ extraPlugins: () => [...paginationPlugins()], }); return ( <StesuraEditor state={editorState} dispatchTransaction={dispatch} schema={schema} pluginFactory={pluginFactory} uiExtensions={uiExtensions} /> ); };

paginationPlugins() returns paginationPlugin(), headerFooterBindingPlugin() and headerFooterSyncPlugin(), in that order. They are exported individually too, but the order matters. To start with pagination off, mount the three yourself, in that order, with paginationPlugin({ showPagination: false }) first.

paginationUiExtension needs no further wiring. It contributes:

  • Node views: section (the page container), section header/footer, paragraph, heading, page break, image, display math and file.
  • Node-view wrappers for table rows and cells. They wrap whichever view is registered, so your own row/cell views keep their page-break behaviour.
  • Toolbar groups: pagination and line numbering on the View tab; header/footer and page numbers on the Page tab.
  • Status bar: the pagination toggle.
  • Overlays: HeaderFooterPanel (the header/footer editor) and the stylesheet that clips oversized nodes.

Turning pagination on and off

Always toggle through the command. The plugin keeps the store in sync and cleans up the page-break artifacts; writing the store directly gets reverted.

import { togglePagination } from "@stesura/pagination"; togglePagination(view.state, view.dispatch);

Read the current value with useShowPagination() inside the editor tree, useCurrentEditorShowPagination() from shared chrome such as a toolbar, or getShowPagination(idOrState) outside React.

Sections and page format

Every document has at least one section, which sets page size, orientation and margins. The commands live in @stesura/core/commands:

import { addSection, setSectionFormat, insertPageBreak } from "@stesura/core/commands"; addSection()(state, dispatch); // new section after the current one setSectionFormat({ orientation: "landscape", margin: { layout: "narrow" } })(state, dispatch); insertPageBreak(state, dispatch);

See Commands: Sections & Track Changes for the full list. Keep-with-next, widow/orphan control and page-break-before come from the paragraph’s pagination attr, falling back to its style: see Stylesheets.

Headers and footers

Open the editor with useSetHeaderFooterEditing from @stesura/editor-react. Variants (different first page, different even pages) are header_footer_content nodes, added and removed with core commands. See Header & Footer.

Line numbering

Driven by the section’s lineNumbers attr: countBy, distance (px), restart ("continuous" | "newPage" | "newSection") and start. Set it with these commands, not with setSectionFormat:

import { setLineNumbers, toggleLineNumbers, toggleSuppressLineNumbers } from "@stesura/core/commands"; toggleLineNumbers()(state, dispatch); // current section setLineNumbers({ countBy: 5, restart: "newPage" })(state, dispatch); toggleSuppressLineNumbers()(state, dispatch); // selected blocks: not counted

Oversized nodes

A block taller than the usable page gets a page of its own and is clipped, as in Word. This needs no code. A custom node view can handle it better, for example by scaling: read the clip height with useOversizedNode(editorId, nodeId), and set data-oversized-handled on its root to opt out of the default clip. See Node views.

Size changes outside transactions

Pagination re-measures the blocks a transaction touches. When a block’s height changes some other way, report it:

  • From a node view, for example an embed resolving or a chart re-rendering: useReportNodeResize(nodeId) returns a ref callback.
  • From anywhere else, for example AI ghost text rendered as a decoration: requestNodeRemeasure(idOrState, nodeIds) from @stesura/pagination.

Layout failures

The layout is checked after each pass. If content overflows the last computed page, or a measured section comes out empty, pagination wipes the section’s cache and recomputes once. Every stage is reported through console.warn and to your handlers:

import { addPaginationFailureHandler } from "@stesura/pagination"; const unsubscribe = addPaginationFailureHandler((report) => { // report: { editorId, sectionId, stage, kind, pageCount, overflow, detail?, … } if (report.stage === "unrecovered") sendToTelemetry(report); });
kindStages
overflow, engine-emptydetectedrecovered or unrecovered
measurement-missing, oversized-contentdetected only; informational, and throttled per section

Waiting for layout in tests

getPaginationActivity(idOrState) returns { engineRuns, enginesSkipped, recomputesSkipped, lastChangeAt }. hasPendingPaginationWork(idOrState) returns whether any nodes, notes or headers/footers are still dirty.

Layout counts as settled when:

  • the engine has run at least once;
  • no work is pending;
  • lastChangeAt has stayed the same for longer than the 300ms header/footer debounce. The quiet window covers work that is scheduled but not yet marked dirty.

Layout state

The plugin state (paginationPluginKey from @stesura/core/plugins) only holds showPagination and the oversized-node decorations. The rest lives in per-editor nanostores:

  • getSectionPaginationCache(idOrState, sectionId) returns a section’s PaginationCache (measurements, pagination items, breaks). Read it imperatively; it isn’t meant to be subscribed to.
  • Per-node results (paragraph breaks, row paddings, clip heights) are published to per-node atoms. Read them through the hooks below, so a node re-renders only when its own value changes.
  • Document-wide page counts and page numbers are in the core editor stores (pageCounts, pageNumbering). useDocPageNumbering reads them.

React hooks

From @stesura/pagination-react:

HookReturns
useShowPagination() / useCurrentEditorShowPagination()Whether pagination is on, for the enclosing editor or for the current root editor.
useOversizedNode(editorId, nodeId)Clip height in px, or undefined if the node fits.
useReportNodeResize(nodeId)A ref callback that re-measures the node when its DOM resizes. Re-exported from @stesura/editor-react.
useParagraphBreaks(editorId, nodeId)Page breaks inside a textblock, or undefined.
useRowHeightOverride(editorId, rowId){ hasOverride, height }: the engine’s height for a fixed-height row. A null height releases it so the row can split.
useRowPaddingTop(editorId, rowId)Top clearance in px for a row that starts a new page, else 0.
useVariantCache(sectionId?)The section’s non-default header/footer variants.
useDocPageNumbering(sectionId){ pageOffset, totalPages, firstPageNumber, format } for the section.
useVariantLabels()Translated variant names.
usePaginationLayoutEffect(node)Runs the engine for a section. Used by the section node view.

Next steps

Last updated on