vegantech/sims

View on GitHub
app/assets/javascripts/jquery.nested-fields.js

Summary

Maintainability
A
2 hrs
Test Coverage
(function($) {
  /**
   *
   * Awesome Nested Fields
   *
   * Requires jquery-ujs adapter.
   * https://github.com/lailsonbm/awesome_nested_fields
   *
   */
   
  var defaultSettings = {
    beforeInsert: function(item, callback) { callback() },
    afterInsert: function(item) {},
    beforeRemove: function(item, callback) { callback() },
    afterRemove: function(item) {},
    itemTemplateSelector: '.item.template',
    emptyTemplateSelector: '.empty.template',
    containerSelector: '.items, .container',
    itemSelector: '.item',
    emptySelector: '.empty',
    addSelector: '.add',
    removeSelector: '.remove',
    newItemIndex: 'new_nested_item',
    unescapeTemplate: true
  };
  
  // PUBLIC API
  var methods = {
    init: function(options) {
      return this.each(function() {        
        var $this = $(this);
        if($this.data('nested-fields.options')) {
          log('Nested fields already defined for this element. If you want to redefine options, destroy it and init again.');
          return $this;
        }

        options = $.extend({}, defaultSettings, options);
        options.itemTemplate = $(options.itemTemplateSelector, $this);
        options.emptyTemplate = $(options.emptyTemplateSelector, $this);
        options.container = $(options.containerSelector, $this);
        options.add = $(options.addSelector, $this);
        $this.data('nested-fields.options', options); 

        bindInsertToAdd(options);
        bindRemoveToItems(options, $this);
      });
    },
    
    insert: function(callback, options) {
      options = $.extend({}, getOptions(this), options);
      return insertItemWithCallbacks(callback, options);
    },
    
    remove: function(element, options) {
      options = $.extend({}, getOptions(this), options);
      return removeItemWithCallbacks(element, options);
    },
    
    removeAll: function(options) {
      options = $.extend({}, getOptions(this), options);
      $(methods.items.apply(this)).each(function(i, el) {
        methods.remove(el, options);
      });
    },
    
    items: function() {
      return findItems(getOptions(this));
    },
    
    destroy: function() {
      $(this).removeData('nested-fields.options');
      $('*', this).unbind('.nested-fields');
    }
  };
  
  $.fn.nestedFields = function(method) {
    if (methods[method]) {
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
    } else if (typeof method === 'object' || !method) {
      return methods.init.apply(this, arguments);
    } else {
      $.error('Method ' +  method + ' does not exist on jQuery.nestedFields');
    }
  };
  
  // Initialization functions
  
  function getOptions(element) {
    var $element = $(element);
    while($element.length > 0) {
      var data = $element.data('nested-fields.options');
      if(data) {
        return data;
      } else {
        $element = $element.parent();
      }
    }
    return null;
  }
  
  function bindInsertToAdd(options) {
    options.add.bind('click.nested-fields', function(e) {
      e.preventDefault();
      insertItemWithCallbacks(null, options);
    });
  }
  
  function bindRemoveToItems(options, $this) {
    $(options.itemSelector, $this).each(function(i, item) {
      bindRemoveToItem(item, options);
    });
  }
  
  // Insertion functions
  
  function prepareTemplate(options) {
    var regexp = new RegExp(options.newItemIndex, 'g');
    var newId = new Date().getTime();
    
    var contents = options.itemTemplate.html();
    if(options['unescapeTemplate']) {
      contents = unescape_html_tags(contents);
    }
    var newItem = $(contents.replace(regexp, newId));
    newItem.attr('data-new-record', true);
    newItem.attr('data-record-id', newId);
    
    bindRemoveToItem(newItem, options);
    
    return newItem;
  }
  
  function insertItemWithCallbacks(onInsertCallback, options) {  
    var newItem = prepareTemplate(options);
    
    function insert() {
      if(onInsertCallback) {
        onInsertCallback(newItem);
      }
      removeEmpty(options);
      options.container.append(newItem);
    }
    
    if(!options.skipBefore) {      
      options.beforeInsert(newItem, insert);
      if(options.beforeInsert.length <= 1) {
        insert();
      }
    } else {
      insert();
    }
    
    if(!options.skipAfter) {
      options.afterInsert(newItem);
    }
    
    return newItem;
  }
  
  function removeEmpty(options) {
    findEmpty(options).remove();
  }
  
  // Removal functions
  
  function removeItemWithCallbacks(element, options) {
    function remove() {
      if($element.attr('data-new-record')) { // record is new
        $element.remove();
      } else { // record should be marked and sent to server
        $element.find("INPUT[name$='[_destroy]']").val('true');
        $element.hide();
      }
      insertEmpty(options);
    }
    
    var $element = $(element);
    if(!options.skipBefore) {
      options.beforeRemove($element, remove);
      if(options.beforeRemove.length <= 1) {
        remove();
      }
    } else {
      remove();
    }
    
    if(!options.skipAfter) {
      options.afterRemove($element);
    }
    
    return $element;
  }
  
  function insertEmpty(options) {
    if(findItems(options).length === 0) {
      var contents = options.emptyTemplate.html();
      if(options['unescapeTemplate']) {
        contents = unescape_html_tags(contents);
      }
      options.container.append(contents);
    }
  }
  
  function bindRemoveToItem(item, options) {
    var removeHandler = $(item).find(options.removeSelector);
    var needsConfirmation = removeHandler.attr('data-confirm');
    
    var event = needsConfirmation ? 'confirm:complete' : 'click';
    removeHandler.bind(event + '.nested-fields', function(e, confirmed) {
      e.preventDefault();
      if(confirmed === undefined || confirmed === true) {
        removeItemWithCallbacks(item, options);
      }
      return false;
    });
  }
  
  // Find functions
  
  function findItems(options) {
    return options.container.find(options.itemSelector + ':visible');
  }
  
  function findEmpty(options) {
    return options.container.find(options.emptySelector);
  }
  
  // Utility functions
  
  function unescape_html_tags(html) {
    var e = document.createElement('div');
    if(html === undefined) {
        html = "";
    }
    e.innerHTML = html;

    return e.childNodes.length === 0 ? "" : $.trim(e.childNodes[0].nodeValue);
  }
  
  function log(msg) {
    if(console && console.log) {
      console.log(msg);
    }
  }
  
})(jQuery);