1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('views/text_field') ;
  9 sc_require('system/utils/misc') ;
 10 sc_require('delegates/inline_text_field');
 11 sc_require('mixins/inline_editor');
 12 
 13 /**
 14   @class
 15 
 16   The inline text editor is used to display an editable area for controls
 17   that are not always editable such as label views and source list views.
 18 
 19   You generally will not use the inline editor directly but instead will
 20   invoke beginEditing() and endEditing() on the views you are
 21   editing. If you would like to use the inline editor for your own views,
 22   you can do that also by using the editing API described here.
 23 
 24   ## Using the Inline Editor in Your Own Views
 25 
 26   To use the inlineEditor on a custom view you should mixin SC.InlineEditable on
 27   it. SC.InlineTextFieldView is the default editor so you do not need to do any
 28   other setup. The class methods beginEditing, commitEditing, and discardEditing
 29   still exist for backwards compatibility but should not be used on new views.
 30 
 31       MyProject.MyView = SC.View.extend(SC.InlineEditable, {
 32       });
 33 
 34   ### Starting the Editor
 35 
 36   The inline editor works by positioning itself over the top of your view
 37   with the same offset, width, and font information.
 38 
 39   To start it simply call beginEditing on your view.
 40 
 41       myView.beginEditing();
 42 
 43   By default, if the inline editor is currently in use elsewhere, it will automatically
 44   close itself over there and begin editing for your view instead. This behavior
 45   is defined by the inlineEditorDelegate of your view, and can be changed by using
 46   one other than the default.
 47 
 48   ## Customizing the editor
 49 
 50   The editor has several parameters that can be used to customize it to your
 51   needs. These options should be set on the editor passed to your delegate's (or
 52   view's) inlineEditorWillBeginEditing method:
 53 
 54    - `exampleFrame` -- The editors initial frame in viewport coordinates.
 55    - `value` -- Initial value of the edit field.
 56    - `exampleElement` -- A DOM element to use when copying styles.
 57    - `multiline` -- If YES then the hitting return will add to the value instead
 58      of exiting the inline editor.
 59    - `commitOnBlur` -- If YES then blurring will commit the value, otherwise it
 60      will discard the current value.  Defaults to YES.
 61    - `validator` -- Validator to be attached to the field.
 62 
 63   For backwards compatibility, calling the class method beginEditing with an
 64   options hash will translate the values in the hash to the correct settings on
 65   the editor.
 66 
 67   ## Committing or Discarding Changes
 68 
 69   Normally the editor will automatically commit or discard its changes
 70   whenever the user exits the edit mode by pressing enter, escape, or clicking
 71   elsewhere on the page. If you need to force the editor to end editing, you can
 72   do so by calling commitEditing() or discardEditing():
 73 
 74       myView.commitEditing();
 75       myView.discardEditing();
 76 
 77   Both methods will try to end the editing context and will call the
 78   relevant delegate methods on the inlineEditorDelegate set on your view.
 79 
 80   Note that it is possible an editor may not be able to commit editing
 81   changes because either the delegate disallowed it or because its validator
 82   failed.  In this case commitEditing() will return NO.  If you want to
 83   end editing anyway, you can discard the editing changes instead by calling
 84   discardEditing().  This method will generally succeed unless your delegate
 85   refuses it as well.
 86 
 87   @extends SC.TextFieldView
 88   @since SproutCore 1.0
 89 */
 90 SC.InlineTextFieldView = SC.TextFieldView.extend(SC.InlineEditor,
 91 /** @scope SC.InlineTextFieldView.prototype */ {
 92   classNames: ['inline-editor'],
 93 
 94   /**
 95     Over-write magic number from SC.TextFieldView
 96   */
 97   _topOffsetForFirefoxCursorFix: 0,
 98 
 99   /**
100     The default size of the inline text field is 0 x 0 so that when it is
101     appended, but before it is positioned it doesn't fill the parent view
102     entirely.
103 
104     This is important, because if the parent view layer allows overflow,
105     we could inadvertently alter the scrollTop or scrollLeft properties
106     of the layer.
107     */
108   layout: { height: 0, width: 0 },
109 
110   /*
111     @private
112 
113     Prevents the view from taking part in child view layout plugins.
114   */
115   useAbsoluteLayout: YES,
116 
117   /*
118   * @private
119   * @method
120   *
121   * Scans the given element for presentation styles from css.
122   *
123   * @params {element} the dom element to scan
124   * @returns {String} a style string that was copied from the element
125   */
126   _updateViewStyle: function(el) {
127     var styles = '',
128         s=SC.getStyle(el,'font-size');
129 
130     if(s && s.length>0) styles = styles + "font-size: "+ s + " !important; ";
131 
132     s=SC.getStyle(el,'font-family');
133     if(s && s.length>0) styles = styles + "font-family: " + s + " !important; ";
134 
135     s=SC.getStyle(el,'font-weight');
136     if(s && s.length>0) styles = styles + "font-weight: " + s + " !important; ";
137 
138     s=SC.getStyle(el,'z-index');
139     if(s && s.length>0) styles = styles + "z-index: " + s + " !important; ";
140 
141     s=SC.getStyle(el,'line-height');
142     if(s && s.length>0) styles = styles + "line-height: " + s + " !important; ";
143 
144     s=SC.getStyle(el,'text-align');
145     if(s && s.length>0) styles = styles + "text-align: " + s + " !important; ";
146 
147     s=SC.getStyle(el,'top-margin');
148     if(s && s.length>0) styles = styles + "top-margin: " + s + " !important; ";
149 
150     s=SC.getStyle(el,'bottom-margin');
151     if(s && s.length>0) styles = styles + "bottom-margin: " + s + " !important; ";
152 
153     s=SC.getStyle(el,'left-margin');
154     if(s && s.length>0) styles = styles + "left-margin: " + s + " !important; ";
155 
156     s=SC.getStyle(el,'right-margin');
157     if(s && s.length>0) styles = styles + "right-margin: " + s + " !important; ";
158 
159     return styles;
160   },
161 
162   /*
163   * @private
164   * @method
165   *
166   * Scans the given element for positioning styles from css.
167   *
168   * @params {element} the dom element to scan
169   * @returns {String} a style string copied from the element
170   */
171   _updateViewPaddingStyle: function(el) {
172     var styles = '',
173     s=SC.getStyle(el,'padding-top');
174 
175     if(s && s.length>0) styles = styles + "top: "+ s + " !important; ";
176 
177     s=SC.getStyle(el,'padding-bottom');
178     if(s && s.length>0) styles = styles + "bottom: " + s + " !important; ";
179 
180     s=SC.getStyle(el,'padding-left');
181     if(s && s.length>0) styles = styles + "left: " + s + " !important; ";
182 
183     s=SC.getStyle(el,'padding-right');
184     if(s && s.length>0) styles = styles + "right: " + s + " !important; ";
185 
186     return styles;
187 	},
188 
189   /*
190   * @private
191   * @method
192   *
193   * Scans the given element for styles and copies them into a style element in
194   * the head. This allows the styles to be overridden by css matching classNames
195   * on the editor.
196   *
197   * @params {element} the dom element to copy
198   */
199 	updateStyle: function(exampleElement) {
200     if(exampleElement.length) exampleElement = exampleElement[0];
201 
202     // the styles are placed into a style element so that they can be overridden
203     // by your css based on the editor className
204     var styleElement = document.getElementById('sc-inline-text-field-style'),
205 		s = this._updateViewStyle(exampleElement),
206 		p = this._updateViewPaddingStyle(exampleElement),
207 
208 		str = ".inline-editor input{"+s+"}" +
209 					".inline-editor textarea{"+s+"}" +
210 					".inline-editor .padding{"+p+"}";
211 
212     // the style element is lazily created
213     if(!styleElement) {
214       var head = document.getElementsByTagName('head')[0];
215       styleElement = document.createElement('style');
216 
217       styleElement.type= 'text/css';
218       styleElement.media= 'screen';
219       styleElement.id = 'sc-inline-text-field-style';
220 
221       head.appendChild(styleElement);
222     }
223 
224     // now that we know the element exists, write the styles
225 
226     // IE method
227     if(styleElement.styleSheet) styleElement.styleSheet.cssText= str;
228     // other browsers
229     else styleElement.innerHTML = str;
230 	},
231 
232   /*
233   * @method
234   *
235   * Positions the editor over the target view.
236   *
237   * If you want to tweak the positioning of the editor, you may pass a custom
238   * frame for it to position itself on.
239   *
240   * @param {SC.View} the view to be positioned over
241   * @param {Hash} optional custom frame
242   * @param {Boolean} if the view is a member of a collection
243   */
244 	positionOverTargetView: function(target, exampleFrame, elem, _oldExampleFrame, _oldElem) {
245     var targetLayout = target.get('layout'),
246         layout = {};
247 
248     // Deprecates isCollection and pane arguments by fixing them up if they appear.
249     if (!SC.none(_oldExampleFrame)) {
250       exampleFrame = _oldExampleFrame;
251       elem = _oldElem;
252 
253       // @if(debug)
254       SC.warn("Developer Warning: the isCollection and pane arguments have been deprecated and can be removed.  The inline text field will now position itself within the same parent element as the target, thus removing the necessity to calculate the position of the target relative to the pane.");
255       // @endif
256     }
257 
258     // In case where the label is part of an SC.ListItemView
259     if (exampleFrame && elem) {
260       var frame = SC.offset(elem, 'parent');
261 
262       layout.top = targetLayout.top + frame.y - exampleFrame.height/2;
263       layout.left = targetLayout.left + frame.x;
264       layout.height = exampleFrame.height;
265       layout.width = exampleFrame.width;
266     } else {
267       layout = SC.copy(targetLayout);
268     }
269 
270     this.set('layout', layout);
271   },
272 
273   /*
274   * Flag indicating whether the editor is allowed to use multiple lines.
275   * If set to yes it will be rendered using a text area instead of a text input.
276   *
277   * @type {Boolean}
278   */
279   multiline: NO,
280 
281   /*
282   * Translates the multiline flag into something TextFieldView understands.
283   *
284   * @type {Boolean}
285   */
286   isTextArea: function() {
287     return this.get('multiline');
288   }.property('multiline').cacheable(),
289 
290   /*
291   * Begins editing the given view, positions the editor on top of the view, and
292   * copies the styling of the view onto the editor.
293   *
294   * @params {SC.InlineEditable} the view being edited
295   *
296   * @returns {Boolean} YES on success
297   */
298   beginEditing: function(original, label) {
299 		if(!original(label)) return NO;
300 
301     var pane = label.get('pane'),
302       elem = this.get('exampleElement');
303 
304     this.beginPropertyChanges();
305 
306     if (label.multiline) this.set('multiline', label.multiline);
307 
308     // if we have an exampleElement we need to make sure it's an actual
309     // DOM element not a jquery object
310     if (elem) {
311       if(elem.length) elem = elem[0];
312     }
313 
314     // if we don't have an element we need to get it from the target
315     else {
316       elem = label.$()[0];
317     }
318 
319     this.updateStyle(elem);
320 
321     this.positionOverTargetView(label, this.get('exampleFrame'), elem);
322 
323     this._previousFirstResponder = pane ? pane.get('firstResponder') : null;
324     this.becomeFirstResponder();
325     this.endPropertyChanges() ;
326 
327     return YES;
328   }.enhance(),
329 
330   /**
331     Invoked whenever the editor loses (or should lose) first responder
332     status to commit or discard editing.
333 
334     @returns {Boolean}
335   */
336   // TODO: this seems to do almost the same thing as fieldDidBlur
337   blurEditor: function(evt) {
338     if (this.get('isEditing')) {
339       return this.commitOnBlur ? this.commitEditing() : this.discardEditing();
340     } else {
341       return true;
342     }
343   },
344 
345   /**
346     @method
347     @private
348 
349     Called by commitEditing and discardEditing to actually end editing.
350 
351   */
352   _endEditing: function(original) {
353     var ret = original();
354 
355     // resign first responder if not done already.  This may call us in a
356     // loop but since isEditing is already NO, nothing will happen.
357     if (this.get('isFirstResponder')) {
358       var pane = this.get('pane');
359       if (pane && this._previousFirstResponder) {
360         pane.makeFirstResponder(this._previousFirstResponder);
361       } else this.resignFirstResponder();
362     }
363     this._previousFirstResponder = null ; // clearout no matter what
364 
365     return ret;
366   }.enhance(),
367 
368   // TODO: make textArea automatically resize to fit content
369 
370   /** @private */
371   mouseDown: function(e) {
372     arguments.callee.base.call(this, e) ;
373     return this.get('isEditing');
374   },
375 
376   touchStart: function(e){
377     this.mouseDown(e);
378   },
379 
380   _scitf_blurInput: function() {
381     var el = this.$input()[0];
382     if (el) el.blur();
383     el = null;
384   },
385 
386   // [Safari] if you don't take key focus away from an element before you
387   // remove it from the DOM key events are no longer sent to the browser.
388   /** @private */
389   willRemoveFromParent: function() {
390     return this._scitf_blurInput();
391   },
392 
393   // ask owner to end editing.
394   /** @private */
395   willLoseFirstResponder: function(responder, evt) {
396     if (responder !== this) return;
397 
398     // if we're about to lose first responder for any reason other than
399     // ending editing, make sure we clear the previous first responder so
400     // isn't cached
401     this._previousFirstResponder = null;
402 
403     // store the original event that caused this to loose focus so that
404     // it can be passed to the delegate
405     this._origEvent = evt;
406 
407     // should have been covered by willRemoveFromParent, but this was needed
408     // too.
409     this._scitf_blurInput();
410     return this.blurEditor(evt) ;
411   },
412 
413   /**
414     invoked when the user presses escape.  Returns true to ignore keystroke
415 
416     @returns {Boolean}
417   */
418   cancel: function() {
419     this.discardEditing();
420     return YES;
421   },
422 
423   // Invoked when the user presses return.  If this is a multi-line field,
424   // then allow the new line to proceed by calling the super class.
425   // Otherwise, try to commit the edit.
426   /** @private */
427   insertNewline: function(evt) {
428     if (this.get('isTextArea')) {
429       return sc_super();
430     } else {
431       this.commitEditing() ;
432       return YES ;
433     }
434   },
435 
436   // Tries to find the next key view when tabbing.  If the next view is
437   // editable, begins editing.
438   /** @private */
439   insertTab: function(evt) {
440     var target = this.target; // removed by commitEditing()
441     this.resignFirstResponder();
442     if(target){
443       var next = target.get('nextValidKeyView');
444       if(next && next.beginEditing) next.beginEditing();
445     }
446     return YES ;
447   },
448 
449   /** @private */
450   insertBacktab: function(evt) {
451     var target = this.target; // removed by commitEditing()
452     this.resignFirstResponder();
453     if(target){
454       var prev = target.get('previousValidKeyView');
455       if(prev && prev.beginEditing) prev.beginEditing();
456     }
457     return YES ;
458   }
459 });
460