Skip to Content
References

References

Footnotes, endnotes and cross-references.

PackageContents
@stesura/referencesPlugins, commands, helpers and the note-panel store. Framework-free.
@stesura/references-reactreferenceUiExtensions: toolbar groups, note panels, the cross-reference panel and menus, anchor node views.

The nodes themselves (footnote_anchor, cross_reference, …) are part of the core schema, so stesuraSchema() already has them. See Schema.

Setup

import { referencesPlugins } from "@stesura/references"; import { referenceUiExtensions } from "@stesura/references-react"; import { StesuraEditor, useLocalEditor } from "@stesura/editor-react"; const uiExtensions = [referenceUiExtensions]; const Editor = () => { const { editorState, dispatch, pluginFactory, schema } = useLocalEditor({ extraPlugins: () => referencesPlugins(), }); return ( <StesuraEditor state={editorState} dispatchTransaction={dispatch} schema={schema} pluginFactory={pluginFactory} uiExtensions={uiExtensions} /> ); };

referencesPlugins() returns every plugin the feature needs, already in the right order: cross-reference resolution, cross-reference activation, note numbering and the two note-panel bindings. Mount it after the core numbering plugins (as extraPlugins does), so cross-references see counters updated in the same transaction.

To number notes without cross-references or the note panels, mount notesPlugin() on its own.

Footnotes and endnotes

Both kinds work the same way. Only where they render differs, and endnotes take their number format from the document’s endnotesOptions.

  • Anchor. An inline footnote_anchor / endnote_anchor in the text, carrying the note’s ref. Several anchors can share a ref; they share its number.
  • Body. A footnote_body / endnote_body holding the note’s content, stored in a document-level container (footnotes, or endnotes, which is created on the first endnote). Bodies are edited in the note panel, never inline.

The notes plugin numbers anchors in document order and keeps bodies matched to them. It creates a missing body, deletes orphaned or duplicate bodies, and gives a ref to anchors that arrive without one (DOCX import, stripped paste).

Notes are not allowed in headers or footers (as in Word, a header repeats on every page), nor inside another note’s body. The sub-editors refuse them. Anything that slips through a paste, import or collab commit is removed by the plugin, and its body goes with it.

Editing

The note panel opens at the bottom of the document on:

  • a double-click, Ctrl/Cmd+Click, or Enter/Space on an anchor;
  • inserting a note;
  • the toolbar’s “open” buttons (References and View tabs), which open the note after the cursor;
  • a search match inside a note body.

The panel shows one note at a time, with Previous/Next, Go to anchor (scrolls the main editor without moving focus) and Delete. It closes on Escape or a click on the overlay. Only one panel, footnote or endnote, is open per editor.

In view mode the open buttons stay enabled and the panel is read-only.

Commands

import { deleteFootnote, insertFootnote, navigateFootnote, nextFootnote, previousFootnote, } from "@stesura/references";

Endnotes have the same five: insertEndnote, deleteEndnote, nextEndnote, previousEndnote, navigateEndnote.

CommandSignatureBehaviour
insertFootnote(ref?, onSuccess?) => CommandInserts an anchor at the cursor, or at the end of a non-empty selection (kept, not replaced). Omit ref to create a new note; pass one to add another anchor to an existing note. onSuccess(ref) runs on dispatch only. False in a code block, on a selected table or a gap cursor.
deleteFootnote(ref) => CommandDeletes the body and every anchor of the note in one undo step.
nextFootnote(onSuccess?) => CommandSelects the next anchor after the cursor, wrapping around. The probe is true whenever the document has a footnote.
previousFootnote(onSuccess?) => CommandThe same, backwards.
navigateFootnote(ref, 1 | -1, onNavigate?) => CommandSelects the note after/before ref in numbering order. Doesn’t wrap: false at either end. Drives the panel’s Previous/Next.

nextFootnote and previousFootnote move the selection only. They don’t open the panel.

Reading note state

import { getEndnotes, getFootnotes } from "@stesura/references"; const footnotes = getFootnotes(view.state); // FootnoteItem[], in numbering order const endnotes = getEndnotes(view.state); // EndnoteItem[]

Each item has the note’s ref, index (0-based), occurrences and the sectionId of its first anchor. The arrays keep their identity until the notes change, so they are safe as useSyncExternalStore snapshots.

Panel state

Which note is open is held per editor in nanostores, exported from the root:

import { activeNoteRefAtom, closeNotePanel, openNotePanel } from "@stesura/references"; openNotePanel(editorId, "footnote", ref); // also closes the endnote panel openNotePanel(editorId, "footnote", ref, { focusOnOpen: false }); // keep focus where it is closeNotePanel(editorId, "footnote"); activeNoteRefAtom(editorId, "endnote"); // atom<string | null>, stable per editor + kind

You only need the rest for a custom panel shell: noteFocusRequestAtom (consumed by <FocusOnRequest> from @stesura/editor-react/sub-editor), footnoteEditorRegistry / endnoteEditorRegistry, and the sub-editor kinds footnoteKind / endnoteKind.

Cross-references

A cross-reference is an inline cross_reference node pointing at another node’s id. The target can be any node type carrying the numbering global attribute (headings and paragraphs by default), numbered or not, as long as it’s outside a header or footer.

refToShows
"default"The target’s number as displayed, e.g. 3.1 or b).
"fullContext"Every level of the target’s number, joined with separator, e.g. 2.3.a.
"aboveBelow"“above” or “below”, in the target’s language.

The plugin keeps every reference’s label in sync with its target: renumbering, moving the target or changing its language updates the text. A reference to an unnumbered target keeps the text it was inserted with (the heading’s text, from the panel). If the target is deleted, the reference keeps its last label and is flagged missingReference, which renders it as broken. It recovers if the target comes back, e.g. on undo.

Cross-references are allowed in note bodies. In a header or footer, the plugin replaces them with their label as plain text.

Editing

  • Insert. The References tab opens the cross-reference panel. Pick a target (numbered items or headings), a kind and, for fullContext, a separator. Then double-click the target or press Insert.
  • Edit. Right-click a selected reference and choose Edit. Inserting replaces the selected reference.
  • Preview. Hold Ctrl/⌘ with a reference selected, or use Open preview in the context menu.
  • Follow. Ctrl/Cmd+Click, or Enter on a focused reference, jumps to the target. This also works from inside a note, where it moves the main editor.

Commands

import { canInsertCrossReference, goToCrossRefTarget, insertCrossReference, } from "@stesura/references"; insertCrossReference({ targetId: "heading-node-id", text: "3.1", refTo: "default", separator: null, })(state, dispatch);
CommandBehaviour
insertCrossReference(args)Replaces the selection with a reference and selects it. False if targetId, text or refTo is empty, or where an inline node can’t go (a selected table, a gap cursor). text is only the initial label; the plugin takes over from there.
canInsertCrossReferenceThe position check alone, usable before a target is picked. The toolbar button probes it.
goToCrossRefTarget({ targetId, missingReference })Selects the target and scrolls to it. False for a missing target.

getCrossRef(state) / isCrossRefSelected(state) read the reference the selection covers exactly. computeFullContext and getAboveBelowText compute labels the same way the plugin does.

Import paths

Everything is exported from @stesura/references. Narrower entry points:

SubpathContents
@stesura/references/commandsAll commands above.
@stesura/references/pluginsreferencesPlugins, notesPlugin, getFootnotes / getEndnotes.
@stesura/references/helpersCross-reference reads, and note finders (findNoteAnchor, findNoteBodyPos, findNoteContainer and their footnote/endnote variants).

Next steps

Last updated on