// ======================================================================== // SproutCore // copyright 2006-2007 Sprout Systems, Inc. // ======================================================================== require('controllers/controller') ; require('foundation/array') ; /** @class An Array Controller provides a way to project the contents of an array out to a view. You can use this object any place you might use an array. Changes to the array will not propogate to the content array until you call commitChanges(). @extends SC.Controller @extends SC.Array */ SC.ArrayController = SC.Controller.extend(SC.Array, /** @scope SC.ArrayController.prototype */ { /** * The array content that is being managed by the controller. * @property * @type {Array} */ content: function( key, value ) { if (!this._content) this._content = []; if (value !== undefined) { // only allow arrays... or array-like objects... this._content = ($type(value) == T_ARRAY) || (value && value.objectAt) ? value : []; } return this._content; }.property(), /** * Watches changes to the content property updates the contentClone. * @private * @observes content **/ _contentObserver: function() { this.contentCloneReset(); }.observes('content'), /** * A log of calls to the replace method that will be played back to the content property on commit. * @private * @property * @type {SC.Array} */ changelog: function( key, value ) { if (!this._changelog) this._changelog = []; if (value !== undefined) this._changelog = value; return this._changelog; }.property(), /** * A collection of all the objects that have been removed using removeObject. * On commit, they are deleted. * @private * @property * @type {SC.Array} */ deletions: function( key, value ) { if (!this._deletions) this._deletions = []; if (value !== undefined) this._deletions = value; return this._deletions; }.property(), /** * The array content that (when committed) will be merged back into the content property. * All array methods will take place on this object. * @private * @property * @type {SC.Array} */ contentClone: function( key, value ) { if (value !== undefined) { this._contentClone = value; this.arrayContentDidChange(); } return this._contentClone; }.property(), /** * Clones the content property into the contentClone property. * @private **/ contentCloneReset: function() { this.set('changelog', []); this.set('deletions', []); this.set('contentClone', null); }, /** * SC.Array interface implimentation. * * @param {Number} idx * Starting index in the array to replace. If idx >= length, then append to * the end of the array. * * @param {Number} amt * Number of elements that should be removed from the array, starting at * *idx*. * * @param {Array} objects * An array of zero or more objects that should be inserted into the array at * *idx* */ replace: function(idx, amt, objects) { if (this.get('contentClone') == undefined) this.set('contentClone', this.get('content').clone()); for (var i=0; i < amt; i++) { this.get('deletions').push( this._getSourceContent().objectAt(idx + i) ); } this.get('changelog').push({ idx: idx, amt: amt, objects: objects }); this.get('contentClone').replace(idx, amt, objects); for (var i=idx, n=idx+amt; i < n; i++) { if ( this._objControllers && this._objControllers[i] ) { this._objControllers[i] = null; } } this.editorDidChange() ; this.arrayContentDidChange(); return this; }, /** * SC.Array interface implimentation. * * @param {Number} idx * The index of the item to return. If idx exceeds the current length, * return null. */ objectAt: function(idx) { var obj = this._getSourceContent().objectAt(idx); if ( !this._objControllers ) this._objControllers = []; if ( !this._objControllers[idx] ) this._objControllers[idx] = this.controllerForValue(obj); return this._objControllers[idx]; }, /** * SC.Array interface implimentation. * @property * @type {integer} */ length: function( key, value ) { return this._getSourceContent().get('length'); }.property(), indexOf: function( obj ) { return this._getSourceContent().indexOf(obj); }, _getSourceContent: function() { return this.get('contentClone') || this.get('content'); }, /** * @private */ performCommitChanges: function() { var content = this.get('content'); var ret = true; // cannot commit changes to null content. Return an error. if (!content) { return $error("No Content"); } if (content.beginPropertyChanges) content.beginPropertyChanges(); // apply all the changes made to the clone this.get('changelog').each(function(change) { content.replace( change.idx, change.amt, change.objects ); }); this.get('deletions').each(function(obj) { if (obj.instanceOf(SC.ObjectController)) obj = obj.get('content'); if (obj && obj.destroy) { obj.destroy(); } }); if (content.endPropertyChanges) content.endPropertyChanges(); if (content.commitChanges) ret = content.commitChanges(); if ($ok(ret)) { this.contentCloneReset(); this.editorDidClearChanges(); } return ret; }, /** * @private */ performDiscardChanges: function() { this.contentCloneReset(); this.editorDidClearChanges(); return true; } });