Implement an efficient mechanism to prioritize key in Item#set()

Closes #1096
This commit is contained in:
Jürg Lehni 2016-07-25 23:17:45 +02:00
parent 4954f5d6ab
commit 3f76bd99ef
7 changed files with 65 additions and 31 deletions

View file

@ -78,17 +78,20 @@ Base.inject(/** @lends Base# */{
},
/**
* #_set() is part of the mechanism for constructors which take one object
* #set() is part of the mechanism for constructors which take one object
* literal describing all the properties to be set on the created instance.
* Through {@link Base.filter()} it supports `_filtered`
* handling as required by the {@link Base.readNamed()} mechanism.
*
* @param {Object} props an object describing the properties to set
* @return {Boolean} {@true if the object is a plain object}
* @param {Object} [exclude] an object that can define any properties as
* `true` that should be excluded
* @return {Object} a reference to `this`, for chainability.
*/
_set: function(props) {
if (props && Base.isPlainObject(props))
return Base.filter(this, props);
set: function(props, exclude) {
if (props)
Base.filter(this, props, exclude, this._prioritize);
return this;
},
statics: /** @lends Base */{
@ -276,14 +279,14 @@ Base.inject(/** @lends Base# */{
var value = this.getNamed(list, name),
hasObject = value !== undefined;
if (hasObject) {
// Create a _filtered object that inherits from argument 0, and
// Create a _filtered object that inherits from list[0], and
// override all fields that were already read with undefined.
var filtered = list._filtered;
if (!filtered) {
filtered = list._filtered = Base.create(list[0]);
// Point _filtering to the original so Base#_set() can
// Point _unfiltered to the original so Base#_set() can
// execute hasOwnProperty on it.
filtered._filtering = list[0];
filtered._unfiltered = list[0];
}
// delete wouldn't work since the masked parent's value would
// shine through.
@ -319,17 +322,24 @@ Base.inject(/** @lends Base# */{
/**
* Copies all properties from `source` over to `dest`, supporting
* `_filtered` handling as required by {@link Base.readNamed()}
* mechanism, as well as an optional exclude` object that lists
* properties to exclude.
* mechanism, as well as a way to exclude and prioritize properties.
*
* @param {Object} dest the destination that is to receive the
* properties
* @param {Object} source the source from where to retrieve the
* properties to be copied
* @param {Object} [exclude] an object that can define any properties
* as `true` that should be excluded when copying
* @param {String[]} [prioritize] a list of keys that should be
* prioritized when copying, if they are defined in `source`,
* processed in the order of appearance
*/
filter: function(dest, source, exclude) {
// If source is a filtering object, we need to get the keys from the
// the original object (it's parent / prototype). See _filtered
// inheritance trick in the argument reading code.
var keys = Object.keys(source._filtering || source);
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
if (!(exclude && exclude[key])) {
filter: function(dest, source, exclude, prioritize) {
var processed;
function handleKey(key) {
if (!(exclude && key in exclude) &&
!(processed && key in processed)) {
// Due to the _filtered inheritance trick, undefined is used
// to mask already consumed named arguments.
var value = source[key];
@ -337,6 +347,25 @@ Base.inject(/** @lends Base# */{
dest[key] = value;
}
}
// If there are prioritized keys, process them first.
if (prioritize) {
var keys = {};
prioritize.forEach(function(key) {
if (key in source) {
handleKey(key);
keys[key] = true;
}
});
// Now reference the processed keys as processed, so that
// handleKey() will not set them again below.
processed = keys;
}
// If source is a filtered object, we get the keys from the
// the original object (it's parent / prototype). See _filtered
// inheritance trick in the argument reading code.
Object.keys(source._unfiltered || source).forEach(handleKey);
return dest;
},

View file

@ -79,7 +79,9 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
clipMask: false,
selected: false,
data: {}
}
},
// Prioritize `applyMatrix` over `matrix`:
_prioritize: ['applyMatrix']
},
new function() { // Injection scope for various item event handlers
var handlers = ['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onClick',
@ -161,9 +163,8 @@ new function() { // Injection scope for various item event handlers
}
// Filter out Item.NO_INSERT before _set(), for performance reasons.
if (hasProps && props !== Item.NO_INSERT) {
// Filter out internal, insert, parent and project properties as
// these were handled above.
Base.filter(this, props, {
this.set(props, {
// Filter out these properties as they were handled above:
internal: true, insert: true, project: true, parent: true
});
}
@ -243,6 +244,8 @@ new function() { // Injection scope for various item event handlers
* values defined in the object literal, if the item has property of the
* given name (or a setter defined for it).
*
* @name Item#set
* @function
* @param {Object} props
* @return {Item} the item itself
*
@ -260,11 +263,6 @@ new function() { // Injection scope for various item event handlers
* selected: true
* });
*/
set: function(props) {
if (props)
this._set(props);
return this;
},
/**
* The unique id of the item.

View file

@ -28,6 +28,8 @@ var Raster = Item.extend(/** @lends Raster# */{
crossOrigin: null, // NOTE: Needs to be set before source to work!
source: null
},
// Prioritize `crossOrigin` over `source`:
_prioritize: ['crossOrigin'],
// TODO: Implement type, width, height.
// TODO: Have SymbolItem & Raster inherit from a shared class?

View file

@ -68,7 +68,8 @@ var Gradient = Base.extend(/** @lends Gradient# */{
initialize: function Gradient(stops, radial) {
// Use UID here since Gradients are exported through dictionary.add().
this._id = UID.get();
if (stops && this._set(stops)) {
if (stops && Base.isPlainObject(stops)) {
this.set(stops);
// Erase arguments since we used the passed object instead.
stops = radial = null;
}

View file

@ -56,7 +56,7 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{
// -1 so first event is 0:
this._moveCount = -1;
this._downCount = -1;
this._set(props);
this.set(props);
},
/**

View file

@ -48,6 +48,9 @@ test('new Group({children:[item]})', function() {
equals(function() {
return paper.project.activeLayer.children.length;
}, 1);
equals(function() {
return path.parent == group;
}, true);
equals(function() {
return group.children[0] == path;
}, true);

View file

@ -787,10 +787,11 @@ test('Item#applyMatrix', function() {
var path = new Path.Rectangle({
size: [100, 100],
position: [0, 0],
applyMatrix: false
position: [0, 0]
});
path.applyMatrix = false;
equals(path.matrix, new Matrix(),
'path.matrix before scaling');
equals(path.bounds, new Rectangle(-50, -50, 100, 100),