Visually compare snapshots of your documents
Tired of playing "spot the difference" with your document versions?
The Snapshot Compare extension adds extra functionality to the Document History by allowing you to visually compare changes made between two versions of a document so you can track what’s been added, removed, or modified. These comparisons are called diffs.
Whether you're collaborating with a team or editing solo, this helps you track changes over time and understand who made specific edits. By highlighting differences within the editor and attributing them to their authors, Snapshot Compare brings transparency and efficiency to the document lifecycle.
Install
Subscription required
This extension requires a valid Entry, Business or Enterprise subscription and a running Tiptap Cloud instance. To install the extension you need access to our private registry, set this up first.
Install the extension from our private registry:
npm install @tiptap-pro/extension-snapshot-compare
Settings
You can configure the SnapshotCompare
extension with the following options:
Setting | Type | Default | Description |
---|---|---|---|
provider | TiptapCollabProvider | null | The Collaboration provider instance |
mapDiffToDecorations | function | () => {} | Control mapping a diff into a decoration to display their content |
const provider = new TiptapCollabProvider({
// ...
})
const editor = new Editor({
// ...
extensions: [
// ...
SnapshotCompare.configure({
provider,
}),
],
})
Using mapDiffToDecorations
for diff decorations
The extension has a default mapping (defaultMapDiffToDecorations
) to represent diffs as ProseMirror decorations.
For more complex integrations and control, you can customize this mapping with the mapDiffToDecorations
option.
Example: Applying custom predefined background colors to inline inserts
SnapshotCompare.configure({
mapDiffToDecorations: ({ diff, tr, editor, defaultMapDiffToDecorations }) => {
if (diff.type === 'inline-insert') {
// return prosemirror decoration(s) or null
return Decoration.inline(
diff.from,
diff.to,
{
class: 'diff',
style: {
backgroundColor: diff.attribution.color.backgroundColor,
},
},
// pass the diff as the decoration's spec, this is required for `extractAttributeChanges`
{ diff },
)
}
// fallback to the default mapping
return defaultMapDiffToDecorations({
diff,
tr,
editor,
attributes: {
// add custom attributes to the decorations
'data-tiptap-user-id': myUserIdMapping[diff.attribution.userId],
},
})
},
})
Storage
The SnapshotCompare
storage object contains the following properties:
Key | Type | Description |
---|---|---|
isPreviewing | boolean | Indicates whether the diff view is active |
diffs | Diff[] | The diffs that are displayed in the diff view |
previousContent | JSONContent | null | The content before the diff view was applied |
Use the isPreviewing
property to check if the diff view is currently active:
if (editor.storage.snapshotCompare.isPreviewing) {
// The diff view is currently active
}
Use the diffs
property to access the diffs displayed in the diff view:
editor.storage.snapshotCompare.diffs
The property previousContent
is used internally by the extension to restore the content when exiting the diff view. Typically, you do not need to interact with it directly.
Commands
Command | Description |
---|---|
compareVersions | Diffs two versions and renders the diff within the editor |
showDiff | Given a change tracking transform, show the diff within the editor |
hideDiff | Hide the diff and restore the previous content |
compareVersions
Use the compareVersions
command to compute and display the differences between two document versions.
editor.chain().compareVersions({
fromVersion: 1,
})
Options
You can pass in additional options for more control over the diffing process:
Key | Description |
---|---|
fromVersion | The version to compare from. The order between fromVersion and toVersion is flexible |
toVersion | The version to compare to (default: latest version) |
hydrateUserData | Add contextual data to each user's changes |
onCompare | Handle the diffing result manually |
enableDebugging | Enable verbose logging for troubleshooting |
Using hydrateUserData
to add metadata
Each diff has an attribution
field, which allows you to add additional metadata with the hydrateUserData
callback function.
Note
Do note that the userId
is populated by the TiptapCollabProvider
and should be used to
identify the user who made the change. Without the user
field provided by the provider, the
userId
will be null
. See more information in the TiptapCollabProvider
documentation.
Example: Color-coding diffs based on user
const colorMapping = new Map([
['user-1', '#ff0000'],
['user-2', '#00ff00'],
['user-3', '#0000ff'],
])
editor.chain().compareVersions({
fromVersion: 1,
toVersion: 3,
hydrateUserData: ({ userId }) => {
return {
color: {
backgroundColor: colorMapping.get(userId),
},
}
},
})
editor.storage.snapshotCompare.diffs[0].attribution.color.backgroundColor // '#ff0000'
Using onCompare
to customize the diffing process
If you need more control over the diffing process, use the onCompare
option to receive the result and handle it yourself.
Example: Filtering diffs by user
editor.chain().compareVersions({
fromVersion: 1,
toVersion: 3,
onCompare: (ctx) => {
if (ctx.error) {
// handle errors that occurred in the diffing process
console.error(ctx.error)
return
}
// filter the diffs to display only the changes made by a specific user
const diffsToDisplay = ctx.diffSet.filter((diff) => diff.attribution.userId === 'user-1')
editor.commands.showDiff(ctx.tr, { diffs: diffsToDisplay })
},
})
showDiff
Use the showDiff
command to display the diff within the editor using a change tracking transform (tr
). This represents all of the changes made to the document since the last snapshot. You can use this transform to show the diff in the editor.
Typically, you use this command after customizing or filtering diffs with compareVersions
, onCompare
.
The showDiff
command temporarily replaces the current editor content with the diff view, showing the differences between versions. It also stashes the content currently displayed in editor so that you can restore it later.
// This will display the changes that change tracking transform recorded in the editor
editor.commands.showDiff(tr)
Options
You can pass additional options to control how the diffs are displayed:
Key | Description |
---|---|
diffs | An array of diffs to visualize |
Example: Displaying specific diffs
// This will display only the diffs made by the user with the ID 'user-1'
const diffsToDisplay = tr.toDiff().filter((diff) => diff.attribution.userId === 'user-1')
editor.commands.showDiff(tr, { diffs: diffsToDisplay })
hideDiff
Use the hideDiff
command to hide the diff and restore the previous content.
// This will hide the diff view and restore the previous content
editor.commands.hideDiff()
Working with NodeView (Advanced)
When using custom node views, the default diff mapping may not work as expected. You can customize the mapping and render the diffs directly within the custom node view.
Use the extractAttributeChanges
helper to extract attribute changes in nodes. This allows you to access the previous and current attributes of a node, making it possible to highlight attribute changes within your custom node views.
Note
When mapping the diffs into decorations yourself, you need to pass the diff
as the
decoration's spec
. This is required for extractAttributeChanges
to work correctly.
Example: Customizing a heading node view to display changes
import { extractAttributeChanges } from '@tiptap-pro/extension-snapshot-compare'
const Heading = BaseHeading.extend({
addNodeView() {
return ReactNodeViewRenderer(({ node, decorations }) => {
const { before, after, isDiffing } = extractAttributeChanges(decorations)
return (
<NodeViewWrapper style={{ position: 'relative' }}>
{isDiffing && before.level !== after.level && (
<span
style={{
position: 'absolute',
right: '100%',
fontSize: '14px',
color: '#999',
backgroundColor: '#f0f0f070',
}}
// Display the difference in level attribute
>
#<s>{before.level}</s>
{after.level}
</span>
)}
<NodeViewContent as={`h${node.attrs.level ?? 1}`} />
</NodeViewWrapper>
)
})
},
})
Technical details
Diff
A Diff
is an object that represents a change made to the document. It contains the following properties:
Property | Type | Description |
---|---|---|
type | 'inline-insert' | 'inline-delete' | 'block-insert' | 'block-delete' | 'inline-update' | 'block-update' | The type of change made |
from | number | The start position of the change |
to | number | The end position of the change |
content | Fragment | The content that was added or removed |
attribution | Attribution | Metadata about the change, such as the user who made the change |
attributes? | Record<string, any> | The attributes after the change; only available for 'inline-update' and 'block-update' diffs |
previousAttributes? | Record<string, any> | The attributes before the change; only available for 'inline-update' and 'block-update' diffs |
DiffSet
A DiffSet
is an array of Diff
objects, each corresponding to a specific change, like insertion, deletion, or update. You can iterate over the array to inspect individual changes or apply custom logic based on the diff types.
const diffsToDisplay = diffSet.filter((diff) => diff.attribution.userId === 'user-1')
// Show the filtered diffs in the editor
editor.commands.showDiff(tr, { diffs: diffsToDisplay })
Attribution
The Attribution
object contains metadata about a change. It includes the following properties:
Property | Type | Description |
---|---|---|
type | 'added' | 'removed' | Indicates the type of change made |
userId | string | undefined | The ID of the user who made the change |
id | Y.ID | undefined | The Y.js client ID of the user who made the change |
You can extend the Attribution
interface to include additional properties:
declare module '@tiptap-pro/extension-snapshot-compare' {
interface Attribution {
userName: string
}
}
ChangeTrackingTransform
The ChangeTrackingTransform
is a class that records changes made to the document (based on ProseMirror's Transform
).
It represents a transform whose steps describe all of the changes made to go from one version of the document to another. It has the following properties:
Property | Type | Description |
---|---|---|
steps | ChangeTrackingStep[] | An array of steps that represent the changes made to the document |
doc | Node | The document after the changes have been applied |
before | Node | The document before the changes have been applied |
ChangeTrackingStep
The ChangeTrackingStep
is a class that represents a single change made to the document, based on ProseMirror's ReplaceStep
class. It has the following property:
Property | Type | Description |
---|---|---|
attribution | Attribution | Metadata about the change, such as the user who made it |
Types
Here is the full TypeScript definition for the SnapshotCompare
extension:
declare module '@tiptap/core' {
interface Commands<ReturnType> {
snapshotCompare: {
/**
* Given a change tracking transform, show the diff within the editor
*/
showDiff: (tr: ChangeTrackingTransform, options?: { diffs?: DiffSet }) => ReturnType
/**
* Hide the diff and restore the previous content
*/
hideDiff: () => ReturnType
/**
* Diffs two versions and renders the diff into the editor
*/
compareVersions: <
T extends Pick<Attribution, 'color'> & Record<string, any> = Pick<Attribution, 'color'> &
Record<string, any>,
>(options: {
/**
* The version to start the diff from
*/
fromVersion: number
/**
* The version to end the diff at
* If not provided, the latest snapshot will be used
*/
toVersion?: number
/**
* Allows adding contextual data to each users changes
*/
hydrateUserData?: (context: {
/**
* The type of event
*/
type: 'added' | 'removed'
/**
* The userId that manipulated this content
*/
userId: string | undefined
/**
* The yjs identifier of the content
*/
id?: y.ID
}) => T
/**
* If provided, allows customizing the behavior of rendering the diffs to the editor.
* @note The default behavior would be to just display the diff immediately.
*/
onCompare?: (
context:
| {
error?: undefined
/**
* The editor instance
*/
editor: Editor
/**
* All of the changes as a transform with attribution metadata
*/
tr: ChangeTrackingTransform<Attribution & T>
/**
* The changes represented as an array of diffs
*/
diffSet: DiffSet<Attribution & T>
}
| {
error: Error
},
) => void
/**
* Verbosely log the diffing process to help track down where things went wrong
*/
enableDebugging?: boolean
}) => ReturnType
}
}
}
export type SnapshotCompareOptions = {
/**
* The tiptap provider instance. This is required for the extension to compute the diffs, but not to display them.
* It is also possible to pass a TiptapCollabProvider instance.
*/
provider: TiptapCollabProvider | null
/**
* This allows you to control mapping of a diff into a decoration to display the content of that diff
*/
mapDiffToDecorations?: (options: {
/**
* The diff to map to a decoration
*/
diff: Diff
/**
* The editor instance
*/
editor: Editor
/**
* The change tracking transform
*/
tr: ChangeTrackingTransform
/**
* The default implementation of how to map a diff to a decoration
*/
defaultMapDiffToDecorations: typeof defaultMapDiffToDecorations
}) => ReturnType<typeof defaultMapDiffToDecorations>
}
export type SnapshotCompareStorageInactive = {
/**
* Whether the diff view is currently active
*/
isPreviewing: false
/**
* The content before the diff view was applied
*/
previousContent: null
/**
* The change tracking transform that was applied
* It is currently empty because the diff view is not active
*/
diffs: DiffSet
/**
* The change tracking transform that was applied
*/
tr: null
}
export type SnapshotCompareStorageActive = {
/**
* Whether the diff view is currently active
*/
isPreviewing: true
/**
* The content before the diff view was applied
*/
previousContent: JSONContent
/**
* The change tracking transform that was applied
*/
diffs: DiffSet
/**
* The change tracking transform that was applied
*/
tr: ChangeTrackingTransform
}
export type SnapshotCompareStorage = SnapshotCompareStorageInactive | SnapshotCompareStorageActive