Skip to content

Undo-Redo integration

vwRtx tracks and performs its own undo-redos. We can tap into this feature easily from our app to make a connected undo-redo exosystem.

Listening for changes

To listen for undoable changes, set up a listener for event changed or use the callbacks.changed function. Both are invoked each time a vwRtx instance experiences an undoable change. The single argument that the callback will receive is the rtx instance. We do not need any more information to power the undo-redo process.

This is how to wire it up

JS
rtx.on('changed', function(rtxInstance){

    // get a serialisation or state value
    const value_to_store = rtxInstance.config

    // or note the rtx info in the app undo list
    storeUndo(rtxInstance.shape.id) 
}) 


// the callback func - called for every change to the value of the rtx content
function anyChange(rtxInstance){

    // get a serialisation or state value
    const value_to_store = rtxInstance.config

    // or note the rtx info in the app undo list
    storeUndo(rtxInstance.shape.id)
}
JS
let config = {
    ...
    callbacks: {
        changed: anyChange,
    }
    ...
}
...

// the callback func - called for every change to the value of the rtx content
function anyChange(rtxInstance){

    // get a serialisation or state value
    const value_to_store = rtxInstance.config

    // or note the rtx info in the app undo list
    storeUndo(rtxInstance.shape.id)
}

Having the vwRtx instance continually parsing the current value, re-measuring the chars and drawing to the canvas, has a high performance cost. To reduce this issue, the config object returned by rtx.config includes a stamp property that is used to recognise when the same, unchanged value, is handed back to it. In this case the overhead is avoided except when the stamp is different or absent.

Optimum integrated undo-redo

The rtx shapes in your canvas will be scattered among the other shapes from which your canvas UI is composed. If you provide an undo-redo process in your UI then it is likely that you will want to handle the changes that happen within each rtx instance - for example the text that the user enters or the changes they make to font colors and faces, etc. There are two approaches to this:

  • Option #1. Get and store the config before and after every change to the rtx. In this approach, you apply the before config to the rtx to affect an undo, and the after config to apply the redo. The issue with this approach is that you may end up with a significant data volume in your undo-redo list. This is because, for example, each letter the user types is classed as a change by rtx - so these can add up quickly. This approach is also potentially harder work for the browser because when we supply a new config to an rtx it runs a text reflow which includes measursing all the text, which can affect performance.

  • Option #2. User the rtx internal undo-redo mechanism. The benefit of this approach is a minimal amount of effort all round. Since the rtx runs its own internal optimised undo-redo process, we have none of the overhead of the config reset approach described in option #1. Using the rtx undo-redo process is the best performance you will get.

Explaining option #2 further, rtx handles it own internal undo-redo process. Therefore, to make the rtx instances on your canvas fit right in with the app's undo-redo process, it is necessary to store in you app's change list only an indicator of which rtx instance experienced the change. Then, as the user employs your undo-redo process, when an rtx change entry is found just send an undo / redo API message into the appropriate rtx instance.

How the app handles undo-redo in your app is up to you - rtx has no opinion. Here's a simple example where the change list is an array of JSON objects which store the id of the shape that was changed, with a before and after note of the details. The concept in this simple example is that to undo a change we play the before data, and to redo the change we apply the after version.

Example

Let us take the case of a canvas with a rect, a circle, and an rtx, and that the user makes the following sequence of changes:

  • #1: User drags rect to (x: 20, y: 30)
  • #2: User types into an rtx 'Hellow world'
  • #3: User transforms circle making it 5 times larger.

The data in the undo list could look something like this:

js
const changeList = [
    {
        shapeId: 'rect123',
        before: {x: 10, y: 10},
        after: {x: 20, y: 30}
    },
    {
        shapeId: 'rtx142',   <-- Note this is the Konva shape id of the rtx instance
        before: 'undo',      <-- Note you don't event need to store these as you can infer them from the undo/redo button the user pressed.
        after: 'redo'
    },
    {
        shapeId: 'circle901',
        before: {scale: 1},
        after: {scale: 5}
    }
]

For the rect position change we store the before and after positions because we have to handle the undo and redo in our app's code. Same for the circle's scale.

But for the rtx all we know is that some change happened. We don't have to record the text that was typed, or whether it line-wrapped, or what the font name / size / color / underline / italic / bold values were. The rtx keeps all of that information.

All we know is that some change happend in the rtx instance.

To make the undo or redo happen remember that the rtx handles its own internal change list. This means that all the app must do for integrating with rtx internal changes is to know that a change happened, and store that in that main change list. As the changes are undone, the app simply has to fork at the rtx change and send the change api message to that rtx instance with the value 'undo'. This will cause the rtx to perform its own internal undo. The same is true for redoing a change.

The change API message looks like this:

js
/**
 * The rtx instance recorded an undoable event. Notify the app undo list.
 * This is actually the _callbackChangeListener_ that we passed into the 
 * setup config for the rtx instance. 
 */
function myRtxChangeListener(rtxInstance)){

   // implementation of the undo store is up to you. Note that 
   // we store the shapeId as Konva provides a fast way to get 
   // to the shape from its id and the rtx is an attr of the 
   // shape.
   storeUndo(rtxInstance.shape.id) 

}

// And the process to apply a change from the undo store in your app...

/**
 * Apply undo / redo to an rtx shape. 
 * parameters are
 *  shapeId: the Konva shape id
 *  direction: the change direction. One of undo|redo.
 */
function applyRtxChange (shapeId, direction){

const shape = stage.findOne('#' + shapeId),  // fast way to get the shape form an id.
      rtx = shape.getAttr('rtx')              // get the rtx instance associated with the shape.

// send that API message. Direction will be one of undo or redo.
rtx.send({
    name: "change",
    value: direction
  })

}

A Vanquished Wombat creation.