简体   繁体   中英

object:removed Event Not Fired - Fabric JS

I am facing a weird issue with fabric events. Take a look at this snippet

canvas.on('object:added', function(e) { 
        console.log(e.target.type);
        console.log("Something was Added");

    });

 canvas.on('object:removed', function(e) { 
            console.log(e.target.type);
            console.log("Something was removed");

        });

Given this code base I am experimenting on an undo / redo functionality. Given both undo & redo can add , modify or remove an object I would like to be notified if something was added or removed in the canvas (I am not much worried about object modified at this stage).

But strange enough no matter if an object is added or removed from the canvas using the undo / redo functionality. I always get the output - Something was Added

Undo / Redo Routines:

// Undo Redo Clear
        canvas.counter = 0;
        var newleft = 0;
        canvas.selection = false;
        var state = [];
        var mods = 0;
        canvas.on(
            'object:modified', function () {
                updateModifications(true);
            },
            'object:added', function () {
                updateModifications(true);
            },
            'object:removed' , function(e){
                updateModifications(true);
                console.log('test me');
            });

        function updateModifications(savehistory) {
            if (savehistory === true) {
                myjson = JSON.stringify(canvas);
                state.push(myjson);
                console.log(myjson);
            }
        }

        undo = function undo() {
            if (mods < state.length) {
                canvas.clear().renderAll();
                canvas.loadFromJSON(state[state.length - 1 - mods - 1]);
                canvas.renderAll();

                mods += 1;
                //check_team();

                //compare_states(state[state.length - 1 - mods - 1] , state[state.length - 1 - mods + 1])

            }
            //make_objects_selectable();
        }

        redo = function redo() {
            if (mods > 0) {
                canvas.clear().renderAll();
                canvas.loadFromJSON(state[state.length - 1 - mods + 1]);
                canvas.renderAll();

                mods -= 1;

                //check_team(); 
            }
            //make_objects_selectable();
        }

        clearcan = function clearcan() {
            canvas.clear().renderAll();
            newleft = 0;
        }

Fabric version:"1.6.0-rc.1"

Update: The Event is working fine in case of a normal delete action. Hence I added the Undo and Redo Routines.

Regards

Both your undo and redo functions do basically the same thing, erase canvas, load a new state and render it. When you clear the canvas, there is no object:removed event, but another event is fired, called canvas:cleared . That is why you never see your object:removed event fired when doing undo/redo. On the other hand, you do see object:added fired on both undo and redo, because I am guessing that canvas.renderAll adds every object on the current state into the canvas (since it was previously removed with canvas.clear()).

EDIT

A better solution is to store every action that happens on canvas, like add, modify or remove, and have each action associated with some object data. For example, you could have an object_added action associated with a serialization of the added object, or an object_removed action associated with a serialization of the removed object. For object_modified you would need two associated object serializations, one prior modification and one after modification. In case of a canvas_cleared action, you would have to store the whole canvas state as associative data.

A simple stack structure can work great for the purpose of action storage.

 function SimpleStackException(msg) { this.message = msg; this.name = 'SimpleStackException'; } function SimpleStack() { var MAX_ENTRIES = 2048; var self = this; self.sp = -1; // stack pointer self.entries = []; // stack heap self.push = function(newEntry) { if (self.sp > MAX_ENTRIES - 1) { throw new SimpleStackException('Can not push on a full stack.'); } self.sp++; self.entries[self.sp] = newEntry; // make sure to clear the "future" stack after a push occurs self.entries.splice(self.sp + 1, self.entries.length); }; self.pop = function() { if (self.sp < 0) { throw new SimpleStackException('Can not pop from an empty stack.'); } var entry = self.entries[self.sp]; self.sp--; return entry; }; self.reversePop = function() { self.sp++; if (!self.entries[self.sp]) { self.sp--; throw new SimpleStackException('Can not reverse pop an entry that has never been created.'); } return self.entries[self.sp]; } } 

Go ahead and create such a structure: var actionHistory = new SimpleStack();

Another feature you will need for the action-based undo/redo to work, is the ability to "reference" objects in the canvas. In fabric.js you can reference objects from canvas.getObjects() , but that is a plain js array and does not help much. I have added object IDs, in the form of UUID. Here is a function (taken somewhere in SO, dont have the link now) tha generates UUIDs

 var lut = []; for (var i = 0; i < 256; i++) { lut[i] = (i < 16 ? '0' : '') + (i).toString(16); } function generateUuid() { var d0 = Math.random() * 0xffffffff | 0; var d1 = Math.random() * 0xffffffff | 0; var d2 = Math.random() * 0xffffffff | 0; var d3 = Math.random() * 0xffffffff | 0; return lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + '-' + lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + '-' + lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + '-' + lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + '-' + lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] + lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] + lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff]; } 

In order for fabric objects to have a new uuid property you need to add it to the object prototype, and to the object serialization method as well

 fabric.Object.prototype.uuid = ""; fabric.Object.prototype.toObject = (function(toObject) { return function() { return fabric.util.object.extend(toObject.call(this), { uuid: this.uuid, }); }; })(fabric.Object.prototype.toObject); 

Finally you need a function to "reference" objects via this uuid property.

 function getFabricObjectByUuid(uuid) { var fabricObject = null; canvas.getObjects().forEach(function(object) { if (object.uuid === uuid) { fabricObject = object; } }); return fabricObject; } 

Now you need to listen for events on the canvas, and update the actionHistory accordingly:

 canvas.on('path:created', function(path) { var object = path.path; object.uuid = generateUuid(); actionHistory.push({ type: 'object_added', object: JSON.stringify(object) }); }); canvas.on('object:added', function(e) { var object = e.target; // bypass the event for path objects, as they are handled by `path:created` if (object.type === 'path') { return; } // if the object has not been given an uuid, that means it is a fresh object created by this client if (!object.uuid) { object.uuid = generateUuid(); } if (!object.bypassHistory) { actionHistory.push({ type: 'object_added', object: JSON.stringify(object) }); } }); canvas.on('object:modified', function(e) { var object = e.target; actionHistory.push({ type: 'object_modified', objectOld: JSON.stringify(latestTouchedObject), objectNew: JSON.stringify(object) }); }); canvas.on('text:changed', function(e) { var object = e.target; actionHistory.push({ type: 'text_changed', objectOld: JSON.stringify(latestTouchedObject), objectNew: JSON.stringify(object) }); }); canvas.on('object:removed', function(e) { var object = e.target; if (!object.bypassHistory) { actionHistory.push({ type: 'object_removed', object: JSON.stringify(object) }); } }); canvas.on('canvas:cleared', function(e) { if (!canvas.bypassHistory) { actionHistory.push({ type: 'canvas_cleared', canvas: JSON.stringify(canvas) }); } }); 

Check out each event handler carefully to understand the actual data that will be stored on actionHistory . Also be mindful when the uuid property is actually added to an object. There are two things to note about the above snippet.

  1. bypassHistory is a custom property of canvas objects and the canvas itself. You only want to store actions that are willingly performed by a user onto the canvas. If a user hand-draws a line, you want to save that action, and you do so by listening to path:cleared . However, in the case of a programmatically drawn line (eg. when performing a redo), you may not want to store the action. To add this custom property do as follows:

    fabric.Object.prototype.bypassHistory = false; // default value false

  2. object_modified is a special action because it needs to store two object representations: before and after modification. While the "after" version is obtained easily via event.target of the object:modified event, the "before" version has to be tracked programmatically. In my solution i have a high level latestTouchedObject variable that keeps track of the latest modified object on the canvas.

 canvas.on('mouse:down', function(options) { if (options.target) { latestTouchedObject = fabric.util.object.clone(options.target); } }); 

Now that the action storage and all listeners have been setup, it's time to implement the undo and redo functions

 function undoAction() { var action, objectCandidate; try { action = actionHistory.pop(); } catch (e) { console.log(e.message); return; } if (action.type === 'object_added') { objectCandidate = JSON.parse(action.object); var object = getFabricObjectByUuid(objectCandidate.uuid); object.bypassHistory = true; canvas.remove(object); } else if (action.type === 'object_removed') { objectCandidate = JSON.parse(action.object); fabric.util.enlivenObjects([objectCandidate], function(actualObjects) { actualObjects[0].uuid = objectCandidate.uuid; var object = actualObjects[0]; object.bypassHistory = true; canvas.add(object); object.bypassHistory = false; }); } else if (action.type === 'object_modified' || action.type === 'text_changed') { objectCandidate = JSON.parse(action.objectOld); fabric.util.enlivenObjects([objectCandidate], function(actualObjects) { actualObjects[0].uuid = objectCandidate.uuid; var object = actualObjects[0]; var existingObject = getFabricObjectByUuid(objectCandidate.uuid); if (existingObject) { existingObject.bypassRemoveEvent = true; existingObject.bypassHistory = true; canvas.remove(existingObject); } object.bypassHistory = true; canvas.add(object); object.bypassHistory = false; }); } else if (action.type === 'canvas_cleared') { var canvasPresentation = JSON.parse(action.canvas); canvas.bypassHistory = true; canvas.loadFromJSON(canvasPresentation); canvas.renderAll(); canvas.bypassHistory = false; } } function redoAction() { var action, objectCandidate; try { action = actionHistory.reversePop(); } catch (e) { console.log(e.message); return; } if (action.type === 'object_added') { objectCandidate = JSON.parse(action.object); fabric.util.enlivenObjects([objectCandidate], function(actualObjects) { actualObjects[0].uuid = objectCandidate.uuid; var object = actualObjects[0]; object.bypassHistory = true; canvas.add(object); object.bypassHistory = false; }); } else if (action.type === 'object_removed') { objectCandidate = JSON.parse(action.object); var object = getFabricObjectByUuid(objectCandidate.uuid); object.bypassHistory = true; canvas.remove(object); object.bypassHistory = false; } else if (action.type === 'object_modified' || action.type === 'text_changed') { objectCandidate = JSON.parse(action.objectNew); fabric.util.enlivenObjects([objectCandidate], function(actualObjects) { actualObjects[0].uuid = objectCandidate.uuid; var object = actualObjects[0]; var existingObject = getFabricObjectByUuid(objectCandidate.uuid); if (existingObject) { existingObject.bypassRemoveEvent = true; existingObject.bypassHistory = true; canvas.remove(existingObject); } object.bypassHistory = true; canvas.add(object); object.bypassHistory = false; }); } else if (action.type === 'canvas_cleared') { canvas.clear(); } } 

I dont know if this solution (and code) fits your needs out-of-the-box. Maybe it is at some degree coupled to my specific application. I hope you manage to understand what i propose and make use of it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM