Skip to content

Commit

Permalink
allow for deep state check and save (#3262)
Browse files Browse the repository at this point in the history
  • Loading branch information
asturur committed Sep 15, 2016
1 parent c34027c commit 437eea1
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 69 deletions.
112 changes: 71 additions & 41 deletions src/mixins/stateful.mixin.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,75 @@
/*
Depends on `stateProperties`
*/
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {

/**
* Returns true if object state (one of its state properties) was changed
* @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called
*/
hasStateChanged: function() {
return this.stateProperties.some(function(prop) {
return this.get(prop) !== this.originalState[prop];
}, this);
},

/**
* Saves state of an object
* @param {Object} [options] Object with additional `stateProperties` array to include when saving state
* @return {fabric.Object} thisArg
*/
saveState: function(options) {
this.stateProperties.forEach(function(prop) {
this.originalState[prop] = this.get(prop);
}, this);

if (options && options.stateProperties) {
options.stateProperties.forEach(function(prop) {
this.originalState[prop] = this.get(prop);
}, this);
}
(function() {

return this;
},
var extend = fabric.util.object.extend;

/**
* Setups state of an object
* @return {fabric.Object} thisArg
*/
setupState: function() {
this.originalState = { };
this.saveState();
/*
Depends on `stateProperties`
*/
function saveProps(origin, destination, props) {
var tmpObj = { }, deep = true;
props.forEach(function(prop) {
tmpObj[prop] = origin[prop];
});
extend(origin[destination], tmpObj, deep);
}

return this;
function _isEqual(origValue, currentValue) {
if (origValue instanceof Array) {
if (origValue.length !== currentValue.length) {
return false
}
var _currentValue = currentValue.concat().sort(),
_origValue = origValue.concat().sort();
return !_origValue.some(function(v, i) {
return !_isEqual(_currentValue[i], v);
});
}
else if (origValue instanceof Object) {
for (var key in origValue) {
if (!_isEqual(origValue[key], currentValue[key])) {
return false;
}
}
return true;
}
else {
return origValue === currentValue;
}
}
});


fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {

/**
* Returns true if object state (one of its state properties) was changed
* @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called
*/
hasStateChanged: function() {
return !_isEqual(this.originalState, this);
},

/**
* Saves state of an object
* @param {Object} [options] Object with additional `stateProperties` array to include when saving state
* @return {fabric.Object} thisArg
*/
saveState: function(options) {
saveProps(this, 'originalState', this.stateProperties);
if (options && options.stateProperties) {
saveProps(this, 'originalState', options.stateProperties);
}
return this;
},

/**
* Setups state of an object
* @param {Object} [options] Object with additional `stateProperties` array to include when saving state
* @return {fabric.Object} thisArg
*/
setupState: function(options) {
this.originalState = { };
this.saveState(options);
return this;
}
});
})();
15 changes: 15 additions & 0 deletions src/shapes/image.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
return;
}

var stateProperties = fabric.Object.prototype.stateProperties.concat();
stateProperties.push(
'alignX',
'alignY',
'meetOrSlice'
);

/**
* Image class
* @class fabric.Image
Expand Down Expand Up @@ -97,6 +104,14 @@
*/
minimumScaleTrigger: 0.5,

/**
* List of properties to consider when checking if
* state of an object is changed ({@link fabric.Object#hasStateChanged})
* as well as for history (undo/redo) purposes
* @type Array
*/
stateProperties: stateProperties,

/**
* Constructor
* @param {HTMLImageElement | String} element Image element
Expand Down
2 changes: 1 addition & 1 deletion src/shapes/object.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@
'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' +
'stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit ' +
'angle opacity fill fillRule globalCompositeOperation shadow clipTo visible backgroundColor ' +
'alignX alignY meetOrSlice skewX skewY'
'skewX skewY'
).split(' '),

/**
Expand Down
30 changes: 25 additions & 5 deletions src/util/lang_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@
* @param {Object} source Where to copy from
* @return {Object}
*/
function extend(destination, source) {
function extend(destination, source, deep) {
// JScript DontEnum bug is not taken care of
for (var property in source) {
destination[property] = source[property];
// the deep clone is for internal use, is not meant to avoid
// javascript traps or cloning html element or self referenced objects.
if (deep) {
if (source instanceof Array) {
destination = source.map(function(v) {
return clone(v, deep)
})
}
else if (source instanceof Object) {
for (var property in source) {
destination[property] = clone(source[property], deep)
}
}
else {
// this sounds odd for an extend but is ok for recursive use
destination = source;
}
}
else {
for (var property in source) {
destination[property] = source[property];
}
}
return destination;
}
Expand All @@ -21,8 +41,8 @@
* @param {Object} object Object to clone
* @return {Object}
*/
function clone(object) {
return extend({ }, object);
function clone(object, deep) {
return extend({ }, object, deep);
}

/** @namespace fabric.util.object */
Expand Down
1 change: 1 addition & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ testrunner.run({
'./test/unit/collection.js',
'./test/unit/point.js',
'./test/unit/intersection.js',
'./test/unit/stateful.js'
]
}, function(err, report) {
if (err) {
Expand Down
22 changes: 0 additions & 22 deletions test/unit/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -680,28 +680,6 @@
}
});

test('hasStateChanged', function() {
var cObj = new fabric.Object();
ok(typeof cObj.hasStateChanged == 'function');
cObj.setupState();
ok(!cObj.hasStateChanged());
cObj.saveState();
cObj.set('left', 123).set('top', 456);
ok(cObj.hasStateChanged());
});

test('saveState', function() {
var cObj = new fabric.Object();
ok(typeof cObj.saveState == 'function');
cObj.setupState();
equal(cObj.saveState(), cObj, 'chainable');
cObj.set('left', 123).set('top', 456);
cObj.saveState();
cObj.set('left', 223).set('top', 556);
equal(cObj.originalState.left, 123);
equal(cObj.originalState.top, 456);
});

test('intersectsWithRectangle', function() {
var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100 });
cObj.setCoords();
Expand Down
98 changes: 98 additions & 0 deletions test/unit/stateful.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
(function(){

QUnit.module('fabric.stateful');

test('hasStateChanged', function() {
var cObj = new fabric.Object();
ok(typeof cObj.hasStateChanged == 'function');
cObj.setupState();
ok(!cObj.hasStateChanged(), 'state should not be changed');
cObj.saveState();
cObj.set('left', 123).set('top', 456);
ok(cObj.hasStateChanged());
});

test('saveState', function() {
var cObj = new fabric.Object();
ok(typeof cObj.saveState == 'function');
cObj.setupState();
equal(cObj.saveState(), cObj, 'chainable');
cObj.set('left', 123).set('top', 456);
cObj.saveState();
cObj.set('left', 223).set('top', 556);
equal(cObj.originalState.left, 123);
equal(cObj.originalState.top, 456);
});

test('saveState with extra props', function() {
var cObj = new fabric.Object();
cObj.prop1 = 'a';
cObj.prop2 = 'b';
cObj.left = 123;
var extraProps = ['prop1', 'prop2'];
var options = { stateProperties: extraProps };
cObj.setupState(options);
equal(cObj.originalState.prop1, 'a', 'it saves the extra props');
equal(cObj.originalState.prop2, 'b', 'it saves the extra props');
cObj.prop1 = 'c';
ok(cObj.hasStateChanged(), 'it detects changes in extra props');
equal(cObj.originalState.left, 123, 'normal props are still there');
});

test('saveState with array', function() {
var cObj = new fabric.Text('Hello');
cObj.set('textDecoration', ['underline']);
cObj.setupState();
deepEqual(cObj.textDecoration, cObj.originalState.textDecoration, 'textDecoration in state is deepEqual');
notEqual(cObj.textDecoration, cObj.originalState.textDecoration, 'textDecoration in not same Object');
cObj.textDecoration[0] = 'overline';
ok(cObj.hasStateChanged(), 'hasStateChanged detects changes in nested props');

cObj.set('textDecoration', ['overline', 'underline']);
cObj.saveState();
cObj.set('textDecoration', ['underline', 'overline']);
ok(!cObj.hasStateChanged(), 'order does no matter');

cObj.set('textDecoration', ['underline']);
cObj.saveState();
cObj.set('textDecoration', ['underline', 'overline']);
ok(cObj.hasStateChanged(), 'more properties added');

cObj.set('textDecoration', ['underline', 'overline']);
cObj.saveState();
cObj.set('textDecoration', ['overline']);
ok(cObj.hasStateChanged(), 'less properties');
});

test('saveState with fabric class gradient', function() {
var cObj = new fabric.Object();
var gradient = new fabric.Gradient({
type: 'linear',
coords: {
x1: 0,
y1: 10,
x2: 100,
y2: 200,
},
colorStops: [
{ offset: 0, color: 'red', opacity: 0 },
{ offset: 1, color: 'green' }
]
});

cObj.set('fill', '#FF0000');
cObj.setupState();
cObj.setFill(gradient);
ok(cObj.hasStateChanged(), 'hasStateChanged detects changes in nested props');
cObj.saveState();
gradient.type = 'radial';
ok(cObj.hasStateChanged(), 'hasStateChanged detects changes in nested props on first level of nesting');
cObj.saveState();
gradient.coords.x1 = 3;
ok(cObj.hasStateChanged(), 'hasStateChanged detects changes in nested props on second level of nesting');
cObj.saveState();
gradient.colorStops[0].color = 'blue';
ok(cObj.hasStateChanged(), 'hasStateChanged detects changes in nested props on third level of nesting');
});

})();

0 comments on commit 437eea1

Please sign in to comment.