// ==========================================================================
// Project:   SproutCore - JavaScript Application Framework
// Copyright: ©2006-2010 Sprout Systems, Inc. and contributors.
//            Portions ©2008-2011 Apple Inc. All rights reserved.
// License:   Licensed under MIT license (see license.js)
// ==========================================================================

/**
  @namespace

  This mixin is for a view that is editable but is acting as a proxy to
  edit another view. For example, a calendar picker that pops up over
  top of a date display.

  Instantiation and destruction will be handled by the InlineEditorDelegate
  defined on the InlineEditable view.

  Any runtime configuration should be handled by the delegate's
  inlineEditorWillBeginEditing method. This will be done at the start of the
  beginEditing function, so your view should be able to handle having value
  changed by this method.

  Your view should also be sure to cleanup completely when editing is finished,
  because the delegate may decide to reuse the same editor elsewhere.

  See SC.InlineTextFieldView for an example of an implementation of
  SC.InlineEditor.
*/
SC.InlineEditor = {

  /**
    Walk like a duck.

    @type Boolean
    @default YES
    @readOnly
  */
  isInlineEditor: YES,

  /**
    Indicates the view is currently editing. For a typical editor, this
    will always be true as long as the view exists, but some
    InlineEditorDelegates may have more complex lifecycles in which
    editors are reused.

    @type Boolean
    @default NO
  */
  isEditing: NO,

  /**
    The delegate responsible for the editors lifecycle as well as the
    target for notifications. This property should be set when the
    delegate creates the editor and does not need to be in the view
    definition.

    @type SC.InlineEditorDelegate
    @default null
  */
  inlineEditorDelegate: null,

  /**
    @private
    The view that this view is responsible for editing.
    @type SC.InlineEditable
  */
  _target: null,

  /**
    Tells the editor to begin editing another view with the given starting value.
    Editors may be reused so make sure that the editor is fully cleaned
    up and reinitialized.

    Sets isEditing to YES.

    Will fail if the editor is already editing.

    If you override this method, be sure to call arguments.callee.base.apply(this,arguments) at the beginning of
    you function so that the delegate will be able to configure the view when it
    is notified of the inlineEditorWillBeginEditing event.

    @param {SC.View} editable the view being edited
    @returns {Boolean} whether the editor was able to successfully begin editing
  */
  beginEditing:function(editable) {
    if(this.get('isEditing') || !editable || !editable.isInlineEditable) return NO;

    var del, target;

    target = this._target = editable;

    del = this.delegateFor('inlineEditorWillBeginEditing', this.inlineEditorDelegate, target);
    if(del) del.inlineEditorWillBeginEditing(this, this.get('value'), target);

    this.set('isEditing', YES);

    // needs to be invoked last because it needs to run after the view becomes
    // first responder
    this.invokeLast(this._callDidBegin);

    // remember that we invoked in case commit gets called before the invoke
    // goes off
    this._didBeginInvoked = YES;

    return YES;
  },

  /**
    @private
    Notifies the delegate of the didBeginEditing event. Needs to be invoked last
    because becoming first responder doesn't happen until the end of the runLoop
    and didBegin is supposed to occur after the editor becomes first responder.
  */
  _callDidBegin: function() {
    // don't notify if we already ended editing
    if(!this.get('isEditing')) return NO;

    this._didBeginInvoked = NO;

    var target = this._target, del;

    del = this.delegateFor('inlineEditorDidBeginEditing', this.inlineEditorDelegate, target);
    if(del) del.inlineEditorDidBeginEditing(this, this.get('value'), target);
  },

  /**
    Tells the editor to save its value back to its target view and end
    editing. Since the editor is a private property of the view it is
    editing for, this function should only be called from the editor
    itself. For example, you may want your editor to handle the enter
    key by calling commitEditing on itself.

    Will fail if the editor is not editing or if the delegate returns NO to
    inlineEditorShouldCommitEditing.

    @returns {Boolean} whether the editor was allowed to successfully commit its value
  */
  commitEditing:function() {
    if(!this.get('isEditing')) return NO;

    // if the handler was invoked but never went off, call it now
    if(this._didBeginInvoked) this._callDidBegin();

    var del, target = this._target;

    del = this.delegateFor('inlineEditorShouldCommitEditing', this.inlineEditorDelegate, target);
    if(del && !del.inlineEditorShouldCommitEditing(this, this.get('value'), target)) return NO;

    del = this.delegateFor('inlineEditorWillCommitEditing', this.inlineEditorDelegate, target);
    if(del) del.inlineEditorWillCommitEditing(this, this.get('value'), target);

    this._endEditing();

    del = this.delegateFor('inlineEditorDidCommitEditing', this.inlineEditorDelegate, target);
    if(del) del.inlineEditorDidCommitEditing(this, this.get('value'), target);

    return YES;
  },

  /**
    Tells the editor to discard its value and end editing. Like
    commitEditing, this should only be called by other methods of the
    editor. For example, the handle for the escape key might call
    discardEditing.

    Will fail if the editor is not editing or if the delegate returns NO to
    inlineEditorShouldDiscardEditing.

    @returns {Boolean} whether the editor was allowed to discard its value
  */
  discardEditing:function() {
    if(!this.get('isEditing')) return NO;

    // if the handler was invoked but never went off, call it now
    if(this._didBeginInvoked) this._callDidBegin();

    var del, target = this._target;

    del = this.delegateFor('inlineEditorShouldDiscardEditing', this.inlineEditorDelegate, target);
    if(del && !del.inlineEditorShouldDiscardEditing(this, target)) return NO;

    del = this.delegateFor('inlineEditorWillDiscardEditing', this.inlineEditorDelegate, target);
    if(del) del.inlineEditorWillDiscardEditing(this, target);

    this._endEditing();

    del = this.delegateFor('inlineEditorDidDiscardEditing', this.inlineEditorDelegate, target);
    if(del) del.inlineEditorDidDiscardEditing(this, target);

    return YES;
  },

  /**
    @private
    Performs the cleanup functionality shared between discardEditing and
    commitEditing.
  */
  _endEditing: function() {
    this.set('isEditing', NO);
    this._target = null;
  }

};
