// TODO's 
// send off forms dirty event when a form is dirtied so the page knows its been tarnished. (done, dvd 09-01-2009)
// make the stoppers work for anchor links
// Write docs + examples (done, dvd 09-01-2009)
// investigate: can make this work for "non-form"-forms too? I.e. given a container element, check dirtiness for the inputs inside it.
// investigate: possible to change this so that one can provide an extra selector to further filter the inputs in a form. I.e. "this form is to be considered dirty if inputs with class foo changes"
/*
=====================
= DirtyForms How-To =
=====================

Example usage:

  // All forms in the page will be observed
  // Whenever an input value changes it will get the 'changed' class added to it
  $(function(){
    $("form").dirty_form();
  });
    
  // All forms in the page will be observed
  // Whenever an input value changes it will get the 'forever_changes' class added to it
  $(function(){
    $("form").dirty_form({changeClass: "forever_changes"});
  });
    
  // Often times you want to apply the "changed" CSS class to more than one element.
  // To support this, DirtyForm can take a "addClassOn" option.
  // Pass in a function that will be executed in the context of the dirty input element
  // and the return value of the function will be added to the list of elements that
  // get the "changed" class. In this function "this" refers to the dirty element.
  // The code below will add the "change" class to the dirty input and to any labels 
  // descendants of the li element that contains the input. 
  // Example:
  $("form").dirty_form({
    addClassOn: function(){ 
      return this.parents("li").find("label");
      }
    });
  
  // Forms within the "ch-ch-ch-changes" DIV in the page will be observed
  // Whenever an input value changes it will get the 'changed' class added to it
  $(function(){
    $("#ch-ch-ch-changes form").dirty_form();
  });

  // Forms can observe the "dirty" event for customized behavior
  $(function(){
    $("form")
      .dirty_form()
      .dirty(function(event, data){
        var label = $(event.target).parents("li").find("label");
        $("body").append("<p>" + label.text() + "Changed from " + data.from + " to: " + data.to+ "</p>")
      })
  });
  
  // As of this writing jQuery does not support event bubbling for custom events
  // See discussion here: http://groups.google.com/group/jquery-dev/browse_thread/thread/1acd358ceeacd67a
  // Once the patch is in any DOM element can subscribe to the "dirty" event
  
  
  // The $.DirtyForm singleton can be used for whole-page configuration
  $(function(){
    $.DirtyForm.dynamic = false // Don't bother watching out for dynamic additions to the DOM
    $.DirtyForm.debug = true    // Turn on logging
    $.DirtyForm.logger = my_fancy_logger_fn // Override the default logger from console.log (if Firebug is available) or a plain-jane alert (for IE) -- NOT WORKING RIGHT NOW. GRR
  });
  
  // All configuration options can be set per-instance
  $(function(){
    $("#some_form").dirty_form({dynamic: false})
    $("#other_form").dirty_form({dynamic: true})
    $("#third_form").dirty_form({debug: true})
  });

*/

if (typeof jQuery == 'undefined') throw("jQuery could not be found.");

(function($){
  
  $.extend({
    DirtyForm: {
      debug         : false, // print out debug info? works best with firebug.
      changedClass  : 'changed',
      addClassOn    : new Function,
      observed_forms: [], 
      hasFirebug    : "console" in window && "firebug" in window.console,
      logger        : function(msg){
                        if(this.debug){
                          msg = "DirtyForm: " + msg;
                          this.hasFirebug ? console.log(msg) : alert(msg);
                        }
                      },
      input_value   : function(input){
                        if(input.is(':radio,:checkbox')){
                          return typeof(input.attr("checked")) == "undefined" ? false : input.attr("checked");
                        } else {
                          return input.val();
                        }
                      }, 
      input_checker : function(event){
                        var npt = $(event.target), form = event.data.form, initial = npt.data("initial"), current = $.DirtyForm.input_value(npt), inputs = event.data.inputs, settings = event.data.settings
                        
                        if(initial != current) {
                          $.DirtyForm.logger("Form "+form.id+" is dirty. Changed from \""+initial+"\" to \""+current+"\"");
                          $.DirtyForm.logger("Class: "+settings.changedClass);
                          form
                            .data("dirty", true)                                      //TODO: check if we can use an expando property here
                            .trigger("dirty", {target: npt, from: initial, to: current, preventDefault: function(){return false}, stopPropagation: function(){return false}, bubbles: true, cancelable: true});
                          npt
                            .add(settings.addClassOn.apply(npt))
                            .addClass(settings.changedClass);                          // TODO: maybe we need to check if the class exists already?
                            
                        } else {
                          npt
                            .add(settings.addClassOn.apply(npt))
                            .removeClass(settings.changedClass)
                        }
                        
                        if(!inputs.filter('.' + settings.changedClass).size()){
                          form
                            .data("dirty",false)
                            .trigger("clean", {target: npt, preventDefault: function(){return false}, stopPropagation: function(){return false}, bubbles: true, cancelable: true});
                        }
                      }
    }
    
  });
    
  // will flag a form as dirty if something is changed on the form.
  $.fn.dirty_form = function(){
    var defaults = {
      changedClass  : $.DirtyForm.changedClass,
      addClassOn    : $.DirtyForm.addClassOn,
      dynamic       : $.isFunction($.livequery)
    }
    
    var settings = $.extend(defaults, arguments.length != 0 ? arguments[0] : {});

    return this.each(function(){
      form = $(this);

      var inputs = $(':input:not(:hidden,:submit,:password,:button)', form)

      if( $.inArray(this, $.DirtyForm.observed_forms) == -1 ){
        $.DirtyForm.observed_forms.push(this);  // A new form, pushing on the list of the observed. Pushing the DOM element, rather than the jQuery'ized one, "form", because $.inArray() didn't work... :(
      }else{
        // unbind all DirtyForms specific events, then proceed to re-add them
        form.unbind("dirty").unbind("clean");
        inputs.unbind("blur.dirty_form");
      }

      $.DirtyForm.logger('Storing initial data for form ' + form.id);
      
      if (settings.dynamic) {
        inputs.livequery(function(){ // use livequery to perform these functions on the new elements added to the form
          $(this)
            .bind("blur.dirty_form", {inputs: inputs, settings: settings, form: form}, $.DirtyForm.input_checker)
            .data('initial', $.DirtyForm.input_value($(this)))
        });
      }else {
        inputs.each(function(){
          $(this)
            .bind("blur.dirty_form", {inputs: inputs, settings: settings, form: form}, $.DirtyForm.input_checker)
            .data("initial", $.DirtyForm.input_value($(this)));
        });
      }
    });
  };
  
  
  // this is meant for selecting links that will warn about proceeding if there are any dirty forms on the page
  $.fn.dirty_stopper = function(){
    $.DirtyForm.logger("Setting dirty stoppers")
    
    return this.each(function(){
      stopper = $(this);
      stopper.click(function(event){
        if($($.DirtyForm.observed_forms).are_dirty()) {
          event.preventDefault();
          var div = $("<div id='dirty_stopper_dialog'/>").appendTo(document.body)
          href = $(this).attr('href')
          div.dialog({
            title: "Warning: Unsaved Changes!",
            height: 300,
            width: 500,
            modal: true,
            buttons: {
              'Proceed': function(){window.location = href},
              'Cancel': function(){$(this).dialog('destroy').remove()}
            },
            resizeable: false,
            autoResize: false,
            overlay: {backgroundColor: "black", opacity: 0.5}
          });
          div.append("<br/><p>You have changed form data without saving. Are you sure you want to proceed?</p>");
        }
      });
    });
  }
  
  // not chainable
  // returns false if any of the forms on the page are dirty
  $.fn.are_dirty = function (){
    var dirty = false
    this.each(function(){
      if($(this).data('dirty')) {
        dirty = true;
      }
    })
    return dirty
  }
  
  // This is just for testing purposes...
  $.fn.dirty_checker = function(){    
    $.DirtyForm.logger("Setting dirty checkers!")
    
    return this.each(function(){
      checker = $(this);
      checker.click(function(){
        if($("form").are_dirty()) {
          alert("Dirty Form!!");
        } else {
          alert("Clean Form ...phew!");
        }
      });
    });
  }
  
  // Shortcut to bind a handler to the "ondirty" event
  $.fn.extend({
    dirty: function(fn) {
  		return this.bind('dirty', fn);
  	},
  	clean: function(fn) {
  		return this.bind('clean', fn);
  	}
  });
})(jQuery);
