// ========================================================================
// SproutCore
// copyright 2006-2007 Sprout Systems, Inc.
// ========================================================================
require('foundation/object') ;
/**
@class SC.Controller
The controller base class provides some common functions you will need
for controllers in your applications, especially related to maintaining
an editing context.
In general you will not use this class, but you can use a subclass such
as ObjectController, CollectionController, or ArrayController.
h2. EDITING CONTEXTS
One major function of a controller is to mediate between changes in the
UI and changes in the model. In particular, you usually do not want
changes you make in the UI to be applied to a model object directly.
Instead, you often will want to collect changes to an object and then
apply them only when the user is ready to commit their changes.
The editing contact support in the controller class will help you
provide this capability.
@extends SC.Object
*/
SC.Controller = SC.Object.extend(
/** @scope SC.Controller.prototype */
{
/**
The controller will set this property to true whenever there are
changes that need to be committed. This property is true whenever
the controller itself has uncommitted changes or when any dependent
editors have uncommitted changes. In your own subclass, call
this.objectDidChange(this) to register changes.
@type Boolean
*/
hasChanges: false,
/**
This is the controller's parent controller usually. The controller will
notify this controller when its changes are committed or discarded.
@type SC.Controller
*/
context: null,
/**
If this is false, then the controller will only commit changes when you
explicitly call commitChanges. Otherwise it will commit them
immediately. You usually want this set to false. It is initially set to
true for compatibility.
@type Boolean
*/
commitChangesImmediately: true,
/**
* Sets the commitChangesImmediately to the parent context's value if a context was passed.
* The Controller also observes changes to the context property and adjusts the commitChangesImmediately prop
*/
init: function()
{
arguments.callee.base.apply(this,arguments);
this._contextObserver();
},
/**
* @private
*/
_contextObserver: function()
{
if ( this.context )
{
// inherit the parent contexts inherit property
this.commitChangesImmediately = this.context.commitChangesImmediately;
}
}.observes('context'),
/**
If the controller has uncommitted changes, call this method to
commit them. This method will commit the changes for any dependent
editors as well. This will return true if the commit completed and
false or an error object if it failed.
*/
commitChanges: function() {
this._commitTimeout = null ; // clear timeout
var ret = this._canCommitChanges() ;
if (!$ok(ret)) return ret ;
return this._performCommitChanges() ;
},
/**
If this controller has uncommitted changes that you do not want to keep,
call this method to discard them. This method will also discard
changes for any dependent editors as well.
*/
discardChanges: function() {
var ret = this._canDiscardChanges() ;
if (!$ok(ret)) return ret ;
return this._performDiscardChanges() ;
},
/**
This method will return an appropriate controller object for the
value of the property you name. This will return one of:
Value Type | Returns |
Array-compatible | SC.ArrayController |
SC.Collection | SC.CollectionController |
Kind of SC.Object | SC.ObjectController |
other | value |
This is a helper method used by subclasses to create the appropriate
type of controller.
*/
controllerForValue: function(value) {
var ret = null ;
switch($type(value)) {
case T_OBJECT:
if (value.kindOf(SC.Collection)) {
ret = SC.CollectionController ;
} else ret = SC.ObjectController ;
break ;
case T_ARRAY:
ret = SC.ArrayController ;
break ;
default:
ret = null ;
}
return (ret) ? ret.create({ content: value, context: this }) : value;
},
/**
Call this method whenever you have uncommitted changes. This will
handle notifying your parent context as well.
@param {SC.Controller} editor
This is the object that has uncommitted changes. Normally you should
not pass a value. If you do pass an object, then that object will
become a dependent editor of the receiver.
*/
editorDidChange: function(editor) {
if (!editor) editor = this ; // set default value
// if this is another editor, add it to the list of editors that need
// to be notified of a change.
if (editor != this) {
if (!this._dirtyEditors) this._dirtyEditors = SC.Set.create();
this._dirtyEditors.add(editor) ;
} else {
this._hasLocalChanges = true ;
}
if (!this.get('hasChanges')) {
this.set('hasChanges', true) ;
// if we have a parent context notify them
if (this.context) {
this.context.editorDidChange(this) ;
// otherwise, if commit changes immediately is true, schedule commit.
// commit is only done once per cycle so that at least all the
// changes you might make at one time will be batched.
} else if (this.get('commitChangesImmediately')) {
if (!this._commitTimeout) {
this._commitTimeout = this.commitChanges.bind(this).defer();
}
}
}
},
/**
Call this method when your object no longer has uncommitted changes.
This will clear your hasChanges property and notify your parent context.
This is called automatically whenever changes are committed or discarded
on your controller.
*/
editorDidClearChanges: function(editor) {
if (!editor) editor = this ; // set default value
if (editor != this) {
// if we are currently clearing changes, then we will clean up the
// hasChanges state and dirtyeditors in bulk when this is all done.
// so do nothing.
if (this._clearingChanges) return ;
if (this._dirtyEditors) this._dirtyEditors.remove(editor) ;
} else {
this._hasLocalChanges = false ;
}
// _dirtyEditors may be undefined so use !! to force this to a bool value.
var hasChanges = !!(this._hasLocalChanges || (this._dirtyEditors && this._dirtyEditors.length > 0)) ;
if (this.get('hasChanges') != hasChanges) {
this.set('hasChanges', hasChanges) ;
if (this.context) this.context.editorDidClearChanges(editor) ;
}
},
/**
Override this method to determine if your controller can commit the
changes. This should validate your changes. Return false or an error
object if you cannot commit the change. This method will not be called
unless hasChanges is true and all your dependent editors are return
true as well.
*/
canCommitChanges: function() {
return true ;
},
/**
Override this method to actually commit the changes for your controller.
This will only be called if all controllers indicate that they can
commit. Return true if you succeeded or false or an error if you failed.
*/
performCommitChanges: function() {
return $error('performCommitChanges is not implemented') ;
},
/**
Override this method to determine if your controller can discard the
changes it has built up. This method will not be called unless you
have set hasChanges to true. Return false or an error object if you
cannot discard the change.
*/
canDiscardChanges: function() {
return true ;
},
/**
Override this method to actually discard the changes for your controller.
This will only be called if all controllers indicate that they can discard
their changes. Return true if you succeed or false or an error if you
failed.
*/
performDiscardChanges: function() {
return $error('performDiscardChanges is not implemented');
},
// ....................................
// PRIVATE
_canCommitChanges: function() {
if (!this.get('hasChanges')) return false ;
// validate editors.
var ret = true ;
if (this._dirtyEditors) {
ret = this._dirtyEditors.invokeWhile(true, '_canCommitChanges') ;
if (!$ok(ret)) return ret ;
}
// then validate receiver
return this.canCommitChanges() ;
},
_performCommitChanges: function() {
if (!this.get('hasChanges')) return true ;
// first commit any editors. If not successful, return. otherwise,
// clear editors.
var ret = true ;
if (this._dirtyEditors) {
this._clearingChanges = true ;
ret = this._dirtyEditors.invokeWhile(true, '_performCommitChanges') ;
this._clearingChanges = false ;
if ($ok(ret)) {
this._dirtyEditors = null ;
} else return ret ;
}
// now commit changes for the receiver.
ret = this.performCommitChanges() ;
if ($ok(ret)) this.editorDidClearChanges() ;
return ret ;
},
_canDiscardChanges: function() {
if (!this.get('hasChanges')) return false ;
// validate editors.
var ret = true ;
if (this._dirtyEditors) {
ret = this._dirtyEditors.invokeWhile(true, '_canDiscardChanges') ;
if (!$ok(ret)) return ret ;
}
// then validate receiver
return this.canDiscardChanges() ;
},
_performDiscardChanges: function() {
if (!this.get('hasChanges')) return true ;
// first discard changes for any editors. If not successful, return.
// otherwise, clear editors.
var ret = true ;
if (this._dirtyEditors) {
this._clearingChanges = true ;
ret = this._dirtyEditors.invokeWhile(true, '_performDiscardChanges') ;
this._clearingChanges = false ;
if ($ok(ret)) {
this._dirtyEditors = null ;
} else return ret ;
}
// now discard changes for the receiver.
ret = this.performDiscardChanges() ;
if ($ok(ret)) this.editorDidClearChanges() ;
return ret ;
}
}) ;