lil-gui

Undo / Redo

This example makes an undo system by combining gui.onFinishChange with load() and save().

This page:
import GUI from '../../dist/lil-gui.esm.js';

const gui = new GUI( { captureKeys: false } );

const params = {
	boolean: true,
	string: 'lil-gui',
	number: 0,
	color: '#aa00ff',
};

gui.add( params, 'boolean' );
gui.add( params, 'string' );
gui.add( params, 'number' );
gui.addColor( params, 'color' );

const undoFolder = gui.addFolder();
const undoBtn = undoFolder.add( { undo }, 'undo' ).name( 'Undo ⌘Z' );
const redoBtn = undoFolder.add( { redo }, 'redo' ).name( 'Redo ⌘⇧Z' );

// Undo / Redo

const undoStack = [];
const redoStack = [];

function storeUndo() {
	undoStack.push( gui.save() );
	redoStack.length = 0;
	updateUndoCount();
}

// There will always be at least 1 undo state in the stack: the initial state.
storeUndo();

// Store an undo step every time a change is finished. We use `loading` to 
// avoid an infinite loop when loading an undo / redo state.
let loading = false;
gui.onFinishChange( () => {
	if ( !loading ) storeUndo();
} );

function undo() {

	if ( undoStack.length > 1 ) {

		// Move the current state from the undo stack to the redo stack.
		redoStack.push( undoStack.pop() );

		// Load the state on top of the undo stack.
		loading = true;
		gui.load( undoStack[ undoStack.length - 1 ] );
		loading = false;

	}

	updateUndoCount();

}

function redo() {

	if ( redoStack.length > 0 ) {

		// Remove the top-most state from the redo stack and put it back
		// on the undo stack.
		const state = redoStack.pop();
		undoStack.push( state );

		// Load the state on top of the undo stack.
		loading = true;
		gui.load( state );
		loading = false;

	}

	updateUndoCount();

}

function updateUndoCount() {

	// The undo stack is "empty" when there's one item left: the initial state.
	const numUndos = undoStack.length - 1;
	const numRedos = redoStack.length;

	// Use a folder title as a readout.
	undoFolder.title( `Undo (${numUndos}) • Redo (${numRedos})` );

	undoBtn.disable( numUndos < 1 );
	redoBtn.disable( numRedos < 1 );

}

// Unless `captureKeys` is false, this won't be called while a GUI input has focus.
window.addEventListener( 'keydown', e => {

	if ( e.key === 'z' && ( e.metaKey || e.ctrlKey ) ) {

		// Prevent native keystroke-based undo
		e.preventDefault();

		// Blur element if it's an <input>
		if ( e.target instanceof HTMLInputElement ) {
			e.target.blur();
		}

		if ( e.shiftKey ) redo();
		else undo();

	}

} );