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