steal('can/util', 'can/util/bind','./bubble.js', './map_helpers.js','can/construct', 'can/util/batch', 'can/compute/get_value_and_bind.js', function (can, bind, bubble, mapHelpers) {
can.Map
provides the observable pattern for JavaScript objects. It
provides an attr
and removeAttr
method that can be used to get/set and
remove properties and nested properties by calling a “pipeline” of protected
methods:
_get
, _set
, _remove
- handle nested properties.__get
, __set
, __remove
- handle triggering events.___get
, ___set
, ___remove
- read / write / remove raw values.When attr
gets or sets multiple properties it calls _getAttrs
or _setAttrs
.
bubble.js - Handles bubbling of child events to parent events. map_helpers.js - Assorted helpers for handling cycles during serialization or instantition of objects.
steal('can/util', 'can/util/bind','./bubble.js', './map_helpers.js','can/construct', 'can/util/batch', 'can/compute/get_value_and_bind.js', function (can, bind, bubble, mapHelpers) {
properties that can’t be observed on … no matter what
var unobservable = {
"constructor": true
};
Extend can.Construct to make inheriting a can.Map
easier.
var Map = can.Map = can.Construct.extend(
/**
* @static
*/
{
Called when a Map constructor is defined/extended to perform any initialization behavior for the new constructor function.
setup: function (baseMap) {
can.Construct.setup.apply(this, arguments);
A cached list of computed properties on the prototype.
this._computedPropertyNames = [];
Do not run if we are defining can.Map.
if (can.Map) {
Provide warnings if can.Map is used incorrectly. !steal-remove-start
if(this.prototype.define && !mapHelpers.define) {
can.dev.warn("can/map/define is not included, yet there is a define property "+
"used. You may want to add this plugin.");
}
if(this.define && !mapHelpers.define) {
can.dev.warn("The define property should be on the map's prototype properties, "+
"not the static properties. Also, can/map/define is not included.");
}
!steal-remove-end
Create a placeholder for default values.
if (!this.defaults) {
this.defaults = {};
}
Go through everything on the prototype. If it’s a primitive, treat it as a default value. If it’s a compute, identify it so it can be setup as a computed property.
for (var prop in this.prototype) {
if (
prop !== "define" &&
prop !== "constructor" &&
(
typeof this.prototype[prop] !== "function" ||
this.prototype[prop].prototype instanceof can.Construct
)
) {
this.defaults[prop] = this.prototype[prop];
} else if (this.prototype[prop].isComputed) {
this._computedPropertyNames.push(prop);
}
}
If define is a function, call it with this can.Map
if(mapHelpers.define) {
mapHelpers.define(this, baseMap.prototype.define);
}
}
If we inherit from can.Map, but not can.List, create a can.List that creates instances of this Map type.
if (can.List && !(this.prototype instanceof can.List)) {
this.List = Map.List.extend({
Map: this
}, {});
}
},
shortName: "Map",
Returns which events to setup bubbling on for a given bound event. By default, only bubbles “change” events if someone listens to a “change” event or a nested event like “foo.bar”.
_bubbleRule: function(eventName) {
return (eventName === "change" || eventName.indexOf(".") >= 0 ) ?
["change"] :
[];
},
bind: can.bindAndSetup,
unbind: can.unbindAndTeardown,
id: "id",
keys: function (map) {
var keys = [];
can.__observe(map, '__keys');
for (var keyName in map._data) {
keys.push(keyName);
}
return keys;
}
},
/**
* @prototype
*/
{
setup: function (obj) {
if(obj instanceof can.Map){
obj = obj.serialize();
}
Where we keep the values of the compute.
this._data = {};
The namespace this object
uses to listen to events.
can.cid(this, ".map");
this._setupComputedProperties();
var teardownMapping = obj && mapHelpers.addToMap(obj, this);
var defaultValues = this._setupDefaults(obj);
var data = can.extend(can.extend(true, {}, defaultValues), obj);
this.attr(data);
if (teardownMapping) {
teardownMapping();
}
},
Sets up computed properties on a Map.
Stores information for each computed property on
this._computedAttrs
that looks like:
{
// the number of bindings on this property
count: 1,
// a handler that forwards events on the compute
// to the map instance
handler: handler,
compute: compute // the compute
}
_setupComputedProperties: function () {
this._computedAttrs = {};
var computes = this.constructor._computedPropertyNames;
for (var i = 0, len = computes.length; i < len; i++) {
var attrName = computes[i];
mapHelpers.addComputedAttr(this, attrName, this[attrName].clone(this));
}
},
_setupDefaults: function(){
return this.constructor.defaults || {};
},
The primary get/set interface for can.Map.
Calls _get
, _set
or _attrs
depending on
how it is called.
attr: function (attr, val) {
var type = typeof attr;
if(attr === undefined) {
return this._getAttrs();
} else if (type !== "string" && type !== "number") {
Get or set multiple attributes.
return this._setAttrs(attr, val);
}
else if (arguments.length === 1) {
Get a single attribute.
return this._get(attr+"");
} else {
Set an attribute.
this._set(attr+"", val);
return this;
}
},
Handles reading nested properties like “foo.bar” by
getting the value of “foo” and recursively
calling _get
for the value of “bar”.
To read the actual values, _get
calls
___get
.
_get: function (attr) {
var dotIndex = attr.indexOf('.');
if( dotIndex >= 0 ) {
Attempt to get the value anyway in case
somone wrote new can.Map({"foo.bar": 1})
.
var value = this.___get(attr);
if (value !== undefined) {
can.__observe(this, attr);
return value;
}
var first = attr.substr(0, dotIndex),
second = attr.substr(dotIndex+1);
var current = this.__get( first );
return current && current._get ? current._get(second) : undefined;
} else {
return this.__get( attr );
}
},
__get: function(attr){
if(!unobservable[attr] && !this._computedAttrs[attr]) {
can.__observe(this, attr);
}
return this.___get( attr );
},
When called with an argument, returns the value of this property. If that property is represented by a computed attribute, return the value of that compute. If no argument is provided, return the raw data.
___get: function (attr) {
if (attr !== undefined) {
var computedAttr = this._computedAttrs[attr];
if (computedAttr && computedAttr.compute) {
return computedAttr.compute();
return computedAttr.compute();
} else {
return this._data.hasOwnProperty(attr) ? this._data[attr] : undefined;
}
} else {
return this._data;
}
},
Handles setting nested properties by finding the
nested observable and recursively calling _set
on it. Eventually,
it calls __set
with the __type
converted value to set
and the current value. The current value is passed for two reasons:
__set
can trigger an event if the value has changed.If the map is initializing, the current value does not need to be read because no change events are dispatched anyway.
_set: function (attr, value, keepKey) {
var dotIndex = attr.indexOf('.'),
current;
if(dotIndex >= 0 && !keepKey){
var first = attr.substr(0, dotIndex),
second = attr.substr(dotIndex+1);
current = this.__inSetup ? undefined : this.___get( first );
if( can.isMapLike(current) ) {
current._set(second, value);
} else {
throw new Error("can.Map: Object does not exist");
}
} else {
current = this.__inSetup ? undefined : this.___get( attr );
//Convert if there is a converter. Remove in 3.0.
if (this.__convert) {
value = this.__convert(attr, value);
}
this.__set(attr, this.__type(value, attr), current);
}
},
Converts set values to another type. By default, this converts Objects to can.Maps and Arrays to can.Lists. This also makes it so if a plain JavaScript object has already been converted to a list or map, that same list or map instance is used.
__type: function(value, prop){
if (typeof value === "object" && !( value instanceof can.Map) && mapHelpers.canMakeObserve(value) ) {
var cached = mapHelpers.getMapFromObject(value);
if(cached) {
return cached;
}
if( can.isArray(value) ) {
var List = can.List;
return new List(value);
} else {
var Map = this.constructor.Map || can.Map;
return new Map(value);
}
}
return value;
},
Handles firing events if the value has changed and
works with the bubble
helpers to setup bubbling.
Calls ___set
to do the actual setting.
__set: function (prop, value, current) {
if (value !== current) {
var computedAttr = this._computedAttrs[prop];
Dispatch an “add” event if adding a new property.
var changeType = computedAttr || current !== undefined || this.___get()
.hasOwnProperty(prop) ? "set" : "add";
Set the value on _data
and set up bubbling.
this.___set(prop, typeof value === "object" ? bubble.set(this, prop, value, current) : value );
Computed properties change events are already forwarded except if no one is listening to them.
if(!computedAttr || !computedAttr.count) {
this._triggerChange(prop, changeType, value, current);
}
Stop bubbling old nested maps.
if (typeof current === "object") {
bubble.teardownFromParent(this, current);
}
}
},
___set: function (prop, val) {
var computedAttr = this._computedAttrs[prop];
if ( computedAttr && computedAttr.compute ) {
computedAttr.compute(val);
} else {
this._data[prop] = val;
}
Adds the property directly to the map instance. But first, checks that it’s not overwriting a method. This should be removed in 3.0.
if ( typeof this.constructor.prototype[prop] !== 'function' && !computedAttr ) {
this[prop] = val;
}
},
removeAttr: function (attr) {
return this._remove(attr);
},
_remove: function(attr){
If this is List.
var parts = mapHelpers.attrParts(attr),
The actual property to remove.
prop = parts.shift(),
The current value.
current = this.___get(prop);
If we have more parts, call removeAttr
on that part.
if (parts.length && current) {
return current.removeAttr(parts);
} else {
If attr does not have a .
if (typeof attr === 'string' && !!~attr.indexOf('.')) {
prop = attr;
}
this.__remove(prop, current);
return current;
}
},
__remove: function(prop, current){
if (prop in this._data) {
this.___remove(prop);
Let others now this property has been removed.
this._triggerChange(prop, "remove", undefined, current);
}
},
___remove: function(prop){
delete this._data[prop];
if (!(prop in this.constructor.prototype)) {
delete this[prop];
}
},
___serialize: function(name, val){
return mapHelpers.getValue(this, name, val, "serialize");
},
_getAttrs: function(){
return mapHelpers.serialize(this, 'attr', {});
},
Sets multiple properties on this object at once. First, goes through all current properties and either merges or removes old properties. Then it goes through the remaining ones to be added and sets those properties.
_setAttrs: function (props, remove) {
props = can.simpleExtend({}, props);
var prop,
self = this,
newVal;
Batch all of the change events until we are done.
can.batch.start();
Merge current properties with the new ones.
this._each(function (curVal, prop) {
You can not have a _cid property; abort.
if (prop === "_cid") {
return;
}
newVal = props[prop];
If we are merging, remove the property if it has no value.
if (newVal === undefined) {
if (remove) {
self.removeAttr(prop);
}
return;
}
Run converter if there is one. Remove in 3.0.
if (self.__convert) {
newVal = self.__convert( prop, newVal );
}
if ( can.isMapLike(curVal) && mapHelpers.canMakeObserve(newVal) ) {
curVal.attr(newVal, remove);
Otherwise just set.
} else if (curVal !== newVal) {
self.__set(prop, self.__type(newVal, prop), curVal);
}
delete props[prop];
});
Add remaining props.
for (prop in props) {
Ignore _cid.
if (prop !== "_cid") {
newVal = props[prop];
this._set(prop, newVal, true);
}
}
can.batch.stop();
return this;
},
serialize: function () {
return mapHelpers.serialize(this, 'serialize', {});
},
A helper function used to trigger events on this map. If the map is bubbling, this will fire a change event. Otherwise, it only fires a “named” event. Triggers a “__keys” event if a property has been added or removed.
_triggerChange: function (attr, how, newVal, oldVal, batchNum) {
if(bubble.isBubbling(this, "change")) {
can.batch.trigger(this, {
type: "change",
target: this,
batchNum: batchNum
}, [attr, how, newVal, oldVal]);
}
can.batch.trigger(this, {
type: attr,
target: this,
batchNum: batchNum
}, [newVal, oldVal]);
if(how === "remove" || how === "add") {
can.batch.trigger(this, {
type: "__keys",
target: this,
batchNum: batchNum
});
}
},
_bindsetup: function(){},
_bindteardown: function(){},
one: can.one,
Listens to an event on a map. If the event is a computed property, listen to the compute and forward its events to this map.
bind: function (eventName, handler) {
var computedBinding = this._computedAttrs && this._computedAttrs[eventName];
if (computedBinding && computedBinding.compute) {
if (!computedBinding.count) {
computedBinding.count = 1;
computedBinding.compute.bind("change", computedBinding.handler);
} else {
computedBinding.count++;
}
}
Sets up bubbling if needed.
bubble.bind(this, eventName);
return can.bindAndSetup.apply(this, arguments);
},
Stops listening to an event. If this is the last listener of a computed property, stop forwarding events of the computed property to this map.
unbind: function (eventName, handler) {
var computedBinding = this._computedAttrs && this._computedAttrs[eventName];
if (computedBinding) {
if (computedBinding.count === 1) {
computedBinding.count = 0;
computedBinding.compute.unbind("change", computedBinding.handler);
} else {
computedBinding.count--;
}
}
Teardown bubbling if needed.
bubble.unbind(this, eventName);
return can.unbindAndTeardown.apply(this, arguments);
},
Creates a compute that represents a value on this map. If the property is a function on the prototype, a “function” compute wil be created. Otherwise, a compute will be created that reads the observable attributes.
compute: function (prop) {
if (can.isFunction(this.constructor.prototype[prop])) {
return can.compute(this[prop], this);
} else {
var reads = can.compute.read.reads(prop),
last = reads.length - 1;
return can.compute(function (newVal) {
if (arguments.length) {
can.compute.read(this, reads.slice(0, last))
.value.attr(reads[last].key, newVal);
} else {
return can.compute.read(this, reads, {
args: []
}).value;
}
}, this);
}
},
each: function () {
return can.each.apply(undefined, [this].concat(can.makeArray(arguments)));
},
_each: function (callback) {
var data = this.___get();
for (var prop in data) {
if (data.hasOwnProperty(prop)) {
callback(data[prop], prop);
}
}
},
dispatch: can.dispatch
});
Map.prototype.on = Map.prototype.bind;
Map.prototype.off = Map.prototype.unbind;
Map.on = Map.bind;
Map.off = Map.unbind;
return Map;
});