// ======================================================================== // SproutCore // copyright 2006-2008 Sprout Systems, Inc. // ======================================================================== require('core') ; require('foundation/benchmark') ; require('mixins/observable') ; require('mixins/array') ; SC.BENCHMARK_OBJECTS = NO; /** @class Root Object for the SproutCore Framework h2. Class at a Glance Add a one or two paragraph description of the class here. This is the root object of the SproutCore framework. All other objects inherit from you. SC.Object among other things implements support for class inheritance, observers, and bindings. You should use SC.Object to create your own subclasses as well. h2. Overview Some overview information about the class should go here. Please see online documentation for more information about this. h2. Subclassing Notes I should be able to put a long list of things here I suppose. - Do bullet - points - work? {{{ // Also some sample code.goesHere() ; }}} @extends SC.Observable @author Charles Jolley @version 1.0 @since Version 1.0 */ SC.Object = function(noinit) { if (noinit === SC.Object._noinit_) return ; var ret = SC.Object._init.apply(this,$A(arguments)) ; return ret ; }; Object.extend(SC.Object, /** @scope SC.Object */ { _noinit_: '__noinit__', /** Add properties to a object's class definition. @params {Hash} props the properties you want to add @returns {void} */ mixin: function(props) { var ext = $A(arguments) ; for(var loc=0;loc= 0) return ; seen.push(object) ; for(var key in object) { if (key == '__scope__') continue ; if (key == '_type') continue ; if (!key.match(/^[A-Z0-9]/)) continue ; var path = (root) ? [root,key].join('.') : key ; var value = object[key] ; switch($type(value)) { case T_CLASS: if (!value._objectClassName) value._objectClassName = path; if (levels>=0) searchObject(path, value, levels) ; break ; case T_OBJECT: if (levels>=0) searchObject(path, value, levels) ; break ; case T_HASH: if (((root != null) || (path=='SC')) && (levels>=0)) searchObject(path, value, levels) ; break ; default: break; } } } ; searchObject(null, window, 2) ; }, toString: function() { return this.objectClassName(); }, // .......................................... // PROPERTY SUPPORT METHODS // // get the tuple for a property path (the object and key). tupleForPropertyPath: function(path,root) { if (path.constructor == Array) return path ; // * = the rest is a chained property. var parts = path.split('*') ; var key = null ; if (parts && parts.length > 1) { key = parts.pop(); path = parts.join('*') ; } // get object path. property is last part if * was nt found. parts = path.split('.') ; if (!key) key = parts.pop() ; // convert path to object. var obj = this.objectForPropertyPath(parts,root) ; return (obj && key) ? [obj,key] : null ; }, objectForPropertyPath: function(path,root) { var parts = (typeof(path) == "string") ? path.split('.') : path ; if (!root) root = window ; var key = parts.shift() ; while(key && root) { root = (root.get) ? root.get(key) : root[key]; key = parts.shift(); } return (parts.length > 0) ? undefined : root ; }, // .......................................... // INTERNAL SUPPORT METHODS // _init: function(extensions,type) { var ret = this ; for(var loc=0;loc 0) base.outlets = outlets ; //console.log('bindings: %@ -- observers: %@ -- properties: %@'.format(base._bindings,base._observers,base._properties)) ; return base ; }, // Returns true if the receiver is a subclass of the named class. If the // receiver is the class passed, this will return false. See kindOf(). subclassOf: function(scClass) { if (this == scClass) return false ; var t = this._type ; while(t) { if (t == scClass) return true ; t = t._type ; } return false ; }, // Returns true if the receiver is the passed class or is any subclass. kindOf: function(scClass) { if (this == scClass) return true ; return this.subclassOf(scClass); } }) ; SC.idt = { count: 0, t: 0.0, keys: 0, observers: 0, bindings: 0, pv: 0, observers_t: 0, bindings_t: 0, pv_t: 0, conf_t: 0, b1_t: 0, b2_t: 0, b3_t: 0, e_count: 0, e_t: 0, v_count: 0, v_t: 0, vc_t: 0 ,active: false } ; SC.report = function() { var c = SC.idt.count ; var e = SC.idt.e_count ; var v = SC.idt.v_count ; var ret = [] ; ret.push('CREATED: ' + c + ' (avg time: '+(Math.floor(SC.idt.t * 100 / c) / 100)+' msec)') ; ret.push('EXTENDED: ' + e + ' (avg time: '+(Math.floor(SC.idt.e_t * 100 / e) / 100)+' msec)') ; ret.push('AVG KEYS: ' + (Math.floor(SC.idt.keys * 100 / c) / 100)) ; ret.push('AVG OBSERVERS: ' + (Math.floor(SC.idt.observers * 100 / c) / 100) + ' ('+ (Math.floor(SC.idt.observers_t * 100 / c) / 100) + ' msec)') ; ret.push('AVG BINDINGS: ' + (Math.floor(SC.idt.bindings * 100 / c) / 100) + ' ('+ (Math.floor(SC.idt.bindings_t * 100 / c) / 100) + ' msec)') ; ret.push('AVG PV: ' + (Math.floor(SC.idt.pv * 100 / c) / 100) + ' ('+ (Math.floor(SC.idt.pv_t * 100 / c) / 100) + ' msec)') ; ret.push('AVG CONFIGURE OUTLETS: ' + (Math.floor(SC.idt.conf_t * 100 / c) / 100) + ' msec') ; ret.push('AVG B1: ' + (Math.floor(SC.idt.b1_t * 100 / c) / 100) + ' msec') ; ret.push('EXT: ' + SC.idt.ext_c + ' (avg time: ' + (Math.floor(SC.idt.ext_t * 100 / SC.idt.ext_c) / 100) + ' msec)') ; ret.push('VIEWS: ' + v + ' (avg time: ' + (Math.floor(SC.idt.v_t * 100 / v) / 100) + ' msec)') ; ret.push('VIEW CREATE: ' + (Math.floor(SC.idt.vc_t * 100 / v) / 100) + ' msec)') ; console.log(ret.join("\n")) ; return ret.join("\n") ; } ; // .......................................... // DEFAULT OBJECT INSTANCE // SC.Object.prototype = { /** Always YES since this is an object and not a class. */ isObject: true, /** Returns YES if the named value is an executable function. @param methodName {String} the property name to check @returns {Boolean} */ respondsTo: function( methodName ) { return !!(methodName && this[methodName] && ($type(this[methodName]) == T_FUNCTION)); }, /** If the passed property is a method, then it will be executed with the passed arguments. Otherwise, returns NO. @param methodName {String} the method name to try to perform. @param args {*arguments} arbitrary arguments to pass along to the method. @returns {Object} NO if method could not be performed or method result. */ tryToPerform: function( methodName, args ) { if ( !methodName ) return false; var args = $A(arguments); var name = args.shift(); if (this.respondsTo(name)) { return this[name].apply(this,args); } return false; }, /** this function is called automatically when a new object instance is created. You can use this to configure your child elements if you want. Be sure to invoke the base method. */ init: function() { // This keySource is used to fix a bug in FF that will not iterate through // keys you add to an HTMLElement. The key values are still accessible, // they just aren't visible to the for...in loop. viewType is the hash // of values that was applied to the HTMLElement, so its a 1:1 substitute. var keySource = this.viewType || this ; var loc ; var keys ; var key ; var value ; var r = SC.idt.active ; var idtStart ; var idtSt ; if (r) { SC.idt.count++; idtStart = new Date().getTime() ; } ; // Add Observers if (keys = keySource._observers) for(loc=0;loc content this[key] = this.bind(propertyKey, value) ; if (r) SC.idt.bindings_t += (new Date().getTime()) - idtSt ; } // Add Properties if (keys = keySource._properties) for(loc=0;loc 0)) { args = value.dependentKeys.slice() ; args.unshift(key) ; this.registerDependentKey.apply(this,args) ; } } // Call 'initMixin' methods to automatically setup modules. if (this.initMixin) { var inc = Array.from(this.initMixin) ; for(var idx=0; idx < inc.length; idx++) inc[idx].call(this); } if (r) { SC.idt.t += ((new Date().getTime()) - idtStart); } }, /** EXPERIMENTAL: You can use this to call super in any method. This currently does not work in some versions of Safari. Instead you should use: argments.callee.base.apply(this, arguments); to call super. @params args {*args} any arguments you want to pass along. @returns {void} */ $super: function(args) { var caller = SC.Object.prototype.$super.caller; if (!caller) throw "$super cannot determine the caller method" ; if (caller.base) caller.base.apply(this, arguments) ; }, /** Add passed properties to the object's class. @param props {Hash} properties to append. @returns {void} */ mixin: function() { return SC.Object.mixin.apply(this,arguments) ; }, /** Returns all the keys defined on this object, excluding any defined in parent classes unless you pass all. @param {Boolean} all OPTIONAL: if YES return all keys, NO return only keys belonging to object itself. Defaults to NO. */ keys: function(all) { var ret = []; for(var key in this) { if (all || ret.hasOwnProperty(key)) ret.push(key); }; return ret ; }, /** returns true if the receiver is an instance of the named class. See also kindOf(). @param {Class} scClass the class @returns {Boolean} */ instanceOf: function(scClass) { return this._type == scClass ; }, /** Returns true if the receiver is an instance of the named class or any subclass of the named class. See also instanceOf(). @param scClass {Class} the class @returns {Boolean} */ kindOf: function(scClass) { var t = this._type ; while(t) { if (t == scClass) return true ; t = t._type ; } return false ; }, /** @private */ toString: function() { if (!this.__toString) { this.__toString = "%@:%@".fmt(this._type.objectClassName(), this._guid); } return this.__toString ; }, // .......................................... // OUTLETS // /** Activates any outlet connections in the the object. This is called automatically for views typically. A view may contain outlets. Outlets are a way to find and connect to elements within the view. @param key {String} optional single key to awake. @returns {void} */ awake: function(key) { // if a key is passed, convert that from an outlet and awake it. otherwise // awake self. if (key !== undefined) { var obj = this.outlet(key) ; if (obj) obj.awake() ; return ; } if (this._awake) return ; this._awake = true ; // it would be cool to do this in a recursive way, but sadly we cannot // without a stack overflow problem. Just loop through outlets and collect // items to awake. this.bindings.invoke('relay') ; if (this.outlets && this.outlets.length) { var stack = [] ; var working = [this, this.outlets.slice()] ; while(working) { // find the next item to work on. var next = working[1].pop() ; var obj = working[0] ; // an item was found in the array. Process it. if (next) { next = obj[next] ; if (next) { // awake these bindings. if (next.bindings) next.bindings.invoke('relay') ; // next has outlets itself. Start a new context and process them. if (next.outlets && next.outlets.length > 0) { stack.push(working) ; working = [next,next.outlets.slice()] ; } } // no more items found in the current array. pop the stack. } else working = stack.pop() ; } } }, /** Array of outlets to awake automatically. If you have outlets defined on a class, add this array with their property names to have them awake automatically. This array is merged with the parent class outlet's array automatically when you call extend(). @type {Array} @field */ outlets: [], /** Just like get() except it will also create an outlet-packaged view if that is the value of the property. This method works just like get() except if the value of the property is an outlet-packaged View, the view will be created first. You can use this to create lazy outlets. @param {String} key The key to read as an outlet. @returns {Object} the value of the key, possibly an awakened outlet. */ outlet: function(key) { var value = this[key] ; // get the current value. // if its an outlet, then configure it first. if (value && (value instanceof Function) && value.isOutlet == true) { if (!this._originalOutlets) this._originalOutlets = {} ; this._originalOutlets[key] = value ; // create the outlet by calling the outlet function. this should be the owner view. value = value.call(this) ; this.set(key, value) ; } else if (typeof(value) == "string") { if (!this._originalOutlets) this._originalOutlets = {} ; this._originalOutlets[key] = value ; value = (this.$$sel) ? this.$$sel(value) : $$sel(value) ; if (value) value = (value.length > 0) ? ((value.length == 1) ? value[0] : value) : null ; this.set(key, value) ; } return value ; }, /** Invokes the named method after the specified period of time. This is a convenience method that will create a single run timer to invoke a method after a period of time. The method should have the signature: {{{ methodName: function(timer) }}} If you would prefer to pass your own parameters instead, you can instead call invokeLater() directly on the function object itself. @param interval {Number} period from current time to schedule. @param methodName {String} method name to perform. @returns {SC.Timer} scheduled timer. */ invokeLater: function(methodName, interval) { if (interval === undefined) interval = 1 ; var f = methodName ; if (arguments.length > 2) { var args =$A(arguments).slice(2,arguments.length); args.unshift(this); if ($type(f) === T_STRING) f = this[methodName] ; f = f.bind.apply(f, args) ; } return SC.Timer.schedule({ target: this, action: f, interval: interval }); }, _cprops: ['_cprops','outlets','_bindings','_observers','_properties', 'initMixin'] } ; Object.extend(SC.Object.prototype, SC.Observable) ; function logChange(target,key,value) { console.log("CHANGE: " + target + "["+key+"]=" + value) ; } // ........................................................................ // CHAIN OBSERVER // // ChainObservers are used to automatically monitor a property several // layers deep. // org.plan.name = SC._ChainObserver.create({ // target: this, property: 'org', // next: SC._ChainObserver.create({ // property: 'plan', // next: SC._ChainObserver.create({ // property: 'name', func: myFunc // }) // }) // }) // SC._ChainObserver = SC.Object.extend({ isChainObserver: true, // the object this link in the chain is observing. target: null, // the property on the object this link is observing property: null, // if not null, the next link in the chain. The target property // of this next link will be set to the value of this link. next: null, // if not null, the function to be called when the property value changes. func: null, propertyObserver: function(observing,target,key,value) { if ((key == 'target') && (value != this._target)) { var func = this.boundObserver() ; if (this._target && this._target.removeObserver) { this._target.removeObserver(this.property,func); } this._target = value ; if (this._target && this._target.addObserver) { this._target.addObserver(this.property,func) ; } // the target property has probably changed since the target has // changed. notify myself of this. if (!(observing == 'init')) this.targetPropertyObserver() ; } }, boundObserver: function() { if (!this._boundObserver) { this._boundObserver = this.targetPropertyObserver.bind(this) ; } return this._boundObserver ; }, // invoked when the target property changes. act based targetPropertyObserver: function() { // get the new target property value. if the target or property is // not valid, just use null. var value = (this.target && this.target.get && this.property) ? this.target.get(this.property) : null ; if (value !== this._lastTargetProperty) { this._lastTargetProperty = value ; // if we have another item in the chain, just forward the change. if (this.next) { this.next.set('target',value) ; // otherwise, invoke the function. } else if (this.func) this.func(this.target,this.property,value) ; } }, // hookup observer. init: function() { arguments.callee.base.call(this) ; this.propertyObserver('init',this,'target',this.get('target')) ; } }) ; // Class Methods. SC._ChainObserver.mixin({ createChain: function(target,keys,func) { var property = keys.shift() ; var nextTarget = (target && property && target.get) ? target.get(property) : null ; var next = (keys && keys.length>0) ? this.createChain(nextTarget,keys,func) : null ; return this.create({ target: target, property: property, next: next, func: ((next) ? null : func) }) ; } }) ;