// ======================================================================== // SproutCore // copyright 2006-2008 Sprout Systems, Inc. // ======================================================================== require('views/view') ; require('mixins/delegate_support') ; require('views/field/text_field') ; require('mixins/inline_editor_delegate'); /** @class The inline text editor is used to display an editable area for controls that are not always editable such as label views and source list views. You generally will not use the inline editor directly but instead will invoke beginEditing() and endEditing() on the views yous are editing. If you would like to use the inline editor for your own views, you can do that also by using the editing API described here. h2. Using the Inline Editor in Your Own Views If you need to use the inline editor with custom views you have written, you will need to work with the class methods to begin, commit, and discard editing changes. h3. Starting the Editor The inline editor works by positioning itself over the top of your view with the same offset, width, and font information. As the user types, the field will automatically resize vertically to make room for the user's text. To activate the inline editor you must call beginEdition() with at least the target view you want the editor to position itself on top of: {{{ SC.InlineTextFieldView.beginEditing({ target: view, validator: validator }) ; }}} You can pass a variety of options to this method to configure the inline editor behavior, including: - *frame* The editors initial frame in viewport coordinates (REQ) - *exampleElement* A DOM element to use when copying styles. - *delegate* Optional delegate to receive update notices. If not passed, the target view will be treated as the delegate. (REQ) - *value* The initial value of the edit field. If not passed, the value property of the target view will be used instead. - *multiline* If YES then the hitting return will add to the value instead of exiting the inline editor. - *selectedRange* The range of text that should be selected. If omitted, then the insertion point will be placed at the end of the value. - *commitOnBlur* If YES then bluring will commit the value, otherwise it will discard the current value. Defaults to YES. - *validator* Optional validator will be attached to the field. If the inline editor is currently in use elsewhere, it will automatically close itself over there and begin editing for your view instead. The editor expects your source view to implement the InlineTextFieldViewDelegate protocol. h2. Commiting or Discarding Changes Normally the editor will automatically commit or discard its changes whenever the user exits the edit mode. If you need to force the editor to end editing, you can do so by calling commitEditing() or discardEditing(): {{{ SC.InlineTextFieldView.commitEditing(); SC.InlineTextFieldView.discardEditing(); }}} Both methods will try to end the editing context and will call the relevent delegate methods on the delegate you passed to beginEditing(). Note that it is possible an editor may not be able to commit editing changes because either the delegate disallowed it or because its validator failed. In this case commitEditing() will return NO. If you want to end editing anyway, you can discard the editing changes instead by calling discardEditing(). This method will generally suceed unless your delegate refuses it as well. @extends SC.View @extends SC.DelegateSupport @extends SC.InlineEditorDelegate @since SproutCore 1.0 */ SC.InlineTextFieldView = SC.View.extend(SC.DelegateSupport, SC.InlineEditorDelegate, /** @scope SC.InlineTextFieldView.prototype */ { /** Invoked by the class method to begin editing on an inline editor. You generally should call the class method beginEditing() instead of this one since it will make sure to create and use the shared editor instance. @params options {Hash} hash of options for editing @returns {Boolean} YES if editor began editing, NO if it failed. */ beginEditing: function(options) { // end existing editing if necessary this.beginPropertyChanges(); if (this.get('isEditing') && !this.blurEditor()) { this.endPropertyChanges(); return NO ; } this._frame = options.frame ; this._exampleElement = options.exampleElement ; this._delegate = options.delegate ; if (!this._frame || !this._delegate) { throw "At least frame and delegate options are required for inline editor"; } this._originalValue = options.value || '' ; this._multiline = (options.multiline !== undefined) ? options.multiline : NO ; this._commitOnBlur = (options.commitOnBlur !== undefined) ? options.commitOnBlur : YES ; // set field values var field = this.outlet('field') ; field.set('validator', options.validator) ; field.set('value', this._originalValue) ; field.set('selectedRange', options.selectedRange || { start: this._originalValue.length, length: 0 }) ; this.set('isEditing', YES) ; // add to window. SC.window.appendChild(this) ; // get style for view. this.updateViewStyle() ; var del = this._delegate ; this.invokeDelegateMethod(del, 'inlineEditorWillBeginEditing', this) ; this.resizeToFit(field.getFieldValue()) ; // allow notifications to go this.endPropertyChanges() ; // and become first responder this.field.becomeFirstResponder() ; this.invokeDelegateMethod(del, 'inlineEditorDidBeginEditing', this) ; }, /** Tries to commit the current value of the field and end editing. Do not use this method, use the class method instead. */ commitEditing: function() { // try to validate field. If it fails, return false. var field = this.outlet('field') ; if (!$ok(field.validateSubmit())) return NO ; return this._endEditing(field.get('value')) ; }, /** Tries to discard the current value of the field and end editing. Do not use this method, use the class method instead. */ discardEditing: function() { return this._endEditing(this._originalValue) ; }, /** Invoked whenever the editor loses (or should lose) first responder status to commit or discard editing. */ blurEditor: function() { if (!this.get('isEditing')) return YES ; return (this._commitOnBlur) ? this.commitEditing() : this.discardEditing(); }, /** @private Called by commitEditing and discardEditing to actually end editing. @returns {Boolean} NO if editing did not exit */ _endEditing: function(finalValue) { if (!this.get('isEditing')) return YES ; // get permission from the delegate. var del = this._delegate ; if (!this.invokeDelegateMethod(del, 'inlineEditorShouldEndEditing', this, finalValue)) return NO ; // OK, we are allowed to end editing. Notify delegate of final value // and clean up. console.log('applying finalValue: %@'.fmt(finalValue)) ; this.invokeDelegateMethod(del, 'inlineEditorDidEndEditing', this, finalValue) ; // cleanup cached values this._originalValue = this._delegate = this._exampleElement = this._frame = null ; this.set('isEditing', NO) ; // resign first responder if not done already. This may call us in a // loop but since isEditing is already NO, nothing will happen. if (this.field.get('isFirstResponder')) this.field.resignFirstResponder(); if (this.get('parentNode')) this.removeFromParent() ; return YES ; }, /** YES if the editor is currently visible and editing. @type {Boolean} */ isEditing: NO, /** @private */ emptyElement: [ '