Adding global undo and redo functionality to web-pages – implement rich javascript functionality in any JSF, JSP, PHP, etc. page

Most Desktop applications sport features such as CTRL-Z and CTRL-Y that allow us to undo (and undo the undo, aka redo) changes in files, documents and spreadsheets. Browsers such as Firefox have similar features for webpages (HTML Forms), but only on the field level. If you enter values in an HTML based webform, such as the one shown here, there is undo and redo support for every individual field, going back all the way to the original value of when the page first loaded by multiple ctrl+y keypresses.

Adding global undo and redo functionality to web-pages - implement rich javascript functionality in any JSF, JSP, PHP, etc. page globalundo 

After having created a pretty complex, wannabe-rich user interface for massive data manipulation, I was wondering whether perhaps a global undo and redo option, one that goes across all fields and does not require navigation to individual fields, would be convenient. I have created a little bit of JavaScript – that is by no means finished. But the general idea is there, and I am wondering it makes sense at all. So if you have an opinion on this functionality, please share it with me.

What I have created is the following functionality:....

  • changes in the fields in the first two and the last column are recorded
  • whenever the user press CTRL+Shift+Z, the generic Undo is applied: without the user having to navigate back to any of the fields he or she made changes to, the old values are restored. Every addition CTRL+Shift+Z takes back one step further in time. Every undone field is highlighted, so it is clear to the user where changes have been reverted.
  • after one or more Undo actions, the user can press CTRL+Shift+Y to redo an action (or undo the undo).

Let’s make a few changes in the table from above:

Adding global undo and redo functionality to web-pages - implement rich javascript functionality in any JSF, JSP, PHP, etc. page globalundo2

Now we realize that from a certain point in time we have been applying erroneous changes. We cannot quite remember which fields we have changed. And some changes were correct, so reloading the page and reapplying them all is somewhat silly as well. So now we use Global Undo and press Ctrl+Shift+Z, to rewind our changes.

Adding global undo and redo functionality to web-pages - implement rich javascript functionality in any JSF, JSP, PHP, etc. page globalundo3 

Well, that won’t do so let’s revert some more changes, again with a few Ctrl+Shift+Z actions:

Adding global undo and redo functionality to web-pages - implement rich javascript functionality in any JSF, JSP, PHP, etc. page globalundo4

There we are. With Ctrl+Shift+Y we can redo some of these changes if we feel like it. We could even present the user with a popup of all changes made in the current page. And the only changes required are the registration on the fields we want to have undo-enabled of two event listeners. The most straightforward way to do this, is like this:

<tr:inputText value="#{customer.firstName}" onfocus="registerForUndo(this);" onblur="deregisterForUndo(this);" />
<tr:inputText value="#{customer.lastName}"  onfocus="registerForUndo(this);" onblur="deregisterForUndo(this);" />
<tr:inputText value="#{customer.country}"  onfocus="registerForUndo(this);" onblur="deregisterForUndo(this);" /> 

However, we can do it even simpler, by having a JavaScript function called from the onLoad for example find all visible, enabled INPUT elements in the page and attaching these event listeners to all of them.

On the table element (could also be the form or any other container element) we attach a KeyUp event listener to listen for Ctrl+Shift with Y or Z actions:

 <tr:table id="customers" emptyText="No items were found" onkeyup="keyup(this,event);"
            value="#{CustomerManager.customers}" var="customer">
            

Now all we need is to attach a JavaScript library to our page with the functions called in these event listeners. The draft code looks like this:

var currentregisterindex=0;
var undoregister = new Array();
var undomode = false;

function registerForUndo(el) {
if (!undomode &amp;&amp; el.type=="INPUT") {
index = el.id;
changeRecord = {'id':index, 'oldvalue':el.value};
undoregister.push(changeRecord);
}
}

function deregisterForUndo(el) {
if (!undomode &amp;&amp; el.type=="INPUT") {
if (el.id == undoregister[undoregister.length-1].id) {
if (undoregister[undoregister.length-1].oldvalue== el.value) {
// not changed so loose the registration
undoregister.pop();
}
else {
undoregister[undoregister.length-1].newvalue= el.value;
currentregisterindex=undoregister.length-1;
}
}
else // new element, that is already deregistered; this really should not happen
{}
}
}

function keyup(inputField, e)
{
var x='';
if (document.all){
var evnt = window.event;
x=evnt.keyCode;
}//if
else {
x=e.keyCode;
}//else
if (e.shiftKey &amp;&amp; e.ctrlKey &amp;&amp; x==90) // CTRL Z
{
if (currentregisterindex>=0) {
changeRecord=undoregister[currentregisterindex];
el = document.getElementById(changeRecord.id);
undomode=true;
el.value = changeRecord.oldvalue;
el.select();
undomode=false;
if (currentregisterindex>0)
currentregisterindex--;
}
}
if (e.shiftKey &amp;&amp; e.ctrlKey &amp;&amp; x==89) // CTRL Y
{
if (currentregisterindex &lt; undoregister.length-1) {
changeRecord=undoregister[++currentregisterindex];
el = document.getElementById(changeRecord.id);
undomode=true;
el.value = changeRecord.newvalue;
el.select();
undomode=false;
}
}
}