Skip to content

Instantly share code, notes, and snippets.

@alexweissman
Created May 10, 2017 23:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexweissman/824c9e538d3e3545f05acd51e05dfb7d to your computer and use it in GitHub Desktop.
Save alexweissman/824c9e538d3e3545f05acd51e05dfb7d to your computer and use it in GitHub Desktop.
Improvements to ufCollection
/**
* uf-collection plugin. Widget for attaching/detaching related items to a single parent item (e.g. roles for a user, etc).
*
* === USAGE ===
*
* uf-collection can be initialized on a div element as follows:
*
* $("#myCollection").ufCollection(options);
*
* `options` is an object containing any of the following parameters:
* @param {bool} useDropDown Set to true if rows should be added using a select2 dropdown, false for free text inputs.
* @param {Object} dropdown The options to pass to the select2 plugin for the add item dropdown.
* @param {string} dropdown.ajax.url The url from which to fetch options (as JSON data) in the dropdown selector menu.
* @param {string} dropdown.theme The select2 theme to use for the dropdown menu. Defaults to "bootstrap".
* @param {string} dropdown.placeholder Placeholder text to use in the dropdown menu before a selection is made. Defaults to "Item".
* @param {Object} dropdownControl a jQuery selector specifying the dropdown select2 control. Defaults to looking for a .js-select-new element inside the parent object.
* @param {string} dropdownTemplate A Handlebars template to use for rendering the dropdown items.
* @param {Object} rowContainer a jQuery selector specifying the place where rows should be added. Defaults to looking for the first tbody element inside the parent object.
* @param {string} rowTemplate A Handlebars template to use for rendering each row in the table.
*
* == EVENTS ==
*
* ufCollection triggers the following events:
*
* `rowAdd.ufCollection`: triggered when a new row is added to the collection.
* `rowAddVirgin.ufCollection`: triggered when a virgin row is added to the collection (when useDropdown = false).
* `rowDelete.ufCollection`: triggered when a row is removed from the collection.
* `rowTouch.ufCollection`: triggered when any inputs in a row are brought into focus.
*
* UserFrosting https://www.userfrosting.com
* @author Alexander Weissman https://alexanderweissman.com
*/
(function( $ )
{
/**
* The plugin namespace, ie for $('.selector').ufCollection(options)
*
* Also the id for storing the object state via $('.selector').data()
*/
var PLUGIN_NS = 'ufCollection';
var Plugin = function ( target, options ) {
this.$T = $(target);
/** #### OPTIONS #### */
this.options= $.extend(
true, // deep extend
{
useDropdown: true,
dropdown: {
ajax: {
url: "",
dataType: "json",
delay: 250,
data: function (params) {
return {
filters: {
info : params.term
}
};
},
processResults: function (data, params) {
var suggestions = [];
// Process the data into dropdown options
if (data && data['rows']) {
jQuery.each(data['rows'], function(idx, row) {
//if (jQuery.inArray(row.id, base._addedIds)) {
row.text = row.name;
suggestions.push(row);
//}
});
}
return {
results: suggestions
};
},
cache: true
},
placeholder : "Item",
selectOnClose : false, // Make a selection when they click out of the box/press the next button
theme: "default",
width: "100%",
},
dropdownControl : this.$T.find('.js-select-new'),
dropdownTemplate: "",
rowContainer : this.$T.find('tbody').first(),
rowTemplate : "",
DEBUG: false
},
options
);
// Internal counter for adding rows to the collection. Gets updated every time `addRow` is called.
this._rownum = 0;
// Keeps track of which ids already exist in the collection
this._addedIds = [];
// Handlebars template method
this._dropdownTemplateCompiled = Handlebars.compile(this.options.dropdownTemplate);
this._rowTemplateCompiled = Handlebars.compile(this.options.rowTemplate);
this._init( target, this.options );
return this;
}
Plugin.prototype.addRow = function (options) {
var base = this;
base._createRow(options);
return base.$T;
};
Plugin.prototype.addVirginRow = function (options) {
var base = this;
base._createVirginRow(options);
return base.$T;
};
/** #### PRIVATE METHODS #### */
/**
* Initialize the ufCollection widget.
*/
Plugin.prototype._init = function ( target, options ) {
var base = this;
var $el = $(target);
// Add container class
$el.toggleClass("uf-collection", true);
// If we're using dropdown options, create the select2 and add bindings to add a new row when an option is selected
if (base.options.useDropdown) {
base._initDropdownField(base.options.dropdownControl);
base.options.dropdownControl.on("select2:select", function () {
var item = $(this).select2("data");
base.addRow(item);
});
} else {
// Otherwise, add a new virgin row
base.addVirginRow();
}
return this;
};
/**
* Create a new row and attach the handler for deletion to the js-delete-row button
*/
Plugin.prototype._createRow = function (options) {
var base = this;
var params = {
id : "",
rownum: base._rownum
};
// Merge in any prepopulated values for the row
if (typeof options !== 'undefined') {
$.extend(true, params, options[0]);
}
// Generate the row and append to table
var newRowTemplate = base._rowTemplateCompiled(params);
var newRow;
var virginRows = base.options.rowContainer.find('.uf-collection-row-virgin').length;
if (virginRows) {
newRow = $(newRowTemplate).insertBefore(base.options.rowContainer.find('.uf-collection-row-virgin:first'));
} else {
newRow = $(newRowTemplate).appendTo(base.options.rowContainer);
}
// Trigger to delete row
newRow.find('.js-delete-row').on('click', function() {
base._deleteRow($(this).closest('.uf-collection-row'));
});
// Once the new row comes into focus for the first time, it has been "touched"
newRow.find('input').on('focus', function () {
base._touchRow(newRow);
});
base._rownum += 1;
// Fire event when row has been constructed
base.$T.trigger('rowAdd.ufCollection', newRow);
return newRow;
};
/**
* Create a new, blank row with the 'virgin' status.
*/
Plugin.prototype._createVirginRow = function () {
var base = this;
var params = {
id : '',
rownum: base._rownum
};
// Generate the row and append to table
var newRow = base._createRow(params);
// Set the row's 'virgin' status
newRow.addClass('uf-collection-row-virgin');
newRow.find('.js-delete-row').hide();
base.$T.trigger('rowAddVirgin.ufCollection', newRow);
return newRow;
};
/**
* Delete a row from the collection.
*/
Plugin.prototype._deleteRow = function (row) {
var base = this;
row.remove();
base.$T.trigger('rowDelete.ufCollection');
var index = base._addedIds.indexOf(5);
if (index > -1) {
base._addedIds.splice(index, 1);
}
};
/**
* Remove a row's virgin status, show the delete button, and add a new virgin row if needed
*/
Plugin.prototype._touchRow = function (row) {
var base = this;
row.removeClass('uf-collection-row-virgin');
row.find('.js-delete-row').show();
base.$T.trigger('rowTouch.ufCollection', row);
// Assert that the table doesn't already have a virgin row. If not, create a new virgin row.
var virginRows = base.options.rowContainer.find('.uf-collection-row-virgin').length;
if (!virginRows) {
base._createVirginRow();
}
};
Plugin.prototype._initDropdownField = function (field) {
var base = this;
var options = base.options.dropdown;
if (!("templateResult" in options)) {
options.templateResult = function(item) {
// Display loading text if the item is marked as "loading"
if (item.loading) return item.text;
// Must wrap this in a jQuery selector to render as HTML
return $(base._dropdownTemplateCompiled(item));
};
}
// Legacy options (<= v4.0.9)
if ("dataUrl" in base.options) {
options.ajax.url = base.options.dataUrl;
}
if ("ajaxDelay" in base.options) {
options.ajax.delay = base.options.ajaxDelay;
}
if ("dropdownTheme" in base.options) {
options.theme = base.options.dropdownTheme;
}
if ("placeholder" in base.options) {
options.placeholder = base.options.placeholder;
}
if ("selectOnClose" in base.options) {
options.selectOnClose = base.options.selectOnClose;
}
if ("width" in base.options) {
options.width = base.options.width;
}
return field.select2(options);
};
/**
* EZ Logging/Warning (technically private but saving an '_' is worth it imo)
*/
Plugin.prototype.DLOG = function ()
{
if (!this.DEBUG) return;
for (var i in arguments) {
console.log( PLUGIN_NS + ': ', arguments[i] );
}
}
Plugin.prototype.DWARN = function ()
{
this.DEBUG && console.warn( arguments );
}
/*###################################################################################
* JQUERY HOOK
###################################################################################*/
/**
* Generic jQuery plugin instantiation method call logic
*
* Method options are stored via jQuery's data() method in the relevant element(s)
* Notice, myActionMethod mustn't start with an underscore (_) as this is used to
* indicate private methods on the PLUGIN class.
*/
$.fn[ PLUGIN_NS ] = function( methodOrOptions ) {
if (!$(this).length) {
return $(this);
}
var instance = $(this).data(PLUGIN_NS);
// CASE: action method (public method on PLUGIN class)
if ( instance
&& methodOrOptions.indexOf('_') != 0
&& instance[ methodOrOptions ]
&& typeof( instance[ methodOrOptions ] ) == 'function' ) {
return instance[ methodOrOptions ]( Array.prototype.slice.call( arguments, 1 ) );
// CASE: argument is options object or empty = initialise
} else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
instance = new Plugin( $(this), methodOrOptions ); // ok to overwrite if this is a re-init
$(this).data( PLUGIN_NS, instance );
return $(this);
// CASE: method called before init
} else if ( !instance ) {
$.error( 'Plugin must be initialised before using method: ' + methodOrOptions );
// CASE: invalid method
} else if ( methodOrOptions.indexOf('_') == 0 ) {
$.error( 'Method ' + methodOrOptions + ' is private!' );
} else {
$.error( 'Method ' + methodOrOptions + ' does not exist.' );
}
};
})(jQuery);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment