We're nearing the end of our Drawing with FabricJS and TypeScript series. In this part, let's add one last set of improvements by implementing cut, copy, and paste functionality as well as hotkey shortcuts for those, plus undo and redo.

Watch your fingers! Photo by Igor Miske / Unsplash

The Sample Project

Don't forget about the sample project over on GitHub! It contains the final code for this entire series.

exceptionnotfound/FabricJSDrawingWalkthrough
Contribute to exceptionnotfound/FabricJSDrawingWalkthrough development by creating an account on GitHub.
"As if I could forget about it, Matthew, when you keep reminding us every post." I hear you, readers. :)

The Copier Class

Let's start with the copier class, which will implement our cut, copy, and paste functionality. The skeleton of this class looks like this:

class Copier {
    //During cut or paste, memorizedObject is the object that 
    //will be held in memory and then pasted
    private memorizedObject: fabric.Object

    constructor(private readonly editor: DrawingEditor) { }

    copy(callBack?: Function) { }

    cut() { }

    paste() { }
}

We need to fill in each of the methods (other than the constructor, which for this class doesn't have an implementation; it just gets an instance of DrawingEditor).

Let's start with copy(). When we copy an object, we place a copy of it in memory so that it can be pasted later. In this particular case, we need to get the active object on the FabricJS canvas, clone it, and then set it to the memorizedObject property of Copier. Plus, we have an optional callback method that needs to be executed, if it is not undefined. Here's how that looks:

copy(callBack?: Function) {
    //Get active object on canvas
    const activeObject = this.editor.canvas.getActiveObject();

    //If the active object is not null
    if (activeObject !== null) {
        //Clone the object...
        activeObject.clone((object) => {
            //...and set it as the memorizedObject property
            this.memorizedObject = object;
            
            //When the object is pasted, offset the paste location
            //from the original object.
            object.set({
                left: object.left + 10,
                top: object.top + 10
            });

            //If the callback method exists, call it
            if (callBack !== undefined)
                callBack();
        });
    }
}

The callback method becomes immediately useful, because we now need to implement the cut() method. Cut is really just copy and then delete, so our cut() method will pass a function that deletes the selected object after it is cut. Here's the implementation:

cut() {
    this.copy(() => this.editor.deleteSelected());
}

The last part of this is the paste() method. In this method, we clone the memorized object, add it to the canvas, and re-render the canvas. Here's the code:

paste() {
    if (this.memorizedObject !== undefined) {
        this.memorizedObject.clone((clonedObject) => {
            this.editor.canvas.add(clonedObject);
            this.editor.canvas.setActiveObject(clonedObject);
            this.editor.stateManager.saveState();
            this.editor.canvas.renderAll();
        });
    }
}

All that's left to do is to make our main DrawingEditor class aware of the new Copier class...

class DrawingEditor {
    private copier: Copier;
    
    //...Other properties

    constructor(private readonly selector: string,
        canvasHeight: number, canvasWidth: number) {
        //...Rest of constructor
        
        this.copier = new Copier(this);
    }
}

But wait! There's currently no way the user can actually use this functionality! For cut/copy/paste, we aren't going to add display components to the toolbar. Instead, we're going to enable hotkeys (e.g. Ctrl+X, Ctrl+C, Ctrl+V).

Hotkeys

Not only will we enable hotkeys for cut, copy, and paste, we'll also enable them for undo (Ctrl+Z) and redo (Ctrl+Y). To start, we need our DrawingEditor class to be aware of the key codes for each of the letters. One of the ways we can do this is by using a very neat site called keycode.info. Here's the keycodes for X, C, V, Z, and Y:

class DrawingEditor {
    //...Other properties

    private keyCodes = {
        'C': 67,
        'V': 86,
        'X': 88,
        'Y': 89,
        'Z': 90
    }
}

TypeScript defines for us a KeyboardEvent class which represents an event where the user pressed a key on the keyboard. That event also tells us if the Ctrl key was pressed during the event. We can use this class to initialize our key code events:

class DrawingEditor {
    //...Other properties
    
    constructor(private readonly selector: string,
        canvasHeight: number, canvasWidth: number) {
        //...Rest of constructor
        this.initializeKeyCodeEvents();
    }
    
    //...Other methods
    
    private initializeKeyCodeEvents() {
        //When a key is pressed...
        window.addEventListener('keydown', (event: KeyboardEvent) => {
            //...check if the Ctrl key was also pressed...
            if (event.ctrlKey) {
                //...and do different events based on the other key pressed.
                switch (event.keyCode) {
                    case this.keyCodes['Z']:
                        this.undo();
                        break;
                    case this.keyCodes['Y']:
                        this.redo();
                        break;
                    case this.keyCodes['C']:
                        this.copier.copy();
                        break;
                    case this.keyCodes['X']:
                        this.copier.cut();
                        break;
                    case this.keyCodes['V']:
                        this.copier.paste();
                        break;
                }

                event.preventDefault();
                event.stopPropagation();
                return false;
            }
        });
    };
}

That's all we need to do for hotkeys! Now the user can cut, copy, paste, undo, and redo using the keyboard shortcuts for those events! This is difficult to put into a GIF (because you wouldn't be able to see what keyboard shortcut I was using), so there won't be one for this part of the series.

Summary

To implement cut/copy/paste, we needed to:

  1. Create a Copier class that defines this functionality.
  2. Wire that Copier class to the DrawingEditor.

To implement hotkey shortcuts, we needed to:

  1. Create the hotkey initialization method.
  2. Call that method in the DrawingEditor constructor.

Don't forget to check out the sample project over on GitHub!

In the next and final part of this series, we'll see how to submit the generated JSON of objects on our canvas to the server, so that it can be saved into a database!

Drawing with FabricJS and TypeScript Part 9: Saving and Conclusion
Let’s see how to save our drawings via an AJAX call, and wrap up this series!

Happy Drawing!