Operations & Undo/Redo
Canvus delegates transaction history (Undo/Redo) to the host application. Instead of managing a private internal history stack (which would desynchronize with host state like page metadata, code views, or other workspace widgets), the SDK emits discrete Operation payloads when a visual gesture completes.
Host applications listen for these events, push them onto a global transaction stack, and apply them back to the workspace using the applyOperation replay API.
The Operation Contract
Every operation emitted or replayed adheres to the Operation interface:
interface Operation {
/** The action type class name */
type:
| "update-style"
| "update-classes"
| "reparent"
| "reorder"
| "update-text"
| "create-node"
| "delete-node"
/** Unique selector ID of the target content node */
nodeId: string
/** Specific delta payload for applying this change */
payload: any
/** Reciprocal delta payload to undo this change */
undoPayload: any
}Operation Schemas
Style Update (update-style)
Emitted when dragging a resize anchor or mutating padding/margin spacing adjusters.
{
"type": "update-style",
"nodeId": "card-hero",
"payload": {
"padding-top": "40px",
"padding-bottom": "40px"
},
"undoPayload": {
"padding-top": "28px",
"padding-bottom": "28px"
}
}Class Swap (update-classes)
Emitted when calling addClass, removeClass, or toggleClass.
{
"type": "update-classes",
"nodeId": "card-hero",
"payload": {
"add": ["demo-highlight"],
"remove": []
},
"undoPayload": {
"add": [],
"remove": ["demo-highlight"]
}
}Tree Reparent (reparent)
Emitted when a node is dragged into a different parent container.
{
"type": "reparent",
"nodeId": "flex-child-1",
"payload": {
"newParentId": "flex-container",
"index": 0
},
"undoPayload": {
"newParentId": null,
"index": 2
}
}Tree Reorder (reorder)
Emitted when dragging changes a node’s index among its siblings.
{
"type": "reorder",
"nodeId": "flex-child-2",
"payload": { "index": 2 },
"undoPayload": { "index": 1 }
}Text Update (update-text)
Emitted when editing text content inline and blurring.
{
"type": "update-text",
"nodeId": "card-hero",
"payload": {
"path": [0],
"html": "Canvus SDK v1.2"
},
"undoPayload": {
"path": [0],
"html": "Canvus SDK v1.1"
}
}Create Node (create-node)
Emitted when drawing a new node or pasting/duplicating an existing node.
{
"type": "create-node",
"nodeId": "cloned-1-kxp82f",
"payload": {
"parentId": "flex-container",
"index": 3,
"rawMarkup": "<div class=\"card\">Cloned Content</div>",
"rect": { "x": 120, "y": 120, "width": 200, "height": 100 }
},
"undoPayload": {
"parentId": "flex-container"
}
}Delete Node (delete-node)
Emitted when deleting the selected node from the workspace.
{
"type": "delete-node",
"nodeId": "cloned-1-kxp82f",
"payload": {
"parentId": "flex-container"
},
"undoPayload": {
"parentId": "flex-container",
"rawMarkup": "<div class=\"card\">Cloned Content</div>",
"rect": { "x": 120, "y": 120, "width": 200, "height": 100 }
}
}The path array tracks DOM child indices down from the node’s content root to identify the exact sub-element edited.
Host Integration
1. Listen for Visual Changes
const historyStack: Operation[][] = []
const ws = new Workspace(container, {
onOperationsGenerated(ops) {
// Push the batch of operations onto the host undo queue
historyStack.push(ops)
updateUndoButtonUI()
},
})2. Execute Undo
When the user triggers Undo (e.g., Cmd/Ctrl + Z), pop the operations and apply their undoPayload:
function performUndo() {
if (historyStack.length === 0) return
const ops = historyStack.pop()
// Replay in reverse sequence to preserve dependencies
for (let i = ops.length - 1; i >= 0; i--) {
const op = ops[i]
ws.applyOperation({
type: op.type,
nodeId: op.nodeId,
payload: op.undoPayload, // Apply the undo payload
undoPayload: op.payload, // Swap the reciprocal payload
})
}
}Applying operations triggers a synchronous reflow, geometry measurement updates, and overlay redraws automatically.