Skip to Content
Track Changes

Track Changes

Track changes (suggestion mode) records edits as pending suggestions instead of applying them: deleted text stays in the document, struck through, and inserted text and format changes are marked. Reviewers accept or reject each suggestion, or all of them at once.

Packages

PackagePurpose
@stesura/track-changesPlugin, transaction modifier, commands, document compare
@stesura/track-changes-reactReview toolbar group, context-menu entries, format-change tooltip

Setup

Track changes needs three pieces:

  1. trackChangesPlugin() among your plugins. It holds the on/off and show/hide state and syncs it with the user’s preference.
  2. trackChangesTransactionModifier, which rewrites each transaction into suggestions. Pass it to the mount hook (main editor) and to StesuraEditor (sub-editors: headers/footers, footnotes, …).
  3. trackChangesUIExtension for the built-in controls, or the individual components below.
import { trackChangesPlugin, trackChangesTransactionModifier } from "@stesura/track-changes"; import { trackChangesUIExtension } from "@stesura/track-changes-react"; import { StesuraEditor, useLocalEditor } from "@stesura/editor-react"; const uiExtensions = [trackChangesUIExtension]; const Editor = () => { const { editorState, dispatch, pluginFactory, schema } = useLocalEditor({ transactionModifier: trackChangesTransactionModifier, // main editor extraPlugins: () => [trackChangesPlugin()], }); return ( <StesuraEditor state={editorState} dispatchTransaction={dispatch} schema={schema} pluginFactory={pluginFactory} transactionModifier={trackChangesTransactionModifier} // sub-editors uiExtensions={uiExtensions} /> ); };

Building plugins by hand? Append trackChangesPlugin() to stesuraPlugins(schema, editorId, configuration?, options?) from @stesura/core/plugins.

Suggestions are attributed to the currentUser of the surrounding StesuraUserProvider. Without one they are attributed to a shared anonymous “Unknown” user, so every anonymous session counts as the same author.

How suggestions behave

  • Deleting text marks it as deleted; it stays in the document until the deletion is accepted. Deleting your own pending insertion removes it outright; deleting someone else’s marks it as deleted, so the deletion can itself be rejected.
  • Typing marks the new text as inserted. Consecutive adjacent edits by the same user, less than 5 seconds apart, join into one suggestion.
  • Formatting (bold, font size, alignment, heading level, …) is recorded with its previous value, so rejecting restores it. Formatting inside a pending insertion is not recorded separately; it is part of the insertion.
  • Hiding suggestions (toggleTrackChangesVisibility) shows the document as if every suggestion were accepted. Tracking continues while hidden.
  • With tracking off, edits apply normally. Pasted content never brings pending suggestions along: track-changes marks and attrs are stripped from it.
  • Collaborators’ changes and undo/redo are not re-tracked: they were tracked where they were made.

Suggestions are stored in the document, so they sync through collaboration and round-trip through DOCX as Word revisions.

Editor modes

In mode="review", every local edit is a suggestion whatever the user’s toggle: disableTrackChanges and toggleTrackChanges return false, and the toolbar toggle shows as on and locked. If an edit can’t be recorded as a suggestion it is dropped rather than applied untracked. mode="review" throws without the track-changes plugin. See Editor modes.

Commands

All from @stesura/track-changes. Full reference: Commands: Sections & Track Changes.

CommandDescription
enableTrackChanges(show?)Turn tracking on. Also turns visibility on, unless show says otherwise.
disableTrackChanges(show?)Turn tracking off. Pending suggestions stay.
toggleTrackChanges(show?)Toggle tracking, with the same visibility rules.
toggleTrackChangesVisibilityShow or hide suggestions.
resolveTrackChanges(action, moveToNext?)Accept ("approve") or reject ("reject") the suggestion at the cursor, or every suggestion a selection touches. moveToNext then selects the next one.
resolveAllTrackChanges(action)Accept or reject every suggestion in the document.
resolveAllAndStopTrackChanges(action)The same, and turn tracking off, in one transaction. Undo restores the suggestions but not tracking, as in Word.
selectTrackChanges(id)Select the suggestion with this id.
selectNextTrackChangesSelect the next suggestion after the selection.

The resolve and disable commands return false when they would change nothing (no suggestions, tracking already off), so toolbar buttons that probe them disable themselves.

An accept or reject at the cursor resolves only the suggestion the cursor is in: the same author, the same edit burst, and the same kind, so accepting an insertion doesn’t also accept an adjacent deletion. A selection resolves every suggestion it touches, in full.

Resolving programmatically

resolveTrackChangesInRange(state, from, to, tr, action, filter?) resolves suggestions in a range into your own transaction. Set the skipTrackChanges meta on it, as the commands do, so the modifier doesn’t track the resolve itself:

import { resolveTrackChangesInRange } from "@stesura/track-changes"; const tr = state.tr; resolveTrackChangesInRange(state, from, to, tr, "approve", { trackChangesId }); tr.setMeta("skipTrackChanges", true); dispatch(tr);

ResolveTrackChangesFilter fields combine with AND; without a filter everything in the range is resolved.

FieldKeeps
userIdSuggestions by this author.
trackChangesIdSuggestions with this id.
dateMs, burstMsSuggestions in the edit burst around dateMs: the window grows while each next suggestion is within burstMs (default 60 000) of the last.
categoryOnly "insertion" or "deletion" suggestions (format changes are not affected).

Reading state

import { isTrackChangesEnabled } from "@stesura/track-changes"; import { trackChangesKey } from "@stesura/core/plugins"; const enabled = isTrackChangesEnabled(view.state); // is tracking on? const pluginState = trackChangesKey.getState(view.state); // { enabled, show, hasPending, … }

For review UIs, @stesura/track-changes/helpers exports:

  • getTrackChangesAtCursor(state): which kinds of suggestion are at the selection (insertion, deletion, deleteClosure, modification), readable descriptions of any format changes, and the author and date of the closest one.
  • describeModificationsAtPos(state, pos): every format change at a position, as ModificationDescriptions (kind, key, and English label / previous / next, e.g. Block type: ParagraphHeading).

How suggestions are stored

The marks and attributes are part of the core schema; no schema extension is needed.

Inline marks. Each carries id, date, userId and userName.

MarkRendered asMeaning
insertiongreen, underlinedPending inserted content.
deletionred, struck throughPending deleted content, kept until accepted.
modificationdouble underlineA pending mark change: type: "addMark" with the added mark in newValue, or type: "removeMark" with the removed one in previousValue.

The underline colour identifies the author (one of ten colours, the same palette as collaboration cursors). Marks can stack: a deletion can sit on someone else’s insertion or on a format change.

Block attributes, for changes to blocks rather than text:

AttributeMeaning
trackChangesThe block’s pending lifecycle: insertion (whole block inserted, or a paragraph break from Enter), deletion (whole block deleted), or deleteClosure (only its paragraph break deleted; accepting joins it with the next block).
trackChangesModificationPending attribute and block-type changes, storing each attribute’s previous value in attrs.

Error reporting

A failed rewrite or resolve is logged to the console. To forward failures to your own reporter:

import { setTrackChangesErrorHandler } from "@stesura/track-changes"; setTrackChangesErrorHandler((failure) => { // failure.phase === "rewrite": an edit could not be tracked. It landed // untracked (review mode: it was dropped). // failure.phase === "resolve": an accept/reject failed; the document was not changed. reportToMyService(failure); });

Reports contain step types, positions and sizes only, never document content.

Document compare

createDiffState builds an EditorState that shows the changes from one document version to another as suggestions:

import { createDiffState, readJsonFile } from "@stesura/track-changes/compare"; const diffState = createDiffState(schema, startJson, endJson, plugins);

The result is startJson’s content with tracked insertions, deletions and format changes that turn it into endJson. Don’t include trackChangesPlugin() in plugins: a local copy is added that leaves the user’s preference alone. It throws if the diff can’t be tracked; show the error rather than a state that would look unchanged.

readJsonFile(file) reads a user-picked .json document (up to 16 MB) for comparison.

Lower-level building blocks

  • transformToTrackChangesTransaction(tr, state, userData?, forceTrackChanges?, trackChangesId?) rewrites one transaction into its tracked form. It’s what the modifier calls; use it when building suggestions outside the dispatch pipeline. forceTrackChanges tracks even with tracking off, and throws instead of falling back to an untracked edit. Pass your own trackChangesId to resolve the result later by id (this is how AI suggestions are reviewed).
  • @stesura/track-changes/testing has test utilities: createTrackChangesTestState (real plugin), trackChangesStubPlugin, normalizeRevisionIds (for comparing docs with generated ids and dates), and a test schema with builders.

React components

From @stesura/track-changes-react:

ExportPurpose
trackChangesUIExtensionAdds the review-tab toolbar group, context-menu entries and format-change tooltip through uiExtensions.
TrackChangesGroupThe toolbar group: tracking toggle, visibility toggle, accept and reject menus.
ToggleTrackChangesButtonTracking on/off. Locked in review mode.
ToggleTrackChangesVisibilityButtonShow/hide suggestions.
TrackChangesAcceptButton / TrackChangesRejectButtonMenus: this change and move to next, all changes, all changes and stop tracking.
TrackChangesContextMenuAccept/reject in the right-click menu when the cursor is on a suggestion, with what changed and who changed it.
TrackChangesModificationTooltipHover tooltip describing a format change, with author and date.

Next steps

Last updated on