Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/recipe-fiddle.git
/**
 * http://code.google.com/p/jquery-in-place-editor/
 * @version 2.3.0
 * @author Dave Hauenstein
 * @author Martin Häcker
 * @author Dave Jarvis
 */
(function($){

$.fn.editInPlace = function(options) {
  jQuery.browser = {};
  jQuery.browser.mozilla = /mozilla/.test(navigator.userAgent.toLowerCase()) && !/webkit/.test(navigator.userAgent.toLowerCase());
  jQuery.browser.msie = /msie/.test(navigator.userAgent.toLowerCase());
  jQuery.browser.safari = navigator.userAgent.indexOf("Safari") > -1;

  var settings = $.extend({}, $.fn.editInPlace.defaults, options);
  preloadImage(settings.saving_image);
  
  return this.each(function() {
    var dom = $(this);
    // This won't work with live queries as there is no specific element
    // to attach this. One way to deal with this could be to store a
    // reference to self and then compare that in click?
    if (dom.data('editInPlace'))
      return; // already an editor here
    dom.data('editInPlace', true);
    
    new InlineEditor(settings, dom).init();
  });
};

/// Switch these through the dictionary argument to
// $(aSelector).editInPlace(overideOptions)
// Required Options: Either url or callback, so the editor knows what to do
// with the edited values.
$.fn.editInPlace.defaults = {
  url:          "", // string: POST URL to send edited content
  bg_over:      "#ffc", // string: background color of hover of unactivated editor
  bg_out:       "transparent", // string: background color on restore from hover
  hover_class:    "",  // string: class added to root element during hover. Will override bg_over and bg_out
  show_buttons:   false, // boolean: will show the buttons: cancel or save; will automatically cancel out the onBlur functionality
  params:       "", // string: example: first_name=dave&last_name=hauenstein extra paramters sent via the post request to the server
  field_type:     "text", // string: "text", "textarea", or "select";  The type of form field that will appear on instantiation
  default_text:   "Click to edit", // string: text to show up if the element that has this functionality is empty
  select_text:    "Select", // string: default text to show up in select box
  //select_options:   "", // string or array: Used if field_type is set to 'select'. Can be comma delimited list of options 'textandValue,text:value', Array of options ['textAndValue', 'text:value'] or array of arrays ['textAndValue', ['text', 'value']]. The last form is especially usefull if your labels or values contain colons)
  text_size:      null, // integer: set cols attribute of text input, if field_type is set to text. Use CSS if possible though
  
  // Specifying callback_skip_dom_reset will disable all saving_* options
  saving_text:    undefined, // string: text to be used when server is saving information. Example "Saving..."
  saving_image:   "", // string: uses saving text specify an image location instead of text while server is saving
  saving_animation_color: 'transparent', // hex color string, will be the color the pulsing animation during the save pulses to. Note: Only works if jquery-ui is loaded
  
  value_required:   false, // boolean: if set to true, the element will not be saved unless a value is entered
  element_id:     "element_id", // string: name of parameter holding the id or the editable
  update_value:   "update_value", // string: name of parameter holding the updated/edited value
  original_value:   'original_value', // string: name of parameter holding the updated/edited value
  original_html:    "original_html", // string: name of parameter holding original_html value of the editable /* DEPRECATED in 2.2.0 */ use original_value instead.
  save_if_nothing_changed:  false,  // boolean: submit to function or server even if the user did not change anything
  on_blur:      "save", // string: "save" or null; what to do on blur; will be overridden if show_buttons is true
  cancel:       "", // string: if not empty, a jquery selector for elements that will not cause the editor to open even though they are clicked. E.g. if you have extra buttons inside editable fields
  
  // All callbacks will have this set to the DOM node of the editor that triggered the callback
  
  // DJ - name of the text field.
  editor_id:    "inplace_id",
  // DJ  - do not select the text by default.
  selected:     false,
  on_create:    null, // function: called after the editor is created
  on_edit:      null, // function: called before editing begins

  callback:     null, // function: function to be called when editing is complete; cancels ajax submission to the url param. Prototype: function(idOfEditor, enteredText, orinalHTMLContent, settingsParams, callbacks). The function needs to return the value that should be shown in the dom. Returning undefined means cancel and will restore the dom and trigger an error. callbacks is a dictionary with two functions didStartSaving and didEndSaving() that you can use to tell the inline editor that it should start and stop any saving animations it has configured. /* DEPRECATED in 2.1.0 */ Parameter idOfEditor, use $(this).attr('id') instead
  callback_skip_dom_reset: false, // boolean: set this to true if the callback should handle replacing the editor with the new value to show
  success:      null, // function: this function gets called if server responds with a success. Prototype: function(newEditorContentString)
  error:        null, // function: this function gets called if server responds with an error. Prototype: function(request)
  error_sink:     function(idOfEditor, errorString) { alert(errorString); }, // function: gets id of the editor and the error. Make sure the editor has an id, or it will just be undefined. If set to null, no error will be reported. /* DEPRECATED in 2.1.0 */ Parameter idOfEditor, use $(this).attr('id') instead
  preinit:      null, // function: this function gets called after a click on an editable element but before the editor opens. If you return false, the inline editor will not open. Prototype: function(currentDomNode). DEPRECATED in 2.2.0 use delegate shouldOpenEditInPlace call instead
  postclose:      null, // function: this function gets called after the inline editor has closed and all values are updated. Prototype: function(currentDomNode). DEPRECATED in 2.2.0 use delegate didCloseEditInPlace call instead
  delegate:     null // object: if it has methods with the name of the callbacks documented below in delegateExample these will be called. This means that you just need to impelment the callbacks you are interested in.
};

function InlineEditor(settings, dom) {
  this.settings = settings;
  this.dom = dom;
  this.originalValue = null;
  this.didInsertDefaultText = false;
  this.shouldDelayReinit = false;
};

$.extend(InlineEditor.prototype, {
  
  init: function() {
    this.setDefaultTextIfNeccessary();
    this.connectOpeningEvents();
  },
  
  reinit: function() {
    if (this.shouldDelayReinit)
      return;
    
    this.triggerCallback(this.settings.postclose, /* DEPRECATED in 2.1.0 */ this.dom);
    this.triggerDelegateCall('didCloseEditInPlace');
    
    this.markEditorAsInactive();
    this.connectOpeningEvents();
  },
  
  setDefaultTextIfNeccessary: function() {
    if('' !== this.dom.html())
      return;
    
    this.dom.html(this.settings.default_text);
    this.didInsertDefaultText = true;
  },
  
  connectOpeningEvents: function() {
    var that = this;
    this.dom
      .bind('mouseenter.editInPlace', function(){ that.addHoverEffect(); })
      .bind('mouseleave.editInPlace', function(){ that.removeHoverEffect(); })
      .bind('click.editInPlace', function(anEvent){ that.openEditor(anEvent); });
  },
  
  disconnectOpeningEvents: function() {
    // prevent re-opening the editor when it is already open
    this.dom.unbind('.editInPlace');
  },
  
  addHoverEffect: function() {
    if (this.settings.hover_class)
      this.dom.addClass(this.settings.hover_class);
    else
      this.dom.css("background-color", this.settings.bg_over);
  },
  
  removeHoverEffect: function() {
    if (this.settings.hover_class)
      this.dom.removeClass(this.settings.hover_class);
    else
      this.dom.css("background-color", this.settings.bg_out);
  },
  
  openEditor: function(anEvent) {
    if (this.shouldOpenEditor(anEvent)) {
      this.disconnectOpeningEvents();
      this.removeHoverEffect();
      this.removeInsertedDefaultTextIfNeccessary();
      this.saveOriginalValue();
      this.markEditorAsActive();
      this.replaceContentWithEditor();
      this.setInitialValue();
      this.workAroundMissingBlurBug();
      this.connectClosingEventsToEditor();
      this.triggerDelegateCall('didOpenEditInPlace');
    }
  },
  
  shouldOpenEditor: function(anEvent) {
    if (this.isClickedObjectCancelled(anEvent.target))
      return false;
    
    if (false === this.triggerCallback(this.settings.preinit, /* DEPRECATED in 2.1.0 */ this.dom))
      return false;
    
    if (false === this.triggerDelegateCall('shouldOpenEditInPlace', true, anEvent))
      return false;
    
    return true;
  },
  
  removeInsertedDefaultTextIfNeccessary: function() {
    if ( ! this.didInsertDefaultText
      || this.dom.html() !== this.settings.default_text)
      return;
    
    this.dom.html('');
    this.didInsertDefaultText = false;
  },
  
  isClickedObjectCancelled: function(eventTarget) {
    if ( ! this.settings.cancel)
      return false;
    
    var eventTargetAndParents = $(eventTarget).parents().andSelf();
    var elementsMatchingCancelSelector = eventTargetAndParents.filter(this.settings.cancel);
    return 0 !== elementsMatchingCancelSelector.length;
  },
  
  saveOriginalValue: function() {
    this.originalValue = trim(this.dom.text());
  },
  
  restoreOriginalValue: function() {
    this.setClosedEditorContent(this.originalValue);
  },
  
  setClosedEditorContent: function(aValue) {
    this.dom.text(aValue);
  },
  
  workAroundMissingBlurBug: function() {
    // Strangely, all browser will forget to send a blur event to an input element
    // when another one is created and selected programmatically. (at least under some circumstances). 
    // This means that if another inline editor is opened, existing inline editors will _not_ close 
    // if they are configured to submit when blurred.
    
    // Using parents() instead document as base to workaround the fact that in the unittests
    // the editor is not a child of window.document but of a document fragment
    var ourInput = this.dom.find(':input');
    this.dom.parents(':last').find('.editInPlace-active :input').not(ourInput).blur();
  },
  
  replaceContentWithEditor: function() {
    // needs to happen before anything is replaced
    var editorElement = this.createEditorElement();

    /* insert the new in place form after the element they click, then empty out the original element */

    // DJ - http://code.google.com/p/jquery-in-place-editor/issues/detail?id=72
    // DJ - Removed buttons
    this.dom.html('').append(
      '<form name="eipform" class="inplace_form" style="display: inline; margin: 0; padding: 0;"></form>')
      .find('form[name="eipform"]')
      .append( editorElement );

    // DJ: The input editor is part of the DOM and can now be manipulated.
    if( this.settings.on_create ) {
      this.settings.on_create();
    }
  },
  
  createEditorElement: function() {
    var editor = null;

    if ("text" === this.settings.field_type)
      editor = $('<input type="text" ' + this.inputNameAndClass() 
        + ' size="' + this.settings.text_size  + '" />');
    
    return editor;
  },
  
  setInitialValue: function() {
    var initialValue = this.triggerDelegateCall('willOpenEditInPlace', this.originalValue);
    var editor = this.dom.find(':input');

    // DJ: Function called before editing takes place.
    if( this.settings.on_edit ) {
      initialValue = this.settings.on_edit( this.dom, initialValue );
    }

    editor.val(initialValue);
    
    // Workaround for select fields which don't contain the original value.
    // Somehow the browsers don't like to select the instructional choice (disabled) in that case
    if (editor.val() !== initialValue)
      editor.val(''); // selects instructional choice
  },
  
  /**
   * Returns the input name, class, and ID for the editor.
   */
  inputNameAndClass: function() {
    var result = ' name="inplace_value" class="inplace_field" ';

    // DJ: Append the ID to the editor input element.
    if( this.settings.editor_id ) {
      result += 'id="' + this.settings.editor_id + '" ';
    }

    return result;
  },

  connectClosingEventsToEditor: function() {
    var that = this;
    function cancelEditorAction(anEvent) {
      that.handleCancelEditor(anEvent);
      return false; // stop event bubbling
    }

    function saveEditorAction(anEvent) {
      that.handleSaveEditor(anEvent);
      return false; // stop event bubbling
    }
    
    var form = this.dom.find('form[name="eipform"]');

    // DJ - Moved .select() to be conditional.
    var form_element = form.find(".inplace_field").focus();

    // DJ - Sometimes the text should be selected initially.
    if( this.settings.selected ) {
      form_element.select();
    }

    form.find(".inplace_cancel").click(cancelEditorAction);
    form.find(".inplace_save").click(saveEditorAction);

    if ( ! this.settings.show_buttons) {
        // TODO: Firefox has a bug where blur is not reliably called when focus is lost 
        //       (for example by another editor appearing)
      if ("save" === this.settings.on_blur)
        form_element.blur(saveEditorAction);
      else
        form_element.blur(cancelEditorAction);
      
      // workaround for msie & firefox bug where it won't submit on enter if no button is shown
      if ($.browser.mozilla || $.browser.msie)
        this.bindSubmitOnEnterInInput();
    }
    
    form.keyup(function(anEvent) {
      // allow canceling with escape
      // DJ: smaller code
      if (anEvent.which === 27) {
        return cancelEditorAction();
      }
    });
    
    // workaround for webkit nightlies where they won't submit at all on enter
    // REFACT: find a way to just target the nightlies
    if ($.browser.safari)
      this.bindSubmitOnEnterInInput();

    form.submit(saveEditorAction);
  },
  
  bindSubmitOnEnterInInput: function() {
    var that = this;
    this.dom.find(':input').keyup(function(event) {
      // DJ: tighter code
      if (event.which === 13) {
        return that.dom.find('form[name="eipform"]').submit();
      }
    });
  },
  
  handleCancelEditor: function(anEvent) {
    // REFACT: remove duplication between save and cancel
    this.restoreOriginalValue();

    if (false === this.triggerDelegateCall('shouldCloseEditInPlace', true, anEvent))
      return;
    
    var enteredText = this.dom.find(':input').val();
    enteredText = this.triggerDelegateCall('willCloseEditInPlace', enteredText);
    
    this.restoreOriginalValue();
    this.reinit();
  },
  
  handleSaveEditor: function(anEvent) {
    if (false === this.triggerDelegateCall('shouldCloseEditInPlace', true, anEvent))
      return;
    
    var enteredText = this.dom.find(':input').val();
    enteredText = this.triggerDelegateCall('willCloseEditInPlace', enteredText);
    
    if (this.isDisabledDefaultSelectChoice()
      || this.isUnchangedInput(enteredText)) {
      this.handleCancelEditor(anEvent);
      return;
    }
    
    if (this.didForgetRequiredText(enteredText)) {
      this.handleCancelEditor(anEvent);
      return;
    }
    
    this.showSaving(enteredText);

    if (this.settings.callback)
      this.handleSubmitToCallback(enteredText);
    else
      this.handleSubmitToServer(enteredText);
  },
  
  didForgetRequiredText: function(enteredText) {
    return this.settings.value_required 
      && ("" === enteredText 
        || undefined === enteredText
        || null === enteredText);
  },
  
  isDisabledDefaultSelectChoice: function() {
    return this.dom.find('option').eq(0).is(':selected:disabled');
  },
  
  isUnchangedInput: function(enteredText) {
    return ! this.settings.save_if_nothing_changed
      && this.originalValue === enteredText;
  },
  
  showSaving: function(enteredText) {
    if (this.settings.callback && this.settings.callback_skip_dom_reset)
      return;
    
    var savingMessage = enteredText;
    if (hasContent(this.settings.saving_text))
      savingMessage = this.settings.saving_text;
    if(hasContent(this.settings.saving_image))
      // REFACT: alt should be the configured saving message
      savingMessage = $('<img />').attr('src', this.settings.saving_image).attr('alt', savingMessage);
    this.dom.html(savingMessage);
  },
  
  handleSubmitToCallback: function(enteredText) {
    // REFACT: consider to encode enteredText and originalHTML before giving it to the callback
    this.enableOrDisableAnimationCallbacks(true, false);
    var newHTML = this.triggerCallback(this.settings.callback, /* DEPRECATED in 2.1.0 */ this.id(), enteredText, this.originalValue, 
      this.settings.params, this.savingAnimationCallbacks());
    
    if (this.settings.callback_skip_dom_reset) {
      // do nothing
    }
    else if (undefined === newHTML) {
      // failure; put original back
      this.restoreOriginalValue();
    }
    else
      // REFACT: use setClosedEditorContent
      this.dom.html(newHTML);
    
    if (this.didCallNoCallbacks()) {
      this.enableOrDisableAnimationCallbacks(false, false);
      this.reinit();
    }
  },
  
  handleSubmitToServer: function(enteredText) {
    // DJ changed element_id to return value of element_id attribute
    var data = this.settings.update_value + '=' + encodeURIComponent(enteredText) 
      + '&' + this.settings.element_id + '=' + this.dom.attr( this.settings.element_id )
      + ((this.settings.params) ? '&' + this.settings.params : '')
      + '&' + this.settings.original_html + '=' + encodeURIComponent(this.originalValue) /* DEPRECATED in 2.2.0 */
      + '&' + this.settings.original_value + '=' + encodeURIComponent(this.originalValue);

    this.enableOrDisableAnimationCallbacks(true, false);
    this.didStartSaving();
    var that = this;

    $.ajax({
      url: that.settings.url,
      type: "POST",
      data: data,
      dataType: "html",
      complete: function(request){
        that.didEndSaving();
      },
      success: function(html){
        var new_text = html || that.settings.default_text;
        
        /* put the newly updated info into the original element */
        // FIXME: should be affected by the preferences switch
        that.dom.html(new_text);
        // REFACT: remove dom parameter, already in this, not documented, should be easy to remove
        // REFACT: callback should be able to override what gets put into the DOM
        that.triggerCallback(that.settings.success, html);
      },
      error: function(request) {

        that.dom.html(that.originalHTML); // REFACT: what about a restorePreEditingContent()
        if (that.settings.error)
          // REFACT: remove dom parameter, already in this, not documented, can remove without deprecation
          // REFACT: callback should be able to override what gets entered into the DOM
          that.triggerCallback(that.settings.error, request);
      }
    });
  },
  
  // Utilities .........................................................
  
  triggerCallback: function(aCallback /*, arguments */) {
    if ( ! aCallback)
      return; // callback wasn't specified after all
    
    var callbackArguments = Array.prototype.slice.call(arguments, 1);
    return aCallback.apply(this.dom[0], callbackArguments);
  },
  
  /// defaultReturnValue is only used if the delegate returns undefined
  triggerDelegateCall: function(aDelegateMethodName, defaultReturnValue, optionalEvent) {
    // REFACT: consider to trigger equivalent callbacks automatically via a mapping table?
    if ( ! this.settings.delegate
      || ! $.isFunction(this.settings.delegate[aDelegateMethodName]))
      return defaultReturnValue;
    
    var delegateReturnValue =  this.settings.delegate[aDelegateMethodName](this.dom, this.settings, optionalEvent);
    return (undefined === delegateReturnValue)
      ? defaultReturnValue
      : delegateReturnValue;
  },
  
  // REFACT: this method should go, callbacks should get the dom node itself as an argument
  id: function() {
    return this.dom.attr('id');
  },
  
  markEditorAsActive: function() {
    this.dom.addClass('editInPlace-active');
  },
  
  markEditorAsInactive: function() {
    this.dom.removeClass('editInPlace-active');
  },
  
  // REFACT: consider rename, doesn't deal with animation directly
  savingAnimationCallbacks: function() {
    var that = this;
    return {
      didStartSaving: function() { that.didStartSaving(); },
      didEndSaving: function() { that.didEndSaving(); }
    };
  },
  
  enableOrDisableAnimationCallbacks: function(shouldEnableStart, shouldEnableEnd) {
    this.didStartSaving.enabled = shouldEnableStart;
    this.didEndSaving.enabled = shouldEnableEnd;
  },
  
  didCallNoCallbacks: function() {
    return this.didStartSaving.enabled && ! this.didEndSaving.enabled;
  },
  
  assertCanCall: function(methodName) {
    if ( ! this[methodName].enabled)
      throw new Error(methodName);
  },
  
  didStartSaving: function() {
    this.assertCanCall('didStartSaving');
    this.shouldDelayReinit = true;
    this.enableOrDisableAnimationCallbacks(false, true);
    
    this.startSavingAnimation();
  },
  
  didEndSaving: function() {
    this.assertCanCall('didEndSaving');
    this.shouldDelayReinit = false;
    this.enableOrDisableAnimationCallbacks(false, false);
    this.reinit();
    
    this.stopSavingAnimation();
  },

  startSavingAnimation: function() {
    if($.ui!==undefined) {
      var that = this;
      this.dom
        .animate({backgroundColor: this.settings.saving_animation_color}, 400)
        .animate({backgroundColor: 'transparent'}, 400, 'swing', function(){
        setTimeout(function(){that.startSavingAnimation();}, 10);
      });
    }
  },

  stopSavingAnimation: function() {
    this.dom
      .stop(true)
      .css({backgroundColor: ''});
  }
});



// Private helpers .......................................................

/* preload the loading icon if it is configured */
function preloadImage(anImageURL) {
  if ('' === anImageURL)
    return;
  
  var loading_image = new Image();
  loading_image.src = anImageURL;
}

function trim(aString) {
  return aString
    .replace(/^\s+/, '')
    .replace(/\s+$/, '');
}

function hasContent(something) {
  if (undefined === something || null === something)
    return false;
  
  if (0 === something.length)
    return false;
  
  return true;
}

})(jQuery);