Undo and Redo

PSPDFKit for Web supports undo and redo for creating, updating, and deleting annotations in both Standalone and Server-backed deployments. Users can undo and redo their changes using either the corresponding built-in toolbar items — undo and redo — or the standard shortcut key combinations Ctrl/Cmd + Z (undo) and Ctrl/Cmd + Shift + Z (redo).

These buttons are hidden by default, and they need to be enabled in the configuration object passed to PSPDFKit.load(), or by using the instance.setToolbarItems() API method.

Even if the buttons are enabled, undo and redo won’t work unless the History API, which holds the new API methods for controlling undo and redo, is enabled either by setting PSPDFKit.Configuration#enableHistory or via the corresponding instance.history.enable() API method, with its counterpart, ìnstance.history.disable().

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

PSPDFKit.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]);
	}, []),
});
Undo and redo toolbar buttons.

Controlling Undo and Redo Using the API

Apart from the new API methods and configuration settings mentioned above, the new History API includes the following methods to perform different tasks related to this feature.

  • 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 an operation can be undone. Otherwise, it returns false — this can happen if there are no operations left to be undone.

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

    await instance.create(
    	new PSPDFKit.Annotations.RectangleAnnotation({
    		pageIndex: 0,
    		boundingBox: new PSPDFKit.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 an operation can be redone. Otherwise, it returns false — this can happen if there are no operations left to be redone.

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

    await instance.create(
    	new PSPDFKit.Annotations.RectangleAnnotation({
    		pageIndex: 0,
    		boundingBox: new PSPDFKit.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!');
    

    Note how a previous undo operation is needed to be able to execute redo.

  • 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 PSPDFKit.Annotations.RectangleAnnotation({
    		pageIndex: 0,
    		boundingBox: new PSPDFKit.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 performed either via the UI or the API that isn’t a result of undoing or redoing will clear the redoable operations list.

Tracking History Events

Undo and redo operations can be tracked by listening to the following new 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 or unsubscribe to these events at any moment, even when the History API is disabled.

Using Undo and Redo with Instant

The History feature only tracks local changes to annotations, and therefore, it’s not possible to track changes performed by other Instant clients.

When an external change to annotations affects local annotations that are being tracked using the History API, it’ll be overridden by any undo and redo operation.

Notes

Here’s a description of how the behavior of undo and redo may affect different elements of the SDK:

  • AP Streams — When AP stream rendering is enabled in PSPDFKit.Configuration#isAPStreamRendered(), annotations that lose their AP stream after a modification will have it restored if that modification is undone.

  • Annotation Z Order — The original annotation Z order won’t be restored for annotations that are deleted and later restored with undo. Those annotations will be recreated in the foreground.

  • Annotation Presets — Annotation presets aren’t affected by undo and redo operations, and they won’t be restored to a previous or following state as a result of undo or redo.

  • Comments — Currently, undo and redo only works for annotations and not for comments. However, basic support is in place to allow restoring an annotation with its former comments in case its creation is undone and then redone, for example. However, individual comments aren’t tracked. This means that executing undo while writing comments won’t affect those comments, but it will affect the last annotation operation.