todomvc
Version:
> Helping you select an MV\* framework
443 lines (417 loc) • 13.8 kB
JavaScript
/*!
* CanJS - 2.0.3
* http://canjs.us/
* Copyright (c) 2013 Bitovi
* Tue, 26 Nov 2013 18:21:22 GMT
* Licensed MIT
* Includes: CanJS default build
* Download from: http://canjs.us/
*/
define(["can/util/library", "can/construct", "can/map", "can/list", "can/view", "can/compute"], function(can){
var isObserve = function(obj) {
return obj instanceof can.Map || (obj && obj.__get);
},
getProp = function(obj, prop){
var val = obj[prop];
if(typeof val !== "function" && obj.__get) {
return obj.__get(prop);
}
else {
return val;
}
},
escapeReg = /(\\)?\./g,
escapeDotReg = /\\\./g,
getNames = function(attr){
var names = [], last = 0;
attr.replace(escapeReg, function($0, $1, index) {
if (!$1) {
names.push(attr.slice(last, index).replace(escapeDotReg,'.'));
last = index + $0.length;
}
});
names.push(attr.slice(last).replace(escapeDotReg,'.'));
return names;
}
/**
* @add can.view.Scope
*/
var Scope = can.Construct.extend(
/**
* @static
*/
{
// reads properties from a parent. A much more complex version of getObject.
/**
* @function can.view.Scope.read read
* @parent can.view.Scope.static
*
* @signature `Scope.read(parent, reads, options)`
*
* Read properties from an object.
*
* @param {*} parent A parent object to read properties from.
* @param {Array<String>} reads An array of properties to read.
* @param {can.view.Scope.readOptions} options Configures
* how to read properties and values and register callbacks
*
* @return {{value: *, parent: *}} Returns an object that
* provides the value and parent object.
*
* @option {*} value The value found by reading `reads` properties. If
* no value was found, value will be undefined.
*
* @option {*} parent The most immediate parent object of the value specified by `key`.
*
* @body
*
*
*/
read: function(parent, reads, options){
options = options || {};
// `cur` is the current value.
var cur = parent,
type,
// `prev` is the object we are reading from.
prev,
// `foundObs` did we find an observable.
foundObs;
for( var i = 0, readLength = reads.length ; i < readLength; i++ ) {
// Update what we are reading from.
prev = cur;
// Read from the compute. We can't read a property yet.
if( prev && prev.isComputed ) {
options.foundObservable && options.foundObservable(prev, i)
prev = prev()
}
// Look to read a property from something.
if( isObserve(prev) ) {
!foundObs && options.foundObservable && options.foundObservable(prev, i);
foundObs = 1;
// is it a method on the prototype?
if(typeof prev[reads[i]] === "function" && prev.constructor.prototype[reads[i]] === prev[reads[i]] ){
// call that method
if(options.returnObserveMethods){
cur = cur[reads[i]]
} else {
cur = prev[ reads[i] ].apply(prev, options.args ||[])
}
} else {
// use attr to get that value
cur = cur.attr( reads[i] );
}
} else {
// just do the dot operator
cur = prev[reads[i]]
}
// If it's a compute, get the compute's value
// unless we are at the end of the
if( cur && cur.isComputed && (!options.isArgument && i < readLength - 1) ) {
!foundObs && options.foundObservable && options.foundObservable(prev, i+1)
cur = cur()
}
type = typeof cur;
// if there are properties left to read, and we don't have an object, early exit
if( i < reads.length -1 && ( cur == null || (type != "function" && type != "object" ) ) ) {
options.earlyExit && options.earlyExit(prev, i, cur);
// return undefined so we know this isn't the right value
return {value: undefined, parent: prev};
}
}
// if we don't have a value, exit early.
if( cur === undefined ){
options.earlyExit && options.earlyExit(prev, i - 1)
}
// handle an ending function
if(typeof cur === "function"){
if( options.isArgument ) {
if( ! cur.isComputed && options.proxyMethods !== false) {
cur = can.proxy(cur, prev)
}
} else {
cur.isComputed && !foundObs && options.foundObservable && options.foundObservable(cur, i)
cur = cur.call(prev)
}
}
return {value: cur, parent: prev};
}
},
/**
* @prototype
*/
{
init: function(context, parent){
this._context = context;
this._parent = parent;
},
/**
* @function can.view.Scope.prototype.attr
*
* Reads a value from the current context or parent contexts.
*
* @param {can.Mustache.key} key A dot seperated path. Use `"\."` if you have a
* property name that includes a dot.
*
* @return {*} The found value or undefined if no value is found.
*
* @body
*
* ## Use
*
* `scope.attr(key)` looks up a value in the current scope's
* context, if a value is not found, parent scope's context
* will be explored.
*
* var list = [{name: "Justin"},{name: "Brian"}],
* justin = list[0];
*
* var curScope = new can.view.Scope(list).add(justin);
*
* curScope.attr("name") //-> "Justin"
* curScope.attr("length") //-> 2
*/
attr: function(key){
return this.read(key,{isArgument: true, returnObserveMethods:true, proxyMethods: false}).value
},
/**
* @function can.view.Scope.prototype.add
*
* Creates a new scope with its parent set as the current scope.
*
* @param {*} context The context of the new scope object.
*
* @return {can.view.Scope} A scope object.
*
* @body
*
* ## Use
*
* `scope.add(context)` creates a new scope object that
* first looks up values in context and then in the
* parent `scope` object.
*
* var list = [{name: "Justin"},{name: "Brian"}],
* justin = list[0];
*
* var curScope = new can.view.Scope(list).add(justin);
*
* curScope.attr("name") //-> "Justin"
* curScope.attr("length") //-> 2
*/
add: function(context){
if(context !== this._context){
return new this.constructor( context, this );
} else {
return this;
}
},
/**
* @function can.view.Scope.prototype.computeData
*
* @description Provides a compute that represents a
* key's value and other information about where the value was found.
*
*
* @param {can.Mustache.key} key A dot seperated path. Use `"\."` if you have a
* property name that includes a dot.
*
* @param {can.view.Scope.readOptions} [options] Options that configure how the `key` gets read.
*
* @return {{}} An object with the following values:
*
* @option {can.compute} compute A compute that returns the
* value of `key` looked up in the scope's context or parent context. This compute can
* also be written to, which will set the observable attribute or compute value at the
* location represented by the key.
*
* @option {can.view.Scope} scope The scope the key was found within. The key might have
* been found in a parent scope.
*
* @option {*} initialData The initial value at the key's location.
*
* @body
*
* ## Use
*
* `scope.computeData(key, options)` is used heavily by [can.Mustache] to get the value of
* a [can.Mustache.key key] value in a template. Configure how it reads values in the
* scope and what values it returns with the [can.view.Scope.readOptions options] argument.
*
* var context = new Map({
* name: {first: "Curtis"}
* })
* var scope = new can.view.Scope(context)
* var computeData = scope.computeData("name.first");
*
* computeData.scope === scope //-> true
* computeData.initialValue //-> "Curtis"
* computeData.compute() //-> "Curtis"
*
* The `compute` value is writable. For example:
*
* computeData.compute("Andy")
* context.attr("name.first") //-> "Andy"
*
*/
computeData: function(key, options ){
options = options || {args: []};
var self = this,
rootObserve,
rootReads,
computeData = {
compute: can.compute(function(newVal){
if(arguments.length){
// check that there's just a compute with nothing from it ...
if(rootObserve.isComputed && !rootReads.length){
rootObserve(newVal)
} else {
var last = rootReads.length-1;
Scope.read(rootObserve,rootReads.slice(0, last)).value.attr(rootReads[last], newVal)
}
} else {
if( rootObserve ) {
return Scope.read(rootObserve, rootReads, options).value
}
// otherwise, go get the value
var data = self.read(key, options);
rootObserve = data.rootObserve;
rootReads = data.reads;
computeData.scope = data.scope;
computeData.initialValue = data.value;
return data.value;
}
})
};
return computeData
},
/**
* @hide
* @function can.view.Scope.prototype.read read
*
* Read a key value from the scope and provide useful information
* about what was found along the way.
*
* @param {can.Mustache.key} attr A dot seperated path. Use `"\."` if you have a property name that includes a dot.
* @param {can.view.Scope.readOptions} options that configure how this gets read.
*
* @return {{}}
*
* @option {Object} parent the value's immediate parent
*
* @option {can.Map|can.compute} rootObserve the first observable to read from.
*
* @option {Array<String>} reads An array of properties that can be used to read from the rootObserve to get the value.
*
* @option {*} value the found value
*/
read : function(attr, options){
// check if we should be running this on a parent.
if( attr.substr(0,3) === "../" ) {
return this._parent.read( attr.substr(3), options )
} else if(attr == ".."){
return {value: this._parent._context}
} else if(attr == "." || attr == "this"){
return {value: this._context};
}
// Split the name up.
var names = attr.indexOf('\\.') == -1
// Reference doesn't contain escaped periods
? attr.split('.')
// Reference contains escaped periods (`a.b\c.foo` == `a["b.c"].foo)
: getNames(attr),
namesLength = names.length,
j,
// The current context (a scope is just data and a parent scope).
context,
// The current scope.
scope = this,
// While we are looking for a value, we track the most likely place this value will be found.
// This is so if there is no me.name.first, we setup a listener on me.name.
// The most likely canidate is the one with the most "read matches" "lowest" in the
// context chain.
// By "read matches", we mean the most number of values along the key.
// By "lowest" in the context chain, we mean the closest to the current context.
// We track the starting position of the likely place with `defaultObserve`.
defaultObserve,
// Tracks how to read from the defaultObserve.
defaultReads = [],
// Tracks the highest found number of "read matches".
defaultPropertyDepth = -1,
// `scope.read` is designed to be called within a compute, but
// for performance reasons only listens to observables within one context.
// This is to say, if you have me.name in the current context, but me.name.first and
// we are looking for me.name.first, we don't setup bindings on me.name and me.name.first.
// To make this happen, we clear readings if they do not find a value. But,
// if that path turns out to be the default read, we need to restore them. This
// variable remembers those reads so they can be restored.
defaultComputeReadings,
// Tracks the default's scope.
defaultScope,
// Tracks the first found observe.
currentObserve,
// Tracks the reads to get the value for a scope.
currentReads;
// While there is a scope/context to look in.
while(scope){
// get the context
context = scope._context;
if (context != null) {
// Lets try this context
var data = Scope.read(context, names, can.simpleExtend({
// Called when an observable is found.
foundObservable: function(observe, nameIndex){
// Save the current observe.
currentObserve = observe;
currentReads = names.slice(nameIndex);
},
// Called when we were unable to find a value.
earlyExit: function(parentValue, nameIndex){
// If this has more matching values,
if(nameIndex > defaultPropertyDepth) {
// save the state.
defaultObserve = currentObserve;
defaultReads = currentReads;
defaultPropertyDepth = nameIndex;
defaultScope = scope;
// Clear and save readings so next attempt does not use these readings
defaultComputeReadings = can.__clearReading && can.__clearReading();
}
}
}, options));
// Found a matched reference.
if (data.value !== undefined ) {
return {
scope: scope,
rootObserve: currentObserve,
value: data.value,
reads: currentReads
};
}
}
// Prevent prior readings.
can.__clearReading && can.__clearReading();
// Move up to the next scope.
scope = scope._parent;
}
// If there was a likely observe.
if( defaultObserve ) {
// Restore reading for previous compute
can.__setReading && can.__setReading(defaultComputeReadings)
return {
scope: defaultScope,
rootObserve: defaultObserve,
reads: defaultReads,
value: undefined
}
} else {
// we found nothing and no observable
return {
names: names,
value: undefined
};
}
}
});
can.view.Scope = Scope;
return Scope;
});