// ========================================================================== // Project: SproutCore Costello - Property Observing Library // Copyright: ©2006-2009 Sprout Systems, Inc. and contributors. // Portions ©2008-2009 Apple, Inc. All rights reserved. // License: Licened under MIT license (see license.js) // ========================================================================== require('core') ; require('mixins/observable') ; require('mixins/array') ; /*globals $$sel */ SC.BENCHMARK_OBJECTS = NO; // .......................................................... // PRIVATE HELPER METHODS // // Private helper methods. These are not kept as part of the class // definition because SC.Object is copied frequently and we want to keep the // number of class methods to a minimum. /** @private Augments a base object by copying the properties from the extended hash. In addition to simply copying properties, this method also performs a number of optimizations that can make init'ing a new object much faster including: - concating concatenatedProperties - prepping a list of bindings, observers, and dependent keys - caching local observers so they don't need to be manually constructed. @param {Hash} base hash @param {Hash} extension @returns {Hash} base hash */ SC._object_extend = function _object_extend(base, ext) { if (!ext) return base; // nothing to do // set _kvo_cloned for later use base._kvo_cloned = null; // get some common vars var key, idx, len, cur, cprops = base.concatenatedProperties, K = SC.K ; var p1,p2; // first, save any concat props. use old or new array or concat idx = (cprops) ? cprops.length : 0 ; var concats = (idx>0) ? {} : null; while(--idx>=0) { key = cprops[idx]; p1 = base[key]; p2 = ext[key]; if (p1) { if (!(p1 instanceof Array)) p1 = SC.$A(p1); concats[key] = (p2) ? p1.concat(p2) : p2 ; } else { if (!(p2 instanceof Array)) p2 = SC.$A(p2); concats[key] = p2 ; } } // setup arrays for bindings, observers, and properties. Normally, just // save the arrays from the base. If these need to be changed during // processing, then they will be cloned first. var bindings = base._bindings, clonedBindings = NO; var observers = base._observers, clonedObservers = NO; var properties = base._properties, clonedProperties = NO; var paths, pathLoc, local ; // outlets are treated a little differently because you can manually // name outlets in the passed in hash. If this is the case, then clone // the array first. var outlets = base.outlets, clonedOutlets = NO ; if (ext.outlets) { outlets = (outlets || SC.EMPTY_ARRAY).concat(ext.outlets); clonedOutlets = YES ; } // now copy properties, add superclass to func. for(key in ext) { if (key === '_kvo_cloned') continue; // do not copy // avoid copying builtin methods if (!ext.hasOwnProperty(key)) continue ; // get the value. use concats if defined var value = (concats.hasOwnProperty(key) ? concats[key] : null) || ext[key] ; // Possibly add to a bindings. if (key.slice(-7) === "Binding") { if (!clonedBindings) { bindings = (bindings || SC.EMPTY_ARRAY).slice() ; clonedBindings = YES ; } if (bindings === null) bindings = (base._bindings || SC.EMPTY_ARRAY).slice(); bindings[bindings.length] = key ; // Also add observers, outlets, and properties for functions... } else if (value && (value instanceof Function)) { // add super to funcs. Be sure not to set the base of a func to // itself to avoid infinite loops. if (!value.superclass && (value !== (cur=base[key]))) { value.superclass = value.base = cur || K; } // handle regular observers if (value.propertyPaths) { if (!clonedObservers) { observers = (observers || SC.EMPTY_ARRAY).slice() ; clonedObservers = YES ; } observers[observers.length] = key ; // handle local properties } else if (paths = value.localPropertyPaths) { pathLoc = paths.length; while(--pathLoc >= 0) { local = base._kvo_for(SC.keyFor('_kvo_local', paths[pathLoc]), SC.Set); local.add(key); base._kvo_for('_kvo_observed_keys', SC.Set).add(paths[pathLoc]) ; } // handle computed properties } else if (value.dependentKeys) { if (!clonedProperties) { properties = (properties || SC.EMPTY_ARRAY).slice() ; clonedProperties = YES ; } properties[properties.length] = key ; // handle outlets } else if (value.autoconfiguredOutlet) { if (!clonedOutlets) { outlets = (outlets || SC.EMPTY_ARRAY).slice(); clonedOutlets = YES ; } outlets[outlets.length] = key ; } } // copy property base[key] = value ; } // copy bindings, observers, and properties base._bindings = bindings || []; base._observers = observers || [] ; base._properties = properties || [] ; base.outlets = outlets || []; // toString is usually skipped. Don't do that! if (ext.hasOwnProperty('toString')) base.toString = ext.toString; return base ; } ; /** @class Root object for the SproutCore framework. SC.Object is the root class for most classes defined by SproutCore. It builds on top of the native object support provided by JavaScript to provide support for class-like inheritance, automatic bindings, properties observers, and more. Most of the classes you define in your application should inherit from SC.Object or one of its subclasses. If you are writing objects of your own, you should read this documentation to learn some of the details of how SC.Object's behave and how they differ from other frameworks. h2. About SproutCore Classes JavaScript is not a class-based language. Instead it uses a type of inheritence inspired by self called "prototypical" inheritance. ... h2. Using SproutCore objects with other JavaScript object. You can create a SproutCore object just like any other object... obj = new SC.Object() ; @extends SC.Observable @author Charles Jolley @constructor @since SproutCore 1.0 */ SC.Object = function(props) { return this._object_init(props); }; SC.mixin(SC.Object, /** @scope SC.Object @static */ { /** Adds the passed properties to the object's class definition. You can pass as many hashes as you want, including Mixins, and they will be added in the order they are passed. This is a shorthand for calling SC.mixin(MyClass, props...); @params {Hash} props the properties you want to add. @returns {Object} receiver */ mixin: function(props) { var len = arguments.length, loc ; for(loc =0;loc YES ClassA.subclassOf(ClassA) => NO }}} @param {Class} scClass class to compare @returns {Boolean} */ subclassOf: function(scClass) { if (this === scClass) return NO ; var t = this ; while(t = t.superclass) if (t === scClass) return YES ; return NO ; }, /** Returns YES if the passed object is a subclass of the receiver. This is the inverse of subclassOf() which you call on the class you want to test. @param {Class} scClass class to compare @returns {Boolean} */ hasSubclass: function(scClass) { return (scClass && scClass.subclassOf) ? scClass.subclassOf(this) : NO; }, /** Returns YES if the receiver is the passed class or is a subclass of the passed class. Unlike subclassOf(), this method will return YES if you pass the receiver itself, since class is a kind of itself. See also subclassOf(). h2. Example {{{ ClassA = SC.Object.extend(); ClassB = ClassA.extend(); ClassB.kindOf(ClassA) => YES ClassA.kindOf(ClassA) => YES }}} @param {Class} scClass class to compare @returns {Boolean} */ kindOf: function(scClass) { return (this === scClass) || this.subclassOf(scClass) ; } }) ; // .......................................... // DEFAULT OBJECT INSTANCE // SC.Object.prototype = { /** @private This is the first method invoked on a new instance. It will first apply any added properties to the new instance and then calls the real init() method. @param {Array} extensions an array-like object with hashes to apply. @returns {Object} receiver */ _object_init: function(extensions) { // apply any new properties var idx, len = (extensions) ? extensions.length : 0; for(idx=0;idx "bar" }}} @param {Hash} ext a hash to copy. Only one. @returns {Object} receiver */ mixin: function() { var idx, len = arguments.length; for(idx=0;idx YES instB.instanceOf(ClassA) => NO }}} @param {Class} scClass the class @returns {Boolean} */ instanceOf: function(scClass) { return this.constructor === scClass ; }, /** Returns true if the receiver is an instance of the named class or any subclass of the named class. See also instanceOf(). h2. Example {{{ var ClassA = SC.Object.extend(); var ClassB = SC.Object.extend(); var instA = ClassA.create(); var instB = ClassB.create(); instA.kindOf(ClassA) => YES instB.kindOf(ClassA) => YES }}} @param scClass {Class} the class @returns {Boolean} */ kindOf: function(scClass) { return this.constructor.kindOf(scClass); }, /** @private */ toString: function() { if (!this._object_toString) { this._object_toString = "%@:%@".fmt(SC._object_className(this.constructor), SC.guidFor(this)); } return this._object_toString ; }, /** Activates any outlet connections in object and syncs any bindings. This method is called automatically for view classes but may be used for any object. @returns {void} */ awake: function(key) { this.outlets.forEach(function(key) { this.get(key); },this) ; this.bindings.invoke('sync'); }, /** Invokes the passed method or method name one time during the runloop. You can use this method to schedule methods that need to execute but may be too expensive to execute more than once, such as methods that update the DOM. @param {Funciton|String} method method or method name @returns {SC.Object} receiver */ invokeOnce: function(method) { SC.RunLoop.currentRunLoop.invokeOnce(this, method) ; return this ; }, /** Invokes the passed method once at the beginning of the next runloop, before any other methods (including events) are processed. This is useful for situations where you know you need to update something, but due to the way the run loop works, you can't actually do the update until the run loop has completed. A simple example is setting the selection on a collection controller to a newly created object. Because the collection controller won't have its content collection updated until later in the run loop, setting the selection immediately will have no affect. In this situation, you could do this instead: {{{ // Creates a new MyRecord object and sets the selection of the // myRecord collection controller to the new object. createObjectAction: function(sender, evt) { // create a new record and add it to the store var obj = MyRecord.newRecord() ; // update the collection controller's selection MyApp.myRecordCollectionController.invokeNext( function() { this.set('selection', [obj]) ; }); } }}} You can call invokeNext as many times as you like and the method will only be invoked once. @param {Funciton|String} method method or method name @returns {SC.Object} receiver */ invokeNext: function(method) { SC.RunLoop.currentRunLoop.invokeNext(this, method) ; return this ; }, /** The properties named in this array will be concatenated in subclasses instead of replaced. This allows you to name special properties that should contain any values you specify plus values specified by parents. It is used by SproutCore and is available for your use, though you should limit the number of properties you include in this list as it adds a slight overhead to new class and instance creation. @property */ concatenatedProperties: ['concatenatedProperties', 'initMixin', 'destroyMixin'] } ; // bootstrap the constructor for SC.Object. SC.Object.prototype.constructor = SC.Object; // Add observable to mixin SC.mixin(SC.Object.prototype, SC.Observable) ; // .......................................................... // CLASS NAME SUPPORT // /** @private This is a way of performing brute-force introspection. This searches through all the top-level properties looking for classes. When it finds one, it saves the class path name. */ function findClassNames() { if (SC._object_foundObjectClassNames) return ; SC._object_foundObjectClassNames = true ; var seen = [] ; var searchObject = function(root, object, levels) { levels-- ; // not the fastest, but safe if (seen.indexOf(object) >= 0) return ; seen.push(object) ; for(var key in object) { if (key == '__scope__') continue ; if (key == 'superclass') continue ; if (!key.match(/^[A-Z0-9]/)) continue ; var path = (root) ? [root,key].join('.') : key ; var value = object[key] ; switch(SC.typeOf(value)) { case SC.T_CLASS: if (!value._object_className) value._object_className = path; if (levels>=0) searchObject(path, value, levels) ; break ; case SC.T_OBJECT: if (levels>=0) searchObject(path, value, levels) ; break ; case SC.T_HASH: if (((root) || (path==='SC')) && (levels>=0)) searchObject(path, value, levels) ; break ; default: break; } } } ; searchObject(null, window, 2) ; // Internet Explorer doesn's loop over global variables... /*if ( SC.browser.isIE ) { searchObject('SC', SC, 2) ; // get names for the SC classes // get names for the model classes, including nested namespaces (untested) for ( var i = 0; i < SC.Server.servers.length; i++ ) { var server = SC.Server.servers[i]; if (server.prefix) { for (var prefixLoc = 0; prefixLoc < server.prefix.length; prefixLoc++) { var prefixParts = server.prefix[prefixLoc].split('.'); var namespace = window; var namespaceName; for (var prefixPartsLoc = 0; prefixPartsLoc < prefixParts.length; prefixPartsLoc++) { namespace = namespace[prefixParts[prefixPartsLoc]] ; namespaceName = prefixParts[prefixPartsLoc]; } searchObject(namespaceName, namespace, 2) ; } } } }*/ } /** @private Returns the name of this class. If the name is not known, triggers a search. This can be expensive the first time it is called. This method is used to allow classes to determine their own name. */ SC._object_className = function(obj) { if (!SC.isReady) return ''; // class names are not available until ready if (!obj._object_className) findClassNames() ; if (obj._object_className) return obj._object_className ; // if no direct classname was found, walk up class chain looking for a // match. var ret = obj ; while(ret && !ret._object_className) ret = ret.superclass; return (ret && ret._object_className) ? ret._object_className : 'Anonymous'; } ;