Skip to Content
PackagesCorePlaceholders

Placeholders

A placeholder is a fill-in prompt in a template, such as “[Insert study drug name]”: Word’s content-control placeholder text. It is the core placeholder node, so every stesuraSchema() has it and no setup is needed.

It behaves as one unit, as in Word:

  • Click or an arrow key selects the whole placeholder; the next arrow moves past it.
  • Typing with it selected replaces it with what you type, in the placeholder’s own formatting plus the configured filledMarks. One undo brings the placeholder back.
  • Paste with it selected replaces it with the pasted content, which gets filledMarks.
  • Backspace right after it (or Delete right before it) selects it; the next press deletes it.
  • Double-click or Enter opens a floating editor for its text, tag and title. Enter or Escape closes it and leaves the placeholder selected. Emptying the text deletes the placeholder. Word edits a placeholder’s text in place, and only in Design Mode; this is a deliberate deviation.
  • Mod-Alt-p (Ctrl+Alt+P) inserts one. The Insert tab’s Placeholder menu inserts one and walks the unfilled ones (Next / Previous, with a count).
  • Right-click: Edit placeholder text, Remove placeholder, keep text, Delete placeholder. The menu’s key is placeholder.

The node

AttrType
idstring | nullSet by the uniqueId plugin; addresses one instance.
tagstring | nullMachine key for programmatic fill; may repeat. DOCX w:tag.
titlestring | nullHuman label, shown as a tooltip. DOCX w:alias.

PlaceholderAttrs types them. The node is an inline atom with content: "text*": text runs with their marks, nothing else (no hard breaks, no nested placeholders). Its node view has no contentDOM, so the caret never enters it, as with inline math. It renders in var(--stesura-placeholder-color, #808080), Word’s Placeholder Text grey; a colour mark on the text wins. The hover and selection chrome is background and outline only, so the printed width is the edited width, and print mounts get none.

Commands

import { fillPlaceholders, insertPlaceholder, removePlaceholder, selectPlaceholder, setPlaceholderAttrs, } from "@stesura/core/commands"; insertPlaceholder({ text: "[Study drug]", attrs: { tag: "drug" } })(state, dispatch); fillPlaceholders(new Map([["drug", "Aspirin"]]))(state, dispatch);
  • insertPlaceholder({ text?, attrs? }) wraps a text selection in one paragraph (marks kept), or inserts at the caret with text, else the configured defaultContent, else DEFAULT_PLACEHOLDER_TEXT (“Click or tap here to enter text.”; editor-react passes the localised one). It selects the new placeholder. Refused in code blocks, on or in a placeholder, across blocks, and for a selection holding anything but text.
  • removePlaceholder({ pos?, keepText? }) deletes the placeholder at pos (default: the selected one), or with keepText unwraps it to its text.
  • setPlaceholderAttrs(pos, { tag?, title? }); an empty string clears one.
  • fillPlaceholders(values) fills by tag from a map, or through a function of a PlaceholderInfo (match on id, title or text), in one transaction. A value replaces the placeholder with the same marks as typing over it; "" removes it; null leaves it. With track changes on, the fill is tracked.
  • selectPlaceholder(1 | -1) selects the next or previous placeholder in the body, wrapping. Header, footer and note bodies are counted by findPlaceholders but not navigated.

The floating editor’s state and commands come from @stesura/core/plugins: placeholderPlugin (in customPlugins; opt out with placeholderPlugin: false), getPlaceholderEditorState, openPlaceholderEditorTarget, openPlaceholderEditor, openPlaceholderEditorAt(pos), closePlaceholderEditor, setPlaceholderText(pos, text), placeholderEnterCmd (bound to Enter), typeOverPlaceholder(text) (what typing over runs) and unwrapOpenPlaceholders(slice) (the paste rule: a placeholder a slice cuts open becomes its text). placeholderPluginKey holds the state, a PlaceholderEditorState.

setPlaceholderText writes the minimal step, as math’s source editor does, and skips text struck by track changes: liveTextOf(node) is the text without it.

Helpers

From @stesura/core/helpers: findPlaceholders(doc, { includeHidden? }) (every placeholder, as PlaceholderInfo), getSelectedPlaceholder(state), placeholderAt($pos), isPlaceholder(node), placeholderTextMarks(node) (its first run’s marks), filledMarksFor(state, node), fillMarksFor(state, node, parent) (what replacing text gets), getPlaceholderConfig(state) and createPlaceholderNode(schema, content) (from a PlaceholderContent, marks as PlaceholderMarkJSON).

Configuration

config.placeholders (PlaceholderConfig), config-time; a sub-editor uses its mount’s:

const config = { placeholders: { // Content for a new placeholder with no text of its own. defaultContent: ({ parent, pos, state }) => ({ text: "[Enter text]", attrs: { title: "Field" } }), // Marks for text typed, pasted or filled over a placeholder. filledMarks: [{ type: "highlight", attrs: { color: "#FFFF00" } }], }, };

Mark JSON is validated against the schema; unknown marks and marks the paragraph disallows are dropped.

Elsewhere

  • DOCX: imported from content controls showing their placeholder (w:showingPlcHdr), and exported as an inline plain-text control with w:temporary.
  • Markdown: written as the prompt text, with a warning.
  • Track changes: typing over a placeholder strikes it and inserts the text as one suggestion; reject brings it back.
  • Comments: a comment on part of a placeholder anchors around all of it.
Last updated on