Header & Footer
Headers and footers are editable regions at the top and bottom of each page in a paginated document. They are part of the section node model (section_header / section_footer, each holding one header_footer_content per variant) and are only meaningful with the pagination stack mounted.
Packages
| Package | Purpose |
|---|---|
@stesura/pagination | headerFooterBindingPlugin + headerFooterSyncPlugin, variants store, DOM/variant helpers |
@stesura/pagination-react | HeaderFooterPanel, section header/footer node views, paginationUiExtension |
@stesura/editor-react | useHeaderFooterEditing / useSetHeaderFooterEditing editing state |
@stesura/core | Commands (page numbers, distances, variant content, healing), headerFooterGuardPlugin, the headerFooterEditorRegistry of mounted header/footer editors |
Setup
Nothing beyond the standard pagination setup: paginationUiExtension
already registers SectionNodeview, SectionHeaderNodeView and
SectionFooterNodeView, and mounts HeaderFooterPanel in the editor’s
documentOverlaySlot:
import { useLocalEditor } from "@stesura/editor-react/hooks";
import { paginationPlugins } from "@stesura/pagination";
import { paginationUiExtension } from "@stesura/pagination-react";
const Editor = () => {
const { editorState, dispatch, pluginFactory, schema } = useLocalEditor({
extraPlugins: () => [...paginationPlugins()],
});
return (
<StesuraEditor
state={editorState}
dispatchTransaction={dispatch}
schema={schema}
pluginFactory={pluginFactory}
uiExtensions={[paginationUiExtension]}
/>
);
};HeaderFooterPanel hosts its own sub-editor for the part being edited; the
toolbar targets it automatically while it is open. Double-clicking a rendered
header or footer opens it. Only import HeaderFooterPanel from
@stesura/pagination-react directly if you build a fully custom UI extension.
Opening the editor programmatically
Editing state is a piece of React state, read and written through hooks from
@stesura/editor-react. The value is
{ sectionId, target, variant, focusOnOpen? } | null:
import { useHeaderFooterEditing, useSetHeaderFooterEditing } from "@stesura/editor-react";
const setEditing = useSetHeaderFooterEditing();
// Open the default footer of a section:
setEditing({ sectionId, target: "footer", variant: "default" });
// Open without stealing DOM focus (e.g. from search navigation):
setEditing({ sectionId, target: "header", variant: "first", focusOnOpen: false });
// Close the panel:
setEditing(null);
const editing = useHeaderFooterEditing(); // current state, or nullsectionId is the section node’s id attr; variant is
"default" | "first" | "even".
The core commands focusHeaderFooter, openHeader and openFooter do not
open the panel: they set a headerFooterSyncPluginKey meta
(openHeaderId / openFooterId) that nothing reads. The selection
focusHeaderFooter places in the header/footer is also moved back into the body
by headerFooterGuardPlugin (see below). Use setEditing instead.
The mounted header/footer editors are listed in headerFooterEditorRegistry
(from @stesura/core/stores), keyed per main editor by
{ sectionId, target, variant }:
import { headerFooterEditorRegistry } from "@stesura/core/stores";
// The open footer editor's EditorView, if mounted:
const entry = headerFooterEditorRegistry.get(mainEditorId, {
sectionId,
target: "footer",
variant: "default",
});
entry?.view;The generic sub-editor machinery behind it (derived schema, main→inner binding
plugin, step forwarding) lives on @stesura/core/sub-editor.
Variants
Whether a section has a different first-page or even-page header/footer is
derived from the document: a variant is on when its header_footer_content node
exists. Toggle it with the core commands below, as the panel’s variant tabs do.
headerFooterSyncPlugin mirrors the result into a @stesura/pagination store
(nanostores) that rendering and the layout engine read. Don’t write that store
with setHeaderFooterVariants: the next sync overwrites it.
import { getOrCreateSectionVariantsAtom, getVariantForPage } from "@stesura/pagination";
// The section's non-default variants, e.g. Set { "first" }:
const variants = getOrCreateSectionVariantsAtom(view.state, sectionId).get();
// Which variant a section-local, 0-based page index renders (falls back to "default"):
getVariantForPage(0, variants); // "first"“First page” is per section, but odd/even follows document page parity, as in
Word. For a section that doesn’t start the document, pass the number of pages
before it as the third argument (pageOffset); useDocPageNumbering(sectionId)
returns it.
Subscribe through the getOrCreate* accessors (getOrCreateSectionVariantsAtom,
getOrCreateHeaderFooterContentAtom): they return the atom the sync plugin
updates in place, even before the first sync. getHeaderFooterDomId(sectionId, target, variant) returns the DOM id of a rendered part.
Each variant’s content is a header_footer_content node in the document. These
core commands add or remove one in the header and the footer together, keeping
their variant sets equal:
import { insertHeaderFooterContent, deleteHeaderFooterContent } from "@stesura/core/commands";
// Adds an empty "first" content where it is missing; false if both already have one:
insertHeaderFooterContent(sectionId, "first")(state, dispatch);
// Removes "first" from header and footer; refuses "default":
deleteHeaderFooterContent(sectionId, "first")(state, dispatch);Page numbers
page_number is an inline field node (pageType: "currentPage" | "totalPages")
that pagination replaces with the real number on each page. Both are
document-wide, as in Word. totalPages is the whole document’s page count
(Word’s NUMPAGES). currentPage continues across sections unless a section’s
pageNumbering attr ({ start, format }, Word’s w:pgNumType) restarts it
or sets its format (decimal, lowerRoman, upperRoman, lowerLetter,
upperLetter). DOCX import sets that attr; there is no command for it yet.
Commands from @stesura/core/commands:
import { insertPageNumber, insertPageOfPage, insertPageNumberPreset } from "@stesura/core/commands";
// At the cursor (typically inside an open header/footer editor):
insertPageNumber({ pageType: "currentPage" })(state, dispatch);
// "3 of 12": two fields around a separator word (default "of"; pass the bare
// word, spaces are added):
insertPageOfPage()(state, dispatch);
// Word-style preset: a borderless 1×3 table in the header (top) or footer
// (bottom), with the number in the left/center/right cell. Dispatch on the
// MAIN editor state; it creates the variant's content if missing, all in one
// transaction:
insertPageNumberPreset({
sectionId,
position: { vertical: "bottom", horizontal: "center" },
pageType: "currentPage", // default
variant: "default", // default
})(state, dispatch);The preset replaces the content when it is a single empty paragraph, and appends to it otherwise.
Distances from the page edge
headerFromTop / footerFromBottom are section attrs (px):
import { setHeaderFooterDistance } from "@stesura/core/commands";
// Current section, or pass a sectionId as the second argument:
setHeaderFooterDistance({ headerFromTop: 48, footerFromBottom: 40 })(state, dispatch);Editing headers/footers from code
In the main editor, header/footer content is protected by
headerFooterGuardPlugin (from @stesura/core/plugins, on by default):
- a transaction whose steps all fall inside headers/footers is cancelled;
- a transaction that also edits the body goes through, then the changed headers/footers are restored to their previous content;
- a selection that lands in a header/footer is moved into the section body.
Collab and undo/redo transactions are exempt. To edit header/footer content (or place the selection there) on the main editor, set this meta:
tr.setMeta("enableHeaderFooterEdits", true);The core header/footer commands that write content (insertPageNumberPreset,
insertHeaderFooterContent, deleteHeaderFooterContent, fixHeadersFooters)
set it for you, and edits made in the open header/footer editor are forwarded
with it.
Healing malformed structures
fixHeadersFooters repairs header/footer structure in one transaction: it
resolves null variants, removes duplicates, gives header and footer the same
variant set (always including default), and gives each content node at least
one paragraph. It returns false when nothing needed fixing. Run it before
mounting editors on documents from untrusted sources:
import { fixHeadersFooters } from "@stesura/core/commands";
fixHeadersFooters(state, dispatch);Next steps
- Pagination — full pagination setup.
- Commands: Sections & Track Changes — all section commands.
- UI Extensions — how
paginationUiExtensioncontributes UI.