Skip to content

Commit

Permalink
feat: add undo stack of object resize/move [#358] (#490)
Browse files Browse the repository at this point in the history
* feat: add objectModified handler

* feat: resize move add undo stack - middle commit

* feat: resize move add undo stack - middle commit 2

* feat: one item undo redo prototype complate

* feat: undo chage from group selection = #1

* feat: prototyping complete

* feat: add selectionModifyHelper

* feat: complate refactoring

* refactor: complete refactoring

* feat: added test for changeSelection command

* test: add selectionModifyHelper test

* chore: fix misspelling

* chore: apply code review

Co-authored-by: lja1018 <jaeeon.lim@nhn.com>
Co-authored-by: jinwoo-kim-nhn <jw.kim@nhn.com>
  • Loading branch information
3 people committed Dec 8, 2020
1 parent f9ae2fd commit 09e5582
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ import './js/command/resizeCanvasDimension';
import './js/command/rotate';
import './js/command/setObjectProperties';
import './js/command/setObjectPosition';
import './js/command/changeSelection';

module.exports = ImageEditor;
35 changes: 35 additions & 0 deletions src/js/command/changeSelection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @author NHN. FE Development Team <dl_javascript@nhn.com>
* @fileoverview change selection
*/
import commandFactory from '../factory/command';
import {Promise} from '../util';
import {commandNames} from '../consts';
import {getCachedUndoDataForDimension} from '../helper/selectionModifyHelper';

const command = {
name: commandNames.CHANGE_SELECTION,

execute(graphics, props) {
if (this.isRedo) {
props.forEach(prop => {
graphics.setObjectProperties(prop.id, prop);
});
} else {
this.undoData = getCachedUndoDataForDimension();
}

return Promise.resolve();
},
undo(graphics) {
this.undoData.forEach(datum => {
graphics.setObjectProperties(datum.id, datum);
});

return Promise.resolve();
}
};

commandFactory.register(command);

export default command;
4 changes: 3 additions & 1 deletion src/js/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ export const commandNames = {
'ADD_IMAGE_OBJECT': 'addImageObject',
'RESIZE_CANVAS_DIMENSION': 'resizeCanvasDimension',
'SET_OBJECT_PROPERTIES': 'setObjectProperties',
'SET_OBJECT_POSITION': 'setObjectPosition'
'SET_OBJECT_POSITION': 'setObjectPosition',
'CHANGE_SELECTION': 'changeSelection'
};

/**
Expand All @@ -113,6 +114,7 @@ export const eventNames = {
OBJECT_CREATED: 'objectCreated',
OBJECT_ROTATED: 'objectRotated',
OBJECT_ADDED: 'objectAdded',
OBJECT_MODIFIED: 'objectModified',
TEXT_EDITING: 'textEditing',
TEXT_CHANGED: 'textChanged',
ICON_CREATE_RESIZE: 'iconCreateResize',
Expand Down
21 changes: 19 additions & 2 deletions src/js/graphics.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import ShapeDrawingMode from './drawingMode/shape';
import TextDrawingMode from './drawingMode/text';
import {getProperties, includes, isShape, Promise} from './util';
import {componentNames as components, eventNames as events, drawingModes, fObjectOptions} from './consts';
import {
makeSelectionUndoData,
makeSelectionUndoDatum,
setCachedUndoDataForDimension
} from './helper/selectionModifyHelper';

const {extend, stamp, isArray, isString, forEachArray, forEachOwnProperties, CustomEvents} = snippet;
const DEFAULT_CSS_MAX_WIDTH = 1000;
Expand Down Expand Up @@ -986,8 +991,18 @@ class Graphics {
* @private
*/
_onMouseDown(fEvent) {
const originPointer = this._canvas.getPointer(fEvent.e);
this.fire(events.MOUSE_DOWN, fEvent.e, originPointer);
const {e: event, target} = fEvent;
const originPointer = this._canvas.getPointer(event);

if (target) {
const {type} = target;
const undoData = makeSelectionUndoData(target,
item => makeSelectionUndoDatum(this.getObjectId(item), item, type === 'activeSelection'));

setCachedUndoDataForDimension(undoData);
}

this.fire(events.MOUSE_DOWN, event, originPointer);
}

/**
Expand Down Expand Up @@ -1045,6 +1060,8 @@ class Graphics {

items.forEach(item => item.fire('modifiedInGroup', target));
}

this.fire(events.OBJECT_MODIFIED, target, this.getObjectId(target));
}

/**
Expand Down
84 changes: 84 additions & 0 deletions src/js/helper/selectionModifyHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @author NHN. FE Development Team <dl_javascript@nhn.com>
* @fileoverview Selection modification helper
*/

import {extend} from 'tui-code-snippet/src/js/object';

/**
* Cached selection's info
* @type {Array}
* @private
*/
let cachedUndoDataForChangeDimension = null;

/**
* Set cached undo data
* @param {Array} undoData - selection object
* @private
*/
export function setCachedUndoDataForDimension(undoData) {
cachedUndoDataForChangeDimension = undoData;
}

/**
* Get cached undo data
* @returns {Object} cached undo data
* @private
*/
export function getCachedUndoDataForDimension() {
return cachedUndoDataForChangeDimension;
}

/**
* Make undo data
* @param {fabric.Object} obj - selection object
* @param {Function} undoDatumMaker - make undo datum
* @returns {Array} undoData
* @private
*/
export function makeSelectionUndoData(obj, undoDatumMaker) {
let undoData;

if (obj.type === 'activeSelection') {
undoData = obj.getObjects().map(item => {
const {angle, left, top} = item;

obj.realizeTransform(item);
const result = undoDatumMaker(item);

item.set({
angle,
left,
top
});

return result;
});
} else {
undoData = [undoDatumMaker(obj)];
}

return undoData;
}

/**
* Make undo datum
* @param {number} id - object id
* @param {fabric.Object} obj - selection object
* @param {boolean} isSelection - whether or not object is selection
* @returns {Object} undo datum
* @private
*/
export function makeSelectionUndoDatum(id, obj, isSelection) {
return isSelection ? {
id,
width: obj.width,
height: obj.height,
top: obj.top,
left: obj.left,
angle: obj.angle,
scaleX: obj.scaleX,
scaleY: obj.scaleY
} : extend({id}, obj);
}
36 changes: 33 additions & 3 deletions src/js/imageEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import commandFactory from './factory/command';
import Graphics from './graphics';
import {sendHostName, Promise} from './util';
import {eventNames as events, commandNames as commands, keyCodes, rejectMessages} from './consts';
import {
makeSelectionUndoData,
makeSelectionUndoDatum
} from './helper/selectionModifyHelper';

const {isUndefined, forEach, CustomEvents} = snippet;

Expand All @@ -20,6 +24,7 @@ const {
OBJECT_ACTIVATED,
OBJECT_ROTATED,
OBJECT_ADDED,
OBJECT_MODIFIED,
ADD_TEXT,
ADD_OBJECT,
TEXT_EDITING,
Expand Down Expand Up @@ -208,6 +213,7 @@ class ImageEditor {
objectScaled: this._onObjectScaled.bind(this),
objectRotated: this._onObjectRotated.bind(this),
objectAdded: this._onObjectAdded.bind(this),
objectModified: this._onObjectModified.bind(this),
createdPath: this._onCreatedPath,
addText: this._onAddText.bind(this),
addObject: this._onAddObject.bind(this),
Expand Down Expand Up @@ -308,6 +314,7 @@ class ImageEditor {
[OBJECT_ROTATED]: this._handlers.objectRotated,
[OBJECT_ACTIVATED]: this._handlers.objectActivated,
[OBJECT_ADDED]: this._handlers.objectAdded,
[OBJECT_MODIFIED]: this._handlers.objectModified,
[ADD_TEXT]: this._handlers.addText,
[ADD_OBJECT]: this._handlers.addObject,
[TEXT_EDITING]: this._handlers.textEditing,
Expand Down Expand Up @@ -384,8 +391,8 @@ class ImageEditor {

/**
* mouse down event handler
* @param {Event} event mouse down event
* @param {Object} originPointer origin pointer
* @param {Event} event - mouse down event
* @param {Object} originPointer - origin pointer
* @param {Number} originPointer.x x position
* @param {Number} originPointer.y y position
* @private
Expand All @@ -410,6 +417,7 @@ class ImageEditor {
* }
* });
*/

this.fire(events.MOUSE_DOWN, event, originPointer);
}

Expand All @@ -423,6 +431,20 @@ class ImageEditor {
this._invoker.pushUndoStack(command);
}

/**
* Add a 'changeSelection' command
* @param {fabric.Object} obj - selection object
* @private
*/
_pushModifyObjectCommand(obj) {
const {type} = obj;
const props = makeSelectionUndoData(obj, item => makeSelectionUndoDatum(this._graphics.getObjectId(item), item, type === 'activeSelection'));
const command = commandFactory.create(commands.CHANGE_SELECTION, this._graphics, props);
command.execute(this._graphics, props);

this._invoker.pushUndoStack(command);
}

/**
* 'objectActivated' event handler
* @param {ObjectProps} props - object properties
Expand Down Expand Up @@ -1213,7 +1235,6 @@ class ImageEditor {
* @param {Object} objectProps added object properties
* @private
*/

_onObjectAdded(objectProps) {
/**
* The event when object added
Expand All @@ -1235,6 +1256,15 @@ class ImageEditor {
this.fire(ADD_OBJECT_AFTER, objectProps);
}

/**
* 'objectModified' event handler
* @param {fabric.Object} obj - selection object
* @private
*/
_onObjectModified(obj) {
this._pushModifyObjectCommand(obj);
}

/**
* 'selectionCleared' event handler
* @private
Expand Down
61 changes: 61 additions & 0 deletions test/command.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Invoker from '../src/js/invoker';
import commandFactory from '../src/js/factory/command';
import Graphics from '../src/js/graphics';
import {commandNames as commands} from '../src/js/consts';
import {getCachedUndoDataForDimension} from '../src/js/helper/selectionModifyHelper';

describe('commandFactory', () => {
let invoker, mockImage, canvas, graphics;
Expand Down Expand Up @@ -122,6 +123,66 @@ describe('commandFactory', () => {
});
});
});
describe('changeSelectionCommand', () => {
let obj;

beforeEach(() => {
spyOn(canvas, 'getPointer');
obj = new fabric.Rect({
width: 10,
height: 10,
top: 10,
left: 10,
scaleX: 1,
scaleY: 1,
angle: 0
});
graphics._addFabricObject(obj);
graphics._onMouseDown({target: obj});

const props = [{
id: graphics.getObjectId(obj),
width: 30,
height: 30,
top: 30,
left: 30,
scaleX: 0.5,
scaleY: 0.5,
angle: 10
}];
const makeCommand = commandFactory.create(commands.CHANGE_SELECTION, graphics, props);
makeCommand.execute(graphics, props);
invoker.pushUndoStack(makeCommand);
});

it('should work undo command correctly', done => {
invoker.undo().then(() => {
expect(obj.width).toBe(10);
expect(obj.height).toBe(10);
expect(obj.left).toBe(10);
expect(obj.top).toBe(10);
expect(obj.scaleX).toBe(1);
expect(obj.scaleY).toBe(1);
expect(obj.angle).toBe(0);
done();
});
});

it('should work redo command correctly', done => {
invoker.undo().then(() => {
invoker.redo().then(() => {
expect(obj.width).toBe(30);
expect(obj.height).toBe(30);
expect(obj.left).toBe(30);
expect(obj.top).toBe(30);
expect(obj.scaleX).toBe(0.5);
expect(obj.scaleY).toBe(0.5);
expect(obj.angle).toBe(10);
done();
});
});
});
});

describe('loadImageCommand', () => {
const imageURL = 'base/test/fixtures/sampleImage.jpg';
Expand Down

0 comments on commit 09e5582

Please sign in to comment.