Headers and footers
@nextgensoftwares/folio-plugin-header-footer gives documents Word-grade headers and footers: real content (formatted text, tabs, images, tables), fields (page number, total, date, title, current heading, your own variables), a different first page, different odd and even pages, per-section variants with Link to previous, section breaks, page-number restarts and formats, and editing in place: double-click a header and type.
Nothing here is book-specific. A report, a contract or notes use the same features; chapters are an optional way to get sections (see Sections).
import { composePlugins } from '@nextgensoftwares/folio-editor';
import { headerFooterPlugin, settingsFromConfig } from '@nextgensoftwares/folio-plugin-header-footer';
const headerFooter = headerFooterPlugin({
// A document without settings starts from the host's old left/centre/right zones.
seed: () => settingsFromConfig(myOldHeaderFooterConfig),
// Host variables offered in the field picker (values come from layout `variables`).
variables: () => ({ user: me.name }),
});
const composed = composePlugins(standardSchema, [headerFooter /* , tocPlugin(), ... */]);// pageLayers draw the editing labels; the plugin's overlay and settings
// slots render the in-place bar and "Header & footer" settings.
<FolioView editor={editor} painters={composed.painters} pageLayers={composed.pageLayers} ui={composed.ui} />Layout needs a headerFooter option (any value, even {}) to draw bands:
layoutDocument(doc, { measurer, theme, headerFooter: {}, variables: { user: me.name } });Reflow (mobile) views drop that option, so headers and footers are hidden there, and in-place editing needs the print layout.
Where the content lives
Settings are stored in the document, as the doc attribute headerFooter (ProseMirror 1.22+ setDocAttribute), not in the host or a sidecar:
- Undoable: every edit is one step in the main history (typing in a header and pressing Ctrl+Z after leaving it undoes it, as in Word).
- Collaborative: the step (
DocAttrStep) goes throughprosemirror-collab,@nextgensoftwares/folio-syncand the server like any other; checksums and snapshots already include doc attrs. - Saved and exported with the document; PDF and DOCX read the same data.
The trade-off: the whole headerFooter value is one attribute, so two people editing headers at the same moment resolve last-writer-wins for that attribute (each step carries the full value). Header edits are small and rare, which is why this was preferred over per-character merging.
interface HeaderFooterSettings {
enabled?: boolean; // false: no bands at all
differentFirstPage?: boolean; // the first section's; sections override
differentOddEven?: boolean; // document-wide, like Word
headerDistance?: number; // px from the top edge (default: the top margin); sections may set their own
footerDistance?: number; // px from the bottom edge; sections may set their own
pageNumbers?: { format?: '1' | 'i' | 'I' | 'a' | 'A'; start?: number };
title?: string; // the Title field
variables?: Record<string, string>; // user variables for var fields
header?: { default?: FolioNode[]; first?: FolioNode[]; even?: FolioNode[] };
footer?: { default?: FolioNode[]; first?: FolioNode[]; even?: FolioNode[] };
sections?: Record<string, SectionSettings>; // keyed by the section start's hfSection id
}Header and footer content is ordinary Folio JSON (paragraphs with marks and alignment, resizableMedia, tables, lists) plus the hfField inline atom. Text defaults to the theme's chrome size and colour. A tab in a paragraph moves to Word's header tab stops: text before the first tab at the start, then the centre, then the end ("Title\t\tPage").
Fields
| Field | Shows | Word field in DOCX |
|---|---|---|
page | the printed page number in the section's format (format overrides: i, I, a, A) | PAGE (\* roman…) |
numpages | total pages | NUMPAGES |
sectionpages | pages in this section (offered only when there are sections) | SECTIONPAGES |
date | today, format like MMMM d, yyyy, yyyy-MM-dd, dddd | DATE \@ "…" |
title | the document's title setting, else a host title variable, else the first heading | text |
heading | the current heading of level (default 1): the first on the page, else the last before it, else the next one (Word's STYLEREF search) | STYLEREF "Heading N" |
var | name: a host variable, else the document's user variable | text |
The picker (in the edit bar) lists title, page number, total pages, date, current heading, then the user's variables and the host's. Labels are renamable for your product (fieldLabels: { heading: 'Chapter' }), and variables are added, renamed and deleted in settings. Old templates keep working: settingsFromConfig turns {{page}} / {{total}} / {{date}} into fields and any other {{name}} (e.g. {{bookTitle}}) into a variable field.
Fields are inline atoms: one character to the caret, filled per page by layout. Pages showing the same values share one measured layout of the content.
Variants
Every section has three kinds of header and footer, as in Word:
- First page (
first), used on the section's first page whendifferentFirstPageis on. Off by default; when on and empty, the first page has no header (Word). - Even page (
even), used on even printed numbers whendifferentOddEvenis on (document-wide). Mirrored books put the page number at the outside edge:default: [p('\t\t', page)],even: [p(page)]. - Default (all other pages; the odd pages with odd/even on).
Sections: chapters and section breaks
A document is one section unless it has section starts:
- Chapters. With chapters on (
doc.attrs.chapters, default on, level 1), every top-level heading of the chapter level starts a section. Turn them off in settings (orsetChapters({ enabled: false })) and headings are just headings. The heading level is configurable (a book with Parts may use H2). - Section breaks. Any top-level paragraph or heading with
sectionBreak: truestarts a section (Insert ▸ Header & footer ▸ Section break (next page), orinsertSectionBreak()). Breaks work with chapters off. - Odd / even page breaks.
pageBreakBefore: 'odd'(or'even') on a section start (Insert ▸ Header & footer ▸ Section break (odd page) / (even page),insertSectionBreak('oddPage' | 'evenPage')) starts the section on the next odd (even) page: when the page after the break would have the wrong number, layout inserts a blank page, as Word does. The blank page belongs to the section before it (its header and footer, numbering continues). Parity is the page's number counted fromfirstPageNumber. DOCX writesw:typeoddPage/evenPageand reads them back.
A section's settings live in settings.sections[id], where id is the start block's hfSection attribute (assigned the first time the section gets its own settings; a copy-pasted start with a duplicate id links to the previous section instead of sharing). Everything a section doesn't set is linked to the previous section: each part kind, differentFirstPage and the number format. Unlinking ("Link to previous" off) copies what the page shows into the section's own part; relinking drops it. Editing a linked header edits the section it comes from (Word's "Same as previous").
A page belongs to the section holding its first block; a section's first page restarts numbering when pageNumbers.start is set.
Page setup and columns per section
A section may also set its own page (width, height, margin) and columns (count, gap, separator, unequal custom widths): a landscape section for a wide table inside a portrait book, a newsletter section in three columns. Missing fields inherit from the previous section; { count: 1 } ends inherited columns. A section break without pageBreakBefore is continuous (Insert ▸ Header & footer ▸ Section break (continuous), or insertSectionBreak(true)): the new section starts on the same page, below the previous one's (balanced) columns, when the page size is the same. These settings work without any header or footer (enabled: false). The page setup dialog of @nextgensoftwares/folio-react applies to "This section" or the "Whole document" (applySectionSetup, sectionSetupOf, applyPageSetup).
Different header on one page
Different on this page (edit bar) wraps the page in section breaks: its first block starts a section with the chosen look, the block after its last starts one that restores the previous look (numbering continues). The breaks belong to blocks, not to the page: if text before them grows, the special header moves with its content, and the first block starts a new page (it is a next-page break). A page starting with a table or list gets an empty paragraph to carry the break.
Editing in place
Double-click a header or footer band on any page (print layout):
- the body dims, every page shows its bands' labels ("Even page header — Measuring text", "Same as previous"), and the caret goes into the header;
- typing, marks (toolbar and shortcuts), alignment, Tab, images and fields work; edits apply to every page using that variant;
- the bar offers: insert field, different first page, different odd & even, link to previous, different on this page, distance from the edge (of the page's section: other sections keep theirs), close;
- clicking another page's band edits that one; Esc or a double-click in the body leaves.
How it works
HeaderFooterSession is a nested FolioEditor (same schema, keymap and commands) whose document is the variant's content and whose layout is that content laid out exactly as the page shows it (pageChrome(page).header.layout(content, true): same geometry, fields as 1-wide atoms, PM-compatible offsets). It has no history of its own: each change is written to the main document as one headerFooter step, so undo/redo and collaboration go through the main editor; changes arriving from elsewhere (undo, a collaborator) flow back into it.
While a session is active the main editor delegates input to it (editor.setDelegate, see EditorDelegate): keys, text, pointer events, run (toolbar commands) and caret/selection queries go to the session, so the toolbar, menus and view work on the header without knowing about it. Entering is a pointer hook (editor.onPointerDown) installed by the plugin's overlay component (or plugin.edit(editor, 'header')).
Known limits: the toolbar's undo button reads the header's (history-less) state and shows disabled while editing (Ctrl+Z works); action items that insert into "the document" (e.g. an upload dialog) still target the body.
Layout: bands grow with their content
A band is max(theme band, distance − margin + content height): a taller header pushes the body down (Word), only on pages showing that variant, and the body re-paginates. Pagination asks each new page's capacity with the offset where it starts, so the page's section, number and header kind (hence its band) are known exactly while paginating. Pages kept from the previous layout and its converged tail are checked afterwards; if one changed, the layout re-runs once in full. Edits that keep band heights re-lay out only the chrome: page objects are re-wrapped around the same body fragments.
Page reuse compares each page's chrome inputs (variant content, the field values it shows, distances, label): a page that only shows page keeps its object when the total changes; one that shows numpages gets new chrome but the same body.
Export
- PDF / canvas: drawn from
page.chrome, nothing to configure. - DOCX: one Word section per document section, with
titlePg,w:pgNumType(start and format), next-page or continuous starts, and header and footer parts per kind with live fields (PAGE,NUMPAGES,SECTIONPAGES,DATE,STYLEREF "Heading N"), header tab stops, alignment, images and tables. Linked parts are left out so Word's own Link to previous reproduces them;evenAndOddHeadersis set document-wide. Watermark layers are added per section (then every part is written in full so they never break a link). Each section writes its own page size, orientation, margins andw:cols;columnBreakBeforebecomes a column break.
Commands
| Command | Does |
|---|---|
setHeaderFooter(patch) / removeHeaderFooter() | document-level settings |
setSectionOptions(key, patch) / setPageNumbering(key, {format, start}) | a section's first page, numbering and header/footer distances ('' = document level) |
setLinkToPrevious(pageChrome(page), zone, linked) | Word's Link to previous for the page's section |
setPageSectionOptions(pageChrome(page), patch) | same, giving the section an id when needed |
differentOnPage(pageChrome(page), { header, footer }) | section breaks around one page |
insertSectionBreak(kind?) / removeSectionBreak() | at the caret's block: next page (default), true / 'continuous', 'oddPage', 'evenPage' |
setChapters({ enabled, level }) | chapters on/off and their level |
insertField(attrs) | in the header being edited (editor.run) |