We've now completed our implementation for drawing simple objects in FabricJS, using our TypeScript models. Let's now work on how to remove objects from the canvas.
The Sample Project
As always, there's a sample project over on GitHub that contains the completed code for this series. Check it out!
Implementing the DeleteComponent
We're going to implement this delete functionality a bit backwards from the way we've implement other functionality: we're going to write the component first, then integrate it with the drawing editor.
Our component will exist in the toolbar, but needs to only be active when an object is selected; otherwise it should be disabled. Further, this isn't going to be the only component that changes the canvas: later in this series we're going to implement undo/redo functionality. So, we need a "base" class for all components that will do non-drawing functionality.
Chris termed these components ControlComponents, and implemented the following abstract class:
abstract class ControlComponent {
target: string; //Selector for the component's render location
cssClass: string; //CSS classes for icons
hoverText: string; //Tooltip text
canvassDrawer: DrawingEditor;
handlers: { [key: string]: () => void };
constructor(selector: string, classNames: string, altText: string, parent: DrawingEditor, handlers: { [key: string]: () => void }) {
this.target = selector;
this.cssClass = classNames;
this.hoverText = altText;
this.canvassDrawer = parent;
this.render();
this.handlers = handlers;
this.attachEvents();
}
abstract render();
attachEvents() {
if (this.handlers['click'] != null) {
$(this.target).click(this, () => {
this.handlers['click']();
});
}
if (this.handlers['change'] != null) {
$(this.target).change(this, () => {
this.handlers['change']();
});
}
}
}
All of our non-drawing toolbar items will inherit from this abstract class.
Speaking of which, we now need such a class for the delete component. Here's the annotated code:
class DeleteComponent extends ControlComponent {
constructor(target: string, parent: DrawingEditor) {
super(
target,
"fa fa-trash-o", //CSS class for icons
"Delete Selected Item", //Tooltip text
parent,
{
//The component invokes a method
//on DrawingEditor to delete selected objects.
'click': () => { parent.deleteSelected(); }
});
}
//Render a disabled button with a trash can icon
render() {
const html = `<button id="${this.target.replace('#', '')}" title="${this.hoverText}" disabled class="btn btn-danger">
<i class="${this.cssClass}"></i>
</button>`;
$(this.target).replaceWith(html);
}
//Enable the button
//Will be called when a canvas object is selected
enable() {
var ele = document.getElementById(this.target.replace('#', ''));
Object.assign(ele, {
disabled: false
});
}
//Disable the button
//Will be called when no canvas objects are selected
disable() {
var ele = document.getElementById(this.target.replace('#', ''));
Object.assign(ele, {
disabled: true
});
}
}
With our component written, it's time to implement a few changes on the main DrawingEditor
class.
Modifying the DrawingEditor
Our main DrawingEditor
class now needs to react when canvas elements are selected, so it can enable or disable the delete button.
Here's the scenarios we want to react to:
- First, if an item is selected in the canvas, enable the Delete button.
- If no items are selected in the canvas, disable the Delete button.
- If the user clicks the Delete key, delete the selected objects.
We can implement the first two scenarios by modifying the DrawingEditor
class to enable/disable the delete button and implement the deleteSelected()
method needed by our DeleteComponent
.
class DrawingEditor {
//...Properties and Constructor
private initializeCanvasEvents() {
//...Other events
this.canvas.on('object:selected', (o) => {
this.cursorMode = CursorMode.Select;
//sets currently selected object
this.object = o.target;
//If the delete component exists, enable it
if (this.components['delete'] !== undefined) {
(this.components['delete'][0] as DeleteComponent).enable();
}
});
this.canvas.on('selection:cleared', (o) => {
//If the delete component exists, disable it
if (this.components['delete'] !== undefined) {
(this.components['delete'][0] as DeleteComponent).disable();
}
this.cursorMode = CursorMode.Draw;
});
}
//...Other methods
addComponent(target: string, component: string) {
switch (component) {
case 'line':
this.components[component]
= [new LineDisplayComponent(target, this)];
break;
case 'rect':
this.components[component]
= [new RectangleDisplayComponent(target, this)];
break;
case 'oval':
this.components[component]
= [new OvalDisplayComponent(target, this)];
break;
case 'tria':
this.components[component]
= [new TriangleDisplayComponent(target, this)];
break;
case 'text':
this.components[component]
= [new TextDisplayComponent(target, this)];
break;
case 'polyline':
this.components[component]
= [new PolylineDisplayComponent(target, this)];
break;
//New component
case 'delete':
this.components[component]
= [new DeleteComponent(target, this)];
break;
}
}
//...Other methods
deleteSelected(): void {
//Get currently-selected object
const obj = this.canvas.getActiveObject();
//Delete currently-selected object
this.canvas.remove(this.canvas.getActiveObject());
this.canvas.renderAll(); //Re-render the drawing in Fabric
}
Modifying the Markup and Script
There's something little bit strange about FabricJS's Canvas object: it doesn't provide events for keydown or keypress, so we have to wire those events up against the page, and then call the appropriate methods in the DrawingEditor class.
Here's the markup changes and script changes we need to make in our Razor Page. Note that the delete button will sit by itself on the leftmost side of the toolbar:
<div class="row bg-secondary text-light">
<div class="btn-toolbar">
<div class="btn-group mr-2" role="group">
<!-- New Component -->
<div id="deleteComponent"></div>
</div>
</div>
<div class="col-sm-11">
<div class="row col-12">
<div class="row drawingToolbar">
<label class="col-form-label controlLabel">Tools:</label>
<div class="d-inline-block">
<div class="btn-group btn-group-toggle" role="group" data-toggle="buttons" aria-label="Drawing Components">
<div id="polylineDisplayComponent"></div>
<div id="lineDisplayComponent"></div>
<div id="rectangleDisplayComponent"></div>
<div id="ovalDisplayComponent"></div>
<div id="triangleDisplayComponent"></div>
<div id="textDisplayComponent"></div>
</div>
</div>
</div>
</div>
</div>
</div>
Note also that we need to implement a new listener event for "keydown" in the script below:
@section Scripts{
//...Get scripts
<script>
var editor; //We now need to access the editor outside of the
//initialization method
$(function () {
//...Instantiate editor
//Create a list of available display components
const components = [
{ id: '#lineDisplayComponent', type: 'line' },
{ id: '#rectangleDisplayComponent', type: 'rect' },
{ id: '#ovalDisplayComponent', type: 'oval' },
{ id: '#triangleDisplayComponent', type: 'tria' },
{ id: '#textDisplayComponent', type: 'text' },
{ id: '#polylineDisplayComponent', type: 'polyline' },
//New component
{ id: '#deleteComponent', type: 'delete'}
];
//Add the components to the DrawingEditor, which will render them.
editor.addComponents(components);
$('#lineDisplayComponent').click();
//New event, which listens for the delete key
$("html").on('keydown', function (event) {
const key = event.key;
if (key == "Delete") {
editor.deleteSelected();
}
});
});
</script>
}
GIF Time!
Once the drawer, display component, markup, and script are updated, we can now delete objects on our canvas! Here's an example GIF:
Ta-da! We've completed the delete functionality for our drawing canvas!
Summary
To implement deletion of the selected object in our drawing canvas, we needed to:
- Create a new abstract base component for a toolbar control.
- Implement the abstract base component to create a Delete component.
- Wire up the delete component to the
DrawingEditor
. - Listen for the Delete key, and delete selected objects.
Don't forget to check out the sample project over on GitHub!
In the next part of this series, we will implement an undo/redo function, in which we keep track of the items being added to the canvas and can undo or redo them at any time!
Happy Drawing!