1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe 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 used for views that show a seperate editor view to edit.
 12   For example, the default behavior of SC.LabelView if isEditable is set
 13   to YES is to popup an SC.InlineTextFieldView when double clicked. This is a
 14   seperate text input that will handle the editing and save its value back to
 15   the label when it is finished.
 16 
 17   To use this functionality, all you have to do is apply this mixin to
 18   your view. You may define your own SC.InlineEditorDelegate to further
 19   customize editing behavior.
 20 
 21       MyProject.MyView = SC.View.extend(SC.InlineEditable, {
 22         inlineEditorDelegate: myDelegate
 23       });
 24 
 25   The delegate methods will default to your view unless the
 26   inlineEditorDelegate implements them. Simple views do not require a
 27   seperate delegate. If your view has a more complicated editing
 28   interaction, you may also implement a custom delegate. For example, if
 29   you have a form with several views that all edit together, you might
 30   set the parent view as the delegate so it can manage the lifecycle and
 31   layout of the editors.
 32 
 33   See SC.InlineEditorDelegate for more information on using a delegate to
 34   customize your view's edit behavior.
 35 
 36   Your view can now be edited by calling beginEditing() on it.
 37 
 38       myView.beginEditing();
 39 
 40   This will create an editor for the view. You can then end the editing process
 41   by calling commitEditing() or discardEditing() on either the view or the
 42   editor. commitEditing() will save the value and discard will revert to the
 43   original value.
 44 
 45       myView.commitEditing();
 46       myView.discardEditing();
 47 
 48   Note that the editor is a private property of the view, so the only views that
 49   should be able to access the methods on it are the editor itself, the view it
 50   is editing, and their delegates.
 51 */
 52 SC.InlineEditable = {
 53 
 54   /**
 55     Walk like a duck.
 56 
 57     @type Boolean
 58     @default YES
 59   */
 60   isInlineEditable: YES,
 61 
 62   /**
 63     Flag to enable or disable editing.
 64 
 65     @type Boolean
 66     @default YES
 67   */
 68   isEditable: YES,
 69 
 70   /**
 71     The view that will be used to edit this view. Defaults to
 72     SC.InlineTextFieldView, which is simply a text field that positions itself
 73     over the view being edited.
 74 
 75     @type SC.InlineEditor
 76     @default SC.InlineTextFieldView
 77   */
 78   exampleEditor: SC.InlineTextFieldView,
 79 
 80   /**
 81     Indicates whether the view is currently editing. Attempting to
 82     beginEditing a view that is already editing will fail.
 83 
 84     @type Boolean
 85     @default NO
 86   */
 87   isEditing: NO,
 88 
 89   /**
 90     Delegate that will be notified of events related to the editing
 91     process. Also responsible for managing the lifecycle of the editor.
 92 
 93     @type SC.InlineEditorDelegate
 94     @default SC.InlineTextFieldDelegate
 95   */
 96   inlineEditorDelegate: SC.InlineTextFieldDelegate,
 97 
 98   /**
 99     @private
100     The editor responsible for editing this view.
101   */
102   _editor: null,
103 
104   /**
105     Tells the view to start editing. This will create an editor for it
106     and transfer control to the editor.
107 
108     Will fail if the delegate returns NO to inlineEditorShouldBeginEditing.
109 
110     @returns {Boolean} whether the view successfully entered edit mode
111   */
112   beginEditing: function() {
113     var del;
114 
115     del = this.delegateFor('inlineEditorShouldBeginEditing', this.inlineEditorDelegate);
116     if(del && !del.inlineEditorShouldBeginEditing(this, this.get('value'))) return NO;
117 
118     this._editor = this.invokeDelegateMethod(this.inlineEditorDelegate, 'acquireEditor', this);
119 
120     if(this._editor) return this._editor.beginEditing(this);
121     else return NO;
122   },
123 
124   /**
125     Tells the view to save the value currently in the editor and finish
126     editing. The delegate will be consulted first by calling
127     inlineEditorShouldCommitEditing, and the operation will not be
128     allowed if the delegate returns NO.
129 
130     Will fail if the delegate returns NO to inlineEditorShouldCommitEditing.
131 
132     @returns {Boolean} whether the delegate allowed the value to be committed
133   */
134   commitEditing: function() {
135     return this._editor ? this._editor.commitEditing() : NO;
136   },
137 
138   /**
139     Tells the view to leave edit mode and revert to the value it had
140     before editing. May fail if the delegate returns NO to
141     inlineEditorShouldDiscardEditing. It is possible for the delegate to
142     return false to inlineEditorShouldDiscardEditing but true to
143     inlineEditorShouldCommitEditing, so a client view may attempt to
144     call commitEditing in case discardEditing fails.
145 
146     Will fail if the delegate returns NO to inlineEditorShouldDiscardEditing.
147 
148     @returns {Boolean} whether the delegate allowed the view to discard its value
149   */
150   discardEditing: function() {
151     return this._editor ? this._editor.discardEditing() : NO;
152   },
153 
154   /**
155     Allows the view to begin editing if it is editable and it is not
156     already editing.
157 
158     @returns {Boolean} if the view is allowed to begin editing
159   */
160   inlineEditorShouldBeginEditing: function() {
161     return !this.get('isEditing') && this.get('isEditable');
162   },
163 
164   // TODO: implement validator
165   /**
166     By default, the editor starts with the value of the view being edited.
167 
168     @params {SC.InlineEditable} editor the view being edited
169     @params {SC.InlineEditor} value the editor for the view
170     @params {Object} editable the initial value of the editor
171   */
172   inlineEditorWillBeginEditing: function(editor, value, editable) {
173     editor.set('value', this.get('value'));
174   },
175 
176   /**
177     Sets isEditing to YES once editing has begun.
178 
179     @params {SC.InlineEditable} the view being edited
180     @params {SC.InlineEditor} the editor for the view
181     @params {Object} the initial value of the editor
182   */
183   inlineEditorDidBeginEditing: function(editor, value, editable) {
184     this.set('isEditing', YES);
185   },
186 
187   /** @private
188     Calls inlineEditorWillEndEditing for backwards compatibility.
189 
190     @params {SC.InlineEditable} the view being edited
191     @params {SC.InlineEditor} the editor for the view
192     @params {Object} the initial value of the editor
193   */
194   inlineEditorWillCommitEditing: function(editor, value, editable) {
195     if(this.inlineEditorWillEndEditing) this.inlineEditorWillEndEditing(editor, value);
196   },
197 
198   /**
199     By default, commiting editing simply sets the value that the editor
200     returned and cleans up.
201 
202     @params {SC.InlineEditable} the view being edited
203     @params {SC.InlineEditor} the editor for the view
204     @params {Object} the initial value of the editor
205   */
206   inlineEditorDidCommitEditing: function(editor, value, editable) {
207     editable.setIfChanged('value', value);
208 
209     if(this.inlineEditorDidEndEditing) this.inlineEditorDidEndEditing(editor, value);
210 
211     this._endEditing();
212   },
213 
214   /**
215     Calls inlineEditorWillEndEditing for backwards compatibility.
216 
217     @params {SC.InlineEditable} the view being edited
218     @params {SC.InlineEditor} the editor for the view
219     @params {Object} the initial value of the editor
220   */
221   inlineEditorWillDiscardEditing: function(editor, editable) {
222     if(this.inlineEditorWillEndEditing) this.inlineEditorWillEndEditing(editor, this.get('value'));
223   },
224 
225   /**
226     Calls inlineEditorDidEndEditing for backwards compatibility and then
227     cleans up.
228 
229     @params {SC.InlineEditable} the view being edited
230     @params {SC.InlineEditor} the editor for the view
231     @params {Object} the initial value of the editor
232   */
233   inlineEditorDidDiscardEditing: function(editor, editable) {
234     if(this.inlineEditorDidEndEditing) this.inlineEditorDidEndEditing(editor, this.get('value'));
235 
236     this._endEditing();
237   },
238 
239   /**
240     @private
241     Shared code used to cleanup editing after both discarding and commiting.
242   */
243   _endEditing: function() {
244     // _editor may be null if we were called using the
245     // SC.InlineTextFieldView class methods
246     if(this._editor) {
247       this.invokeDelegateMethod(this.inlineEditorDelegate, 'releaseEditor', this._editor);
248       this._editor = null;
249     }
250 
251     this.set('isEditing', NO);
252   }
253 };
254 
255