202 lines
4.4 KiB
JavaScript
202 lines
4.4 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
var Collection = require('./collection');
|
||
|
|
|
||
|
|
function hasOwnProperty(e, property) {
|
||
|
|
return Object.prototype.hasOwnProperty.call(e, property.name || property);
|
||
|
|
}
|
||
|
|
|
||
|
|
function defineCollectionProperty(ref, property, target) {
|
||
|
|
|
||
|
|
var collection = Collection.extend(target[property.name] || [], ref, property, target);
|
||
|
|
|
||
|
|
Object.defineProperty(target, property.name, {
|
||
|
|
enumerable: property.enumerable,
|
||
|
|
value: collection
|
||
|
|
});
|
||
|
|
|
||
|
|
if (collection.length) {
|
||
|
|
|
||
|
|
collection.forEach(function(o) {
|
||
|
|
ref.set(o, property.inverse, target);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function defineProperty(ref, property, target) {
|
||
|
|
|
||
|
|
var inverseProperty = property.inverse;
|
||
|
|
|
||
|
|
var _value = target[property.name];
|
||
|
|
|
||
|
|
Object.defineProperty(target, property.name, {
|
||
|
|
configurable: property.configurable,
|
||
|
|
enumerable: property.enumerable,
|
||
|
|
|
||
|
|
get: function() {
|
||
|
|
return _value;
|
||
|
|
},
|
||
|
|
|
||
|
|
set: function(value) {
|
||
|
|
|
||
|
|
// return if we already performed all changes
|
||
|
|
if (value === _value) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var old = _value;
|
||
|
|
|
||
|
|
// temporary set null
|
||
|
|
_value = null;
|
||
|
|
|
||
|
|
if (old) {
|
||
|
|
ref.unset(old, inverseProperty, target);
|
||
|
|
}
|
||
|
|
|
||
|
|
// set new value
|
||
|
|
_value = value;
|
||
|
|
|
||
|
|
// set inverse value
|
||
|
|
ref.set(_value, inverseProperty, target);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Creates a new references object defining two inversly related
|
||
|
|
* attribute descriptors a and b.
|
||
|
|
*
|
||
|
|
* <p>
|
||
|
|
* When bound to an object using {@link Refs#bind} the references
|
||
|
|
* get activated and ensure that add and remove operations are applied
|
||
|
|
* reversely, too.
|
||
|
|
* </p>
|
||
|
|
*
|
||
|
|
* <p>
|
||
|
|
* For attributes represented as collections {@link Refs} provides the
|
||
|
|
* {@link RefsCollection#add}, {@link RefsCollection#remove} and {@link RefsCollection#contains} extensions
|
||
|
|
* that must be used to properly hook into the inverse change mechanism.
|
||
|
|
* </p>
|
||
|
|
*
|
||
|
|
* @class Refs
|
||
|
|
*
|
||
|
|
* @classdesc A bi-directional reference between two attributes.
|
||
|
|
*
|
||
|
|
* @param {Refs.AttributeDescriptor} a property descriptor
|
||
|
|
* @param {Refs.AttributeDescriptor} b property descriptor
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
*
|
||
|
|
* var refs = Refs({ name: 'wheels', collection: true, enumerable: true }, { name: 'car' });
|
||
|
|
*
|
||
|
|
* var car = { name: 'toyota' };
|
||
|
|
* var wheels = [{ pos: 'front-left' }, { pos: 'front-right' }];
|
||
|
|
*
|
||
|
|
* refs.bind(car, 'wheels');
|
||
|
|
*
|
||
|
|
* car.wheels // []
|
||
|
|
* car.wheels.add(wheels[0]);
|
||
|
|
* car.wheels.add(wheels[1]);
|
||
|
|
*
|
||
|
|
* car.wheels // [{ pos: 'front-left' }, { pos: 'front-right' }]
|
||
|
|
*
|
||
|
|
* wheels[0].car // { name: 'toyota' };
|
||
|
|
* car.wheels.remove(wheels[0]);
|
||
|
|
*
|
||
|
|
* wheels[0].car // undefined
|
||
|
|
*/
|
||
|
|
function Refs(a, b) {
|
||
|
|
|
||
|
|
if (!(this instanceof Refs)) {
|
||
|
|
return new Refs(a, b);
|
||
|
|
}
|
||
|
|
|
||
|
|
// link
|
||
|
|
a.inverse = b;
|
||
|
|
b.inverse = a;
|
||
|
|
|
||
|
|
this.props = {};
|
||
|
|
this.props[a.name] = a;
|
||
|
|
this.props[b.name] = b;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Binds one side of a bi-directional reference to a
|
||
|
|
* target object.
|
||
|
|
*
|
||
|
|
* @memberOf Refs
|
||
|
|
*
|
||
|
|
* @param {Object} target
|
||
|
|
* @param {String} property
|
||
|
|
*/
|
||
|
|
Refs.prototype.bind = function(target, property) {
|
||
|
|
if (typeof property === 'string') {
|
||
|
|
if (!this.props[property]) {
|
||
|
|
throw new Error('no property <' + property + '> in ref');
|
||
|
|
}
|
||
|
|
property = this.props[property];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (property.collection) {
|
||
|
|
defineCollectionProperty(this, property, target);
|
||
|
|
} else {
|
||
|
|
defineProperty(this, property, target);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
Refs.prototype.ensureRefsCollection = function(target, property) {
|
||
|
|
|
||
|
|
var collection = target[property.name];
|
||
|
|
|
||
|
|
if (!Collection.isExtended(collection)) {
|
||
|
|
defineCollectionProperty(this, property, target);
|
||
|
|
}
|
||
|
|
|
||
|
|
return collection;
|
||
|
|
};
|
||
|
|
|
||
|
|
Refs.prototype.ensureBound = function(target, property) {
|
||
|
|
if (!hasOwnProperty(target, property)) {
|
||
|
|
this.bind(target, property);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
Refs.prototype.unset = function(target, property, value) {
|
||
|
|
|
||
|
|
if (target) {
|
||
|
|
this.ensureBound(target, property);
|
||
|
|
|
||
|
|
if (property.collection) {
|
||
|
|
this.ensureRefsCollection(target, property).remove(value);
|
||
|
|
} else {
|
||
|
|
target[property.name] = undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
Refs.prototype.set = function(target, property, value) {
|
||
|
|
|
||
|
|
if (target) {
|
||
|
|
this.ensureBound(target, property);
|
||
|
|
|
||
|
|
if (property.collection) {
|
||
|
|
this.ensureRefsCollection(target, property).add(value);
|
||
|
|
} else {
|
||
|
|
target[property.name] = value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
module.exports = Refs;
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* An attribute descriptor to be used specify an attribute in a {@link Refs} instance
|
||
|
|
*
|
||
|
|
* @typedef {Object} Refs.AttributeDescriptor
|
||
|
|
* @property {String} name
|
||
|
|
* @property {boolean} [collection=false]
|
||
|
|
* @property {boolean} [enumerable=false]
|
||
|
|
*/
|