Skip to content

Transaction

Transactions represent atomic state changes. They contain a sequence of Steps that transform the document.

Use the TransactionBuilder from an EditorState:

const state = editor.getState();
const tr = state.transaction('command')
.insertText(blockId, offset, 'hello', [])
.build();
editor.dispatch(tr);

The origin parameter is optional and defaults to 'api':

// Equivalent — origin defaults to 'api'
const tr = state.transaction()
.setBlockType(blockId, nodeType('heading'), { level: 1 })
.build();
// Insert text with marks at a position
builder.insertText(blockId, offset, text, marks, segments?)
// Delete text with explicit undo data
builder.deleteText(blockId, from, to, deletedText, deletedMarks, deletedSegments?)
// Delete text — auto-derives undo data from working document
builder.deleteTextAt(blockId, from, to)
builder.addMark(blockId, from, to, mark)
builder.removeMark(blockId, from, to, mark)
// Split a block at offset, creating a new block with the given ID
builder.splitBlock(blockId, offset, newBlockId)
// Merge two blocks with explicit target length
builder.mergeBlocks(targetBlockId, sourceBlockId, targetLengthBefore)
// Merge two blocks — auto-derives target length from working document
builder.mergeBlocksAt(targetBlockId, sourceBlockId)
// Change a block's type and optionally its attributes
builder.setBlockType(blockId, nodeType, attrs?)
// Insert a child node at index under the parent path
builder.insertNode(parentPath, index, node)
// Remove a child node at index under the parent path
builder.removeNode(parentPath, index)
// Set attributes on a node at the given path
builder.setNodeAttr(path, attrs)
// Insert an InlineNode at offset within a block
builder.insertInlineNode(blockId, offset, node)
// Remove an InlineNode at offset (auto-derives from working document)
builder.removeInlineNode(blockId, offset)
// Set attributes on an InlineNode at offset
// attrs: Readonly<Record<string, string | number | boolean>>
builder.setInlineNodeAttr(blockId, offset, attrs)
builder.setSelection(selection)
builder.setNodeSelection(nodeId, path)
builder.setStoredMarks(marks, previousMarks)
// Marks this transaction as allowed in readonly mode. Returns `this` for method chaining.
builder.readonlyAllowed()
const transaction = builder.build();

Each transaction has an origin that describes where it came from:

type TransactionOrigin = 'input' | 'paste' | 'command' | 'history' | 'api';
OriginDescription
inputUser typing / input events
pastePaste operations
commandProgrammatic command execution
historyUndo/redo operations
apiExternal API calls (setContentHTML, setJSON) — this is the default

Each transaction carries a metadata object:

interface TransactionMetadata {
readonly origin: TransactionOrigin;
readonly timestamp: number;
readonly historyDirection?: 'undo' | 'redo';
readonly readonlyAllowed?: boolean;
}
PropertyTypeDescription
originTransactionOriginWhere the transaction came from (see origins above)
timestampnumberWhen the transaction was created (Date.now())
historyDirection'undo' | 'redo'Set when the transaction comes from undo/redo
readonlyAllowedbooleanIf true, the transaction bypasses the readonly guard

Every step is invertible for undo support:

StepDescription
InsertTextStepInsert text at position
DeleteTextStepDelete text range
SplitBlockStepSplit block at offset
MergeBlocksStepMerge two adjacent blocks
AddMarkStepAdd inline mark to range
RemoveMarkStepRemove inline mark from range
SetBlockTypeStepChange block type
SetStoredMarksStepSet stored marks (for mark continuity) — internal, not exported
InsertNodeStepInsert a block node into a parent
RemoveNodeStepRemove a block node from a parent
SetNodeAttrStepChange a node’s attributes
InsertInlineNodeStepInsert an inline node at offset
RemoveInlineNodeStepRemove an inline node at offset
SetInlineNodeAttrStepChange an inline node’s attributes

Every transaction can be inverted for undo:

import { invertTransaction } from '@notectl/core';
const inverse = invertTransaction(transaction);
// Applying inverse undoes the original transaction

Individual steps can also be inverted:

import { invertStep } from '@notectl/core';
const invertedStep = invertStep(step);

Transactions pass through a middleware chain before being applied:

context.registerMiddleware((tr, state, next) => {
// Inspect or modify the transaction
console.log(`${tr.steps.length} steps`);
next(tr); // Call next to continue, or skip to cancel
}, { priority: 100 });

Pure functions for applying and inverting individual steps.

Applies a single step to a document, returning a new Document:

import { applyStep } from '@notectl/core';
const newDoc: Document = applyStep(doc, step);

This is the low-level primitive used by EditorState.apply(). It handles all step types: insertText, deleteText, splitBlock, mergeBlocks, addMark, removeMark, setBlockType, setStoredMarks, insertNode, removeNode, setNodeAttr, insertInlineNode, removeInlineNode, and setInlineNodeAttr.

Returns a step that undoes the given step:

import { invertStep } from '@notectl/core';
const undo = invertStep(step);
// applyStep(applyStep(doc, step), undo) ≈ doc

Returns a transaction whose steps undo all steps of the original, in reverse order:

import { invertTransaction } from '@notectl/core';
const undo = invertTransaction(tr);

Pure functions to determine whether a transaction is allowed in readonly mode.

Returns true if the transaction contains no document-mutating steps (i.e. all steps are setStoredMarks, or there are no steps at all):

import { isSelectionOnlyTransaction } from '@notectl/core';
if (isSelectionOnlyTransaction(tr)) {
// Safe to apply in readonly mode
}

Returns true if the transaction may proceed in readonly mode — either it has metadata.readonlyAllowed set, or it is selection-only:

import { isAllowedInReadonly } from '@notectl/core';
const allowed: boolean = isAllowedInReadonly(tr);

Convenience functions for building selection-only transactions.

Builds a collapsed-cursor transaction and clears stored marks:

import { moveTx } from '@notectl/core';
const tr = moveTx(state, blockId('b1'), 5);

extendTx(state, anchorBlockId, anchorOffset, headBlockId, headOffset)

Section titled “extendTx(state, anchorBlockId, anchorOffset, headBlockId, headOffset)”

Builds a range-selection transaction and clears stored marks:

import { extendTx } from '@notectl/core';
const tr = extendTx(state, blockId('b1'), 0, blockId('b1'), 10);

Builds a node-selection transaction and clears stored marks:

import { nodeSelTx } from '@notectl/core';
const tr = nodeSelTx(state, blockId('img-1'));

State-level functions for moving the cursor to block boundaries.

Moves the cursor to offset 0 of the current block. Returns null if already at the start or if the selection is a node/gap cursor:

import { moveToBlockStart } from '@notectl/core';
const tr = moveToBlockStart(state);

Moves the cursor to the end of the current block. Returns null if already at the end:

import { moveToBlockEnd } from '@notectl/core';
const tr = moveToBlockEnd(state);