1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2010 Sprout Systems, Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /**
  9   @namespace
 10 
 11   This mixin is for a view that is editable but is acting as a proxy to
 12   edit another view. For example, a calendar picker that pops up over
 13   top of a date display.
 14 
 15   Instantiation and destruction will be handled by the InlineEditorDelegate
 16   defined on the InlineEditable view.
 17 
 18   Any runtime configuration should be handled by the delegate's
 19   inlineEditorWillBeginEditing method. This will be done at the start of the
 20   beginEditing function, so your view should be able to handle having value
 21   changed by this method.
 22 
 23   Your view should also be sure to cleanup completely when editing is finished,
 24   because the delegate may decide to reuse the same editor elsewhere.
 25 
 26   See SC.InlineTextFieldView for an example of an implementation of
 27   SC.InlineEditor.
 28 */
 29 SC.InlineEditor = {
 30 
 31   /**
 32     Walk like a duck.
 33 
 34     @type Boolean
 35     @default YES
 36     @readOnly
 37   */
 38   isInlineEditor: YES,
 39 
 40   /**
 41     Indicates the view is currently editing. For a typical editor, this
 42     will always be true as long as the view exists, but some
 43     InlineEditorDelegates may have more complex lifecycles in which
 44     editors are reused.
 45 
 46     @type Boolean
 47     @default NO
 48   */
 49   isEditing: NO,
 50 
 51   /**
 52     The delegate responsible for the editors lifecycle as well as the
 53     target for notifications. This property should be set when the
 54     delegate creates the editor and does not need to be in the view
 55     definition.
 56 
 57     @type SC.InlineEditorDelegate
 58     @default null
 59   */
 60   inlineEditorDelegate: null,
 61 
 62   /**
 63     @private
 64     The view that this view is responsible for editing.
 65     @type SC.InlineEditable
 66   */
 67   _target: null,
 68 
 69   /**
 70     Tells the editor to begin editing another view with the given starting value.
 71     Editors may be reused so make sure that the editor is fully cleaned
 72     up and reinitialized.
 73 
 74     Sets isEditing to YES.
 75 
 76     Will fail if the editor is already editing.
 77 
 78     If you override this method, be sure to call sc_super() at the beginning of
 79     you function so that the delegate will be able to configure the view when it
 80     is notified of the inlineEditorWillBeginEditing event.
 81 
 82     @param {SC.View} editable the view being edited
 83     @returns {Boolean} whether the editor was able to successfully begin editing
 84   */
 85   beginEditing:function(editable) {
 86     if(this.get('isEditing') || !editable || !editable.isInlineEditable) return NO;
 87 
 88     var del, target;
 89 
 90     target = this._target = editable;
 91 
 92     del = this.delegateFor('inlineEditorWillBeginEditing', this.inlineEditorDelegate, target);
 93     if(del) del.inlineEditorWillBeginEditing(this, this.get('value'), target);
 94 
 95     this.set('isEditing', YES);
 96 
 97     // needs to be invoked last because it needs to run after the view becomes
 98     // first responder
 99     this.invokeLast(this._callDidBegin);
100 
101     // remember that we invoked in case commit gets called before the invoke
102     // goes off
103     this._didBeginInvoked = YES;
104 
105     return YES;
106   },
107 
108   /**
109     @private
110     Notifies the delegate of the didBeginEditing event. Needs to be invoked last
111     because becoming first responder doesn't happen until the end of the runLoop
112     and didBegin is supposed to occur after the editor becomes first responder.
113   */
114   _callDidBegin: function() {
115     // don't notify if we already ended editing
116     if(!this.get('isEditing')) return NO;
117 
118     this._didBeginInvoked = NO;
119 
120     var target = this._target, del;
121 
122     del = this.delegateFor('inlineEditorDidBeginEditing', this.inlineEditorDelegate, target);
123     if(del) del.inlineEditorDidBeginEditing(this, this.get('value'), target);
124   },
125 
126   /**
127     Tells the editor to save its value back to its target view and end
128     editing. Since the editor is a private property of the view it is
129     editing for, this function should only be called from the editor
130     itself. For example, you may want your editor to handle the enter
131     key by calling commitEditing on itself.
132 
133     Will fail if the editor is not editing or if the delegate returns NO to
134     inlineEditorShouldCommitEditing.
135 
136     @returns {Boolean} whether the editor was allowed to successfully commit its value
137   */
138   commitEditing:function() {
139     if(!this.get('isEditing')) return NO;
140 
141     // if the handler was invoked but never went off, call it now
142     if(this._didBeginInvoked) this._callDidBegin();
143 
144     var del, target = this._target;
145 
146     del = this.delegateFor('inlineEditorShouldCommitEditing', this.inlineEditorDelegate, target);
147     if(del && !del.inlineEditorShouldCommitEditing(this, this.get('value'), target)) return NO;
148 
149     del = this.delegateFor('inlineEditorWillCommitEditing', this.inlineEditorDelegate, target);
150     if(del) del.inlineEditorWillCommitEditing(this, this.get('value'), target);
151 
152     this._endEditing();
153 
154     del = this.delegateFor('inlineEditorDidCommitEditing', this.inlineEditorDelegate, target);
155     if(del) del.inlineEditorDidCommitEditing(this, this.get('value'), target);
156 
157     return YES;
158   },
159 
160   /**
161     Tells the editor to discard its value and end editing. Like
162     commitEditing, this should only be called by other methods of the
163     editor. For example, the handle for the escape key might call
164     discardEditing.
165 
166     Will fail if the editor is not editing or if the delegate returns NO to
167     inlineEditorShouldDiscardEditing.
168 
169     @returns {Boolean} whether the editor was allowed to discard its value
170   */
171   discardEditing:function() {
172     if(!this.get('isEditing')) return NO;
173 
174     // if the handler was invoked but never went off, call it now
175     if(this._didBeginInvoked) this._callDidBegin();
176 
177     var del, target = this._target;
178 
179     del = this.delegateFor('inlineEditorShouldDiscardEditing', this.inlineEditorDelegate, target);
180     if(del && !del.inlineEditorShouldDiscardEditing(this, target)) return NO;
181 
182     del = this.delegateFor('inlineEditorWillDiscardEditing', this.inlineEditorDelegate, target);
183     if(del) del.inlineEditorWillDiscardEditing(this, target);
184 
185     this._endEditing();
186 
187     del = this.delegateFor('inlineEditorDidDiscardEditing', this.inlineEditorDelegate, target);
188     if(del) del.inlineEditorDidDiscardEditing(this, target);
189 
190     return YES;
191   },
192 
193   /**
194     @private
195     Performs the cleanup functionality shared between discardEditing and
196     commitEditing.
197   */
198   _endEditing: function() {
199     this.set('isEditing', NO);
200     this._target = null;
201   }
202 
203 };
204