This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/web/annotations/create-edit-and-remove/undo-redo.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Undo and redo PDF annotations in JavaScript | Nutrient SDK

Nutrient Web SDK supports undo and redo for creating, updating, and deleting annotations in both operational modes. Users can undo and redo their changes with the built-in undo and redo toolbar items. They can also use the standard shortcut key combinations Control/Command-Z (undo) and Control/Command-Shift-Z (redo).

These buttons are hidden by default. Enable them in the configuration object you pass to NutrientViewer.load(), or with the instance.setToolbarItems() API method.

Even if the buttons are enabled, undo and redo don’t work until you enable the History API. This API holds the methods for controlling undo and redo. Enable it by setting NutrientViewer.Configuration#enableHistory or by calling the instance.history.enable() API method. The counterpart method is instance.history.disable().

This example enables the History API and the default undo and redo toolbar buttons:

NutrientViewer.load({
enableHistory: true,
toolbarItems: defaultConfiguration.toolbarItems.reduce(
(acc, item) => {
// Position the undo and redo buttons after the last annotation button, polyline.
if (item.type === "polyline") {
return acc.concat([item, { type: "undo" }, { type: "redo" }]);
}
return acc.concat([item]);
},
[]
)
});

Controlling undo and redo using the API

Besides the API methods and configuration settings mentioned above, the History API includes the following methods.

  • instance.history.undo() rolls back the last annotation change — either a creation, an update, or a deletion — performed via the UI or the API. It returns a Promise instance that resolves to true if there is an operation to undo. Otherwise, it returns false, which happens when no operations are left to undo.

This example uses the API to create an annotation and then undo the creation:

await instance.create(
new NutrientViewer.Annotations.RectangleAnnotation({
pageIndex: 0,
boundingBox: new NutrientViewer.Geometry.Rect({
left: 200,
top: 150,
width: 250,
height: 75
})
})
);
console.log("Annotation created!");
console.log(
"Number of annotations:",
(await instance.getAnnotations(0)).size
); // 1
await instance.history.undo();
console.log("Annotation creation undone: annotation deleted!");
console.log(
"Number of annotations:",
(await instance.getAnnotations(0)).size
); // 0
  • instance.history.redo() repeats the last annotation change rolled back by an undo operation, performed either via the UI or the API. It returns a Promise instance that resolves to true if there is an operation to redo. Otherwise, it returns false, which happens when no operations are left to redo.

This example uses the API to create an annotation, delete it, undo the deletion, and redo it again:

await instance.create(
new NutrientViewer.Annotations.RectangleAnnotation({
pageIndex: 0,
boundingBox: new NutrientViewer.Geometry.Rect({
left: 200,
top: 150,
width: 250,
height: 75
})
})
);
console.log("Annotation created!");
await instance.delete();
console.log("Annotation deleted!");
await instance.history.undo();
console.log("Annotation deletion undone: annotation restored!");
await instance.history.redo();
console.log("Annotation deletion redone: annotation deleted!");

Redo works only after an undo operation.

  • instance.history.canUndo() returns true if there are operations that can be undone. Otherwise, it returns false:

    if (instance.history.canUndo()) {
    instance.history.undo();
    }
  • instance.history.canRedo() returns true if there are operations that can be redone. Otherwise, it returns false:

    if (instance.history.canRedo()) {
    instance.history.redo();
    }
  • instance.history.clear() resets the list of undoable and redoable operations:

    console.log(instance.history.canUndo()); // `false`
    console.log(instance.history.canRedo()); // `false`
    await instance.create(
    new NutrientViewer.Annotations.RectangleAnnotation({
    pageIndex: 0,
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 200,
    top: 150,
    width: 250,
    height: 75
    })
    })
    );
    console.log("Annotation created!");
    await instance.delete();
    console.log("Annotation deleted!");
    console.log(instance.history.canUndo()); // `true`
    console.log(instance.history.canRedo()); // `false`
    await instance.history.undo();
    console.log(instance.history.canUndo()); // `true`
    console.log(instance.history.canRedo()); // `true`
    instance.history.clear();
    console.log(instance.history.canUndo()); // `false`
    console.log(instance.history.canRedo()); // `false`

Any creation, update, or deletion made via the UI or the API clears the redoable operations list, unless it results from undoing or redoing.

Tracking history events

You can track undo and redo operations by listening to the following events.

  • history.undo is dispatched after performing an undo operation, either with the API or with the UI:

    instance.addEventListener("history.undo", function (undoEvent) {
    const {
    type, // "undo"
    before: previousAnnotation, // Previous state of the annotation, or `null` if it's being restored.
    after: annotation // Resulting state of the annotation, or `null` if it's being deleted.
    } = undoEvent;
    });
  • history.redo is dispatched after performing a redo operation, either with the API or with the UI:

    instance.addEventListener("history.redo", function (redoEvent) {
    const {
    type, // "redo"
    before: previousAnnotation, // Previous state of the annotation, or `null` if it's being restored.
    after: annotation // Resulting state of the annotation, or `null` if it's being deleted.
    } = redoEvent;
    });
  • history.change is dispatched after performing an undo or a redo operation, either with the API or with the UI:

    instance.addEventListener("history.change", function (undoRedoEvent) {
    const {
    type, // "undo" or "redo"
    before: previousAnnotation, // Previous state of the annotation, or `null` if it's being restored.
    after: annotation // Resulting state of the annotation, or `null` if it's being deleted.
    } = undoRedoEvent;
    });
  • "history.clear" is dispatched after the history is cleared by a call to instance.history.clear:

    instance.addEventListener("history.clear", function () {
    console.log("History cleared.");
    });

You can subscribe to or unsubscribe from these events at any moment, even when the History API is disabled.

Undoing form field value changes

The History API doesn’t record form field value changes. In standalone mode, or with Document Engine without Instant, keep your own stack of form field value snapshots instead. This approach doesn’t suit Instant, because value changes from other Instant clients also fire formFieldValues.update. Those changes land on the stack, so an undo overwrites the other client’s edit. This example stores a snapshot on each formFieldValues.update event and restores the fields that changed with instance.setFormFieldValues():

// Make sure every form field is loaded before taking the first snapshot.
await instance.getFormFields();
// Serialize the current values so two states compare as strings.
const snapshot = () => JSON.stringify(instance.getFormFieldValues());
const undoStack = [];
let current = snapshot();
let restoring = false;
instance.addEventListener("formFieldValues.update", () => {
// Ignore updates while an undo is in progress.
if (restoring) return;
const next = snapshot();
if (next === current) return;
// Save the state before this change. Then move the baseline forward.
undoStack.push(current);
current = next;
});
// Reverts the last form field value change. Resolves to `false` when
// there's nothing left to undo, an earlier undo is still running, or
// `setFormFieldValues()` rejects.
async function undoFormFieldValue() {
// Skip the call while an earlier undo is still running — for example,
// after a double click.
if (restoring) return false;
// Restore only the fields that differ from now and still exist, so a
// field removed after a snapshot doesn't break the older entries.
const values = instance.getFormFieldValues();
const changes = {};
while (Object.keys(changes).length === 0) {
const previous = undoStack.pop();
if (!previous) return false;
for (const [name, value] of Object.entries(JSON.parse(previous))) {
if (
Object.hasOwn(values, name) &&
JSON.stringify(value) !== JSON.stringify(values[name])
) {
changes[name] = value;
}
}
}
restoring = true;
try {
await instance.setFormFieldValues(changes);
return true;
} catch (error) {
console.error(error);
return false;
} finally {
// Take the values as the SDK now reports them as the new baseline.
// They aren't always identical to the snapshot (an empty radio group
// reports "Off").
current = snapshot();
restoring = false;
}
}

Call undoFormFieldValue() from your own button — for example, a custom toolbar item. The built-in undo toolbar item and keyboard shortcut don’t call it, and this example doesn’t cover redo.

A text field commits its value when it loses focus. Clicking a toolbar item doesn’t move focus, so an edit still in progress isn’t on the stack yet. The undo then reverts the change that precedes the in-progress edit. If that change was to the same field, the field shows the restored value and the typed text is lost. Otherwise, the typed text stays until the field commits.

Using undo and redo with Instant

The history feature tracks only local changes to annotations, so it can’t track changes that other Instant clients make.

When an external change to annotations affects local annotations that the History API tracks, it’ll be overridden by any undo or redo operation.

Notes

Undo and redo may affect different elements of the SDK in the following ways:

  • Appearance streams (AP streams) — Annotations that lose their AP stream after a modification get it back when the modification is undone.
  • Annotation Z order — Undo recreates deleted annotations in the foreground instead of restoring their original Z order.
  • Annotation presets — Undo and redo operations don’t affect annotation presets, so undoing or redoing never restores them to a previous or following state.
  • Comments — Currently, undo and redo work only for annotations, not for comments. Basic support exists for restoring an annotation with its former comments — for example, after its creation is undone and then redone. However, individual comments aren’t tracked. Executing undo while writing comments doesn’t affect those comments, but it does affect the last annotation operation.
  • Form field values — Undo and redo don’t revert form field value changes, whether made in the UI or with instance.setFormFieldValues(). For more information, refer to undoing form field value changes.