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.
The Sample Project
Don't forget about the sample project over on GitHub! It contains the final code for this entire series.
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:
- Create a
Copier
class that defines this functionality. - Wire that
Copier
class to theDrawingEditor
.
To implement hotkey shortcuts, we needed to:
- Create the hotkey initialization method.
- 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!
Happy Drawing!