Block properties and Property definitions
A Block property is a value that a Block carries, such as a status, a due date or a link to another Block. A Property definition declares one: its key, its label and its value type. One definition type serves the whole editor: the properties capability, the Block inspector, the details popover, the table, the AI context and the server's chat schema all describe a Block property with it. The Property definition contract lists the members.
Define the properties of a Block type
A Block type lists the properties capability with { definitions }. The example defines four Block properties for the Block type decision and sets one of them from code:
import type { BlockType, PropertiesConfig, PropertyDefinition } from '@skaile/forge-editor/composites/schema'
import { blockPropertiesAt, setBlockProperty } from '@skaile/forge-editor/composites'
import type { Editor } from '@tiptap/core'
// One Property definition per Block property: the key under which it is stored in `meta`, its label and its value type.
const definitions: PropertyDefinition[] = [
{ key: 'status', label: 'Status', type: 'select', options: [{ value: 'entwurf', label: 'Entwurf' }, { value: 'fertig', label: 'Fertig' }] },
{ key: 'topics', label: 'Themen', type: 'multiselect', options: [{ value: 'cost', label: 'Kosten' }, { value: 'risk', label: 'Risiko' }] },
{ key: 'owner', label: 'Zuständig', type: 'text' },
{ key: 'related', label: 'Verwandt', type: 'reference', targets: ['decision'] },
]
// The `properties` capability carries them; a Block type without it has no Block properties.
export const decision: BlockType = {
id: 'decision',
label: 'Entscheidung',
icon: 'i-lucide-gavel',
capabilities: [{ id: 'properties', config: { definitions } satisfies PropertiesConfig }],
}
/** Sets `status` on the block around the caret, in one transaction. False when no block there has Block properties. */
export function finishAtCaret(editor: Editor, blockTypes: readonly BlockType[]): boolean {
const at = blockPropertiesAt(editor, editor.state.selection.$from, blockTypes)
return at ? setBlockProperty(editor, at.properties, 'status', 'fertig') : false
}
A Block type without the properties capability has no Block properties: the Block inspector shows no section and the details popover shows no fields.
A status property says where a Block is in its work. It is no record that someone checked it: a checked state belongs in a Prüfung, which is pinned to the content of the Block and lives outside the document (Prüfung).
PropertyDefinition member | Content |
|---|---|
key, label | the key under which the Block property is stored in meta, and its label |
type | one of the value types below |
options | { value, label, group? } choices of a select, multiselect or status property (required for multiselect); group ('todo' | 'doing' | 'done') only on status options |
targets | reference only: the Block type ids the Block reference may point to; absent = any Typed block |
hidden | the property is stored and queried like any other, but the Block inspector, the details popover and the Block table do not show it: for a JSON value an own view edits (the query of a collection) |
The value types are the entries of PROPERTY_TYPES (@skaile/forge-editor/core):
| Type | Stored value | Notes |
|---|---|---|
text, textarea | string | a textarea property gets no column in the Block table |
date | string | formatted in the table |
number | number | a numeric string is converted; an empty value removes the key |
select | string | one of options. A select property with the key status also shows as the status chip on the title line |
multiselect | string[] | several of options, which are required |
reference | block ID | a Block reference to a Typed block of the Document; targets limits the Block types. It implies the references capability |
bibliography | CSL-JSON item in meta.bib | needs the bibliography capability; without it the definition drops out of the fields |
derivation | the Derivation | read-only |
checkbox | true | a switch; unchecked removes the key |
url | string | a link as text, edited in an <input type="url"> |
status | string | one of options like select, whose options carry a group (todo, doing, done). A property with the key status shows as the status chip on the title line; its icon and color follow the option's group |
reference-list | BlockAddress[] | several Blocks ({ documentId, blockId }; documentId '' = this Document). The form lists them as removable chips and adds a Typed block of the Document from a select; entries that are no valid address are dropped (blockAddressesOf), and the key is removed when none is left |
source-anchor | SourceAnchor | a passage of a PDF: { page, rects, quote: { exact, prefix?, suffix? } } (SourceAnchor, sourceAnchorOf, @skaile/forge-editor/core). Read-only in the form (page and quoted text); the add-on @skaile/forge-editor-pdf-viewer creates it from a text selection. rects are fractions (0 to 1) of the page; the value is kept only with a page of 1 or more and a non-empty quote.exact, otherwise the key is removed |
multiselect properties. A multiselect property stores its choices as a string[] under its key: meta.topics = ['a', 'b']. The details popover and the Block inspector show a multi-choice menu (a checkbox list in the React binding). An empty choice removes the key from meta, and entries that are not strings are dropped (metaWithProperty). Table cells, sort keys and the AI brief read the option labels joined by , , and the chat model is told to write a JSON list.
Do not declare a Property definition with a reserved meta key (derivation, bib, basket, titleHidden). Every Block type keeps those four itself (Reserved meta keys). A key must not start with $ either: $title, $tags, $type, $id, $doc and $basket are the pseudo-fields of a Block query, and the Block type config check rejects such a key.
Property kinds
PROPERTY_KINDS (@skaile/forge-editor/core) holds one PropertyKind per entry of PROPERTY_TYPES: which filter operators apply (ops), the ordering key of a value (sortKey), the bucket it falls into when rows are grouped (groupKey), the aggregate functions that make sense (aggregates), the value as text (text(value, definition, env), with env: QueryEnv carrying the locale and time zone), whether a cell truncates it (longText) and the column alignment (align). A query and a table read this registry instead of checking the type themselves. statusGroupOf(definition, value) gives the todo, doing or done group of a status value.
Where the values live
Block properties live in the meta attribute of the node. Only the node of a Typed block has that attribute today, so only Typed blocks carry Block properties. The properties capability applies to any node that has a meta attribute and whose Block type lists it (carriesProperties(node)).
Storage is unchanged by the mechanism. In Markdown the values travel in the fence's meta JSON (::: decision {"status":"open"}). changeBlockType, conversions and the fence keep only the keys that the target Block type declares, plus the four reserved ones. A global meta attribute on every Block is not part of the editor: it would need a Markdown block-attribute syntax first, because the fences write meta only inside ::: kind {meta}.
The Block inspector and the other views
The Block inspector (CompositeInspector) shows a section „Blockeigenschaften“ (English: "Block properties") for any Block that has the properties capability, with one input per Property definition of its Block type. The „Dokument“ section stays the Document's own facts. The details popover, the Block table and the AI brief read the same definitions. Every edit is one editor transaction, so it reaches collaborators and undo like any other edit. A Derived Block (any Block whose capabilities include derived) also shows its Derivation in the inspector as „Herkunft“ (English: "Origin"), with „Quelle fehlt“ ("Source missing") when the original is gone, whatever its Block type; a Block type with a derivation Property definition shows it once, through that definition. The model reads it as state.derivation (InspectorDerivation: node, derivation, sourceMissing; null when the Block is not Derived or the definition shows it).
An own view reads the Block properties at the caret from the framework-free model createCompositeInspector(editor) (@skaile/forge-editor/composites): state.properties is a BlockProperties (id, pos, meta, definitions), and setField(definition, value) writes one text value to it (the structured types reference, bibliography, derivation, multiselect, checkbox, reference-list and source-anchor have their own editors and are ignored). The functions below work without it:
Function (@skaile/forge-editor/composites) | Does |
|---|---|
carriesProperties(node) | whether the node type has the meta attribute |
blockPropertiesAt(editor, $pos, blockTypes) | the innermost Block around $pos that carries properties, with its node, or null |
blockPropertiesOf(node, pos, blockTypes, capabilities) | the BlockProperties of one node |
propertyHostOf(doc, target) | finds the Block again by its block ID (or its position without an ID) |
setBlockProperty(editor, target, key, value) | stores one value in one transaction; false when the Block is gone or carries no properties |
metaWithProperty(meta, definition, value) | the new meta object; an empty value removes the key |
setBlockProperty finds the Block again by its block ID, so a position read before a remote change stays usable. A key that the fields do not define is written as text. A table row (CompositeRow) carries the Property definitions of its Block type as fields.
Under collaboration. meta is one attribute of the Block, which Yjs syncs as a whole value; two users who each write their whole meta at the same moment would lose one key. In a collaboration session whose backend offers collaboration.patchProperty (the HTTP backend), setBlockProperty therefore sends the one key to POST /api/editor/documents/:id/property, the collaboration server writes it into the open document (metaWithProperty, one writer) and the sync delivers it to every client: nothing is written locally, and the call returns true when the Block exists. When the server refuses (the document is not open there, the user is read-only, the Block is unknown), the value is written locally and the editor shows the notice „Eigenschaft konnte nicht synchron gespeichert werden“. Writes by single-actor flows (derive, outline cleanup) stay whole-meta writes.
Derive methods and the Derivation index
Register one way to make a block from another block, find the Derived blocks of an original and handle a missing source.
Image studio
The Bild-Studio block: a picture with its description, prompt blocks, references, previous generations and chat. The node, its Markdown form, the state reducers, the images backend port and the dialog for your own store.