@userfrosting/theme-adminlte
Version:
Admin LTE Theme for UserFrosting 5
346 lines (310 loc) • 13.8 kB
JavaScript
/**
* 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 (see https://ux.stackexchange.com/a/15637/53990).
* @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 {bool} selectOnClose Set to true if you want the currently highlighted dropdown item to be automatically added when the dropdown is closed for any reason.
* @param {string} dropdown.theme The select2 theme to use for the dropdown menu. Defaults to "default".
* @param {string} dropdown.placeholder Placeholder text to use in the dropdown menu before a selection is made. Defaults to "Item".
* @param {string} dropdown.width Width of the dropdown selector, when used. Defaults to "100%".
* @param {callback} transformDropdownSelection Custom transformation on objects from the dropdown before passing them to render in the collection table.
* @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.
* `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($, window, document, undefined) {
"use strict";
// Define plugin name and defaults.
var pluginName = "ufCollection",
defaults = {
useDropdown: true,
dropdown: {
ajax : {
url : "",
dataType : "json",
delay : 250,
data : function (params) {
return {
filters: {
info : params.term
}
};
},
processResults : function (data, params) {
// Process the data into dropdown options
var suggestions = [];
if (data && data.rows) {
suggestions = data.rows;
}
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%",
},
transformDropdownSelection: function (item) {
return item;
},
dropdownControl : null,
dropdownTemplate: "",
rowContainer : null,
rowTemplate : "",
DEBUG : false
};
// Constructor
function Plugin (element, options) {
this.element = element[0];
this.$element = $(this.element);
var lateDefaults = {
dropdownControl: this.$element.find('.js-select-new'),
rowContainer: this.$element.find('tbody').first()
};
this.settings = $.extend(true, {}, defaults, lateDefaults, options);
this._defaults = defaults;
this._name = pluginName;
// Detect changes to element attributes
this.$element.attrchange({ callback: function (event) { this.element = event.target; }.bind(this) });
// Internal counter for adding rows to the collection. Gets updated every time `_createRow` is called.
this._rownum = 0;
// Keep track of last added row
this._lastRow = null;
// Handlebars template method
this._dropdownTemplateCompiled = Handlebars.compile(this.settings.dropdownTemplate);
this._rowTemplateCompiled = Handlebars.compile(this.settings.rowTemplate);
// Add container class
this.$element.toggleClass("uf-collection", true);
// Add bindings for any rows already present in the DOM
$.each(this.settings.rowContainer.find('.uf-collection-row'), $.proxy(function(idx, row) {
this._onNewRow($(row));
this._lastRow = row;
}, this));
// If we're using dropdown options, create the select2 and add bindings to add a new row when an option is selected
if (this.settings.useDropdown) {
this._initDropdownField(this.settings.dropdownControl);
this.settings.dropdownControl.on("select2:select", $.proxy(function(e) {
var item = $(e.target).select2("data")[0];
// Apply any transformations before rendering as a row
var transformed = this.settings.transformDropdownSelection(item);
this._createRow(transformed);
}, this));
}
else {
// Otherwise, add a new virgin row
this._createVirginRow();
}
return this;
}
// Functions
$.extend(Plugin.prototype, {
/**
* Add a new row to the collection, optionally passing in prepopulated template data.
*/
addRow: function(options) {
// Grab params, if any
var params = {};
if (typeof options !== 'undefined') {
params = options[0];
}
this._createRow(params);
return this.$element;
},
/**
* Add a new 'virgin' row to the collection, optionally passing in prepopulated template data.
* Virgin rows are rows that have not yet been brought into focus by the user.
* When a virgin row is brought into focus, it loses its virgin status and a new virgin row is created.
*/
addVirginRow: function(options) {
// Grab params, if any
var params = {};
if (typeof options !== 'undefined') {
params = options[0];
}
this._createVirginRow(params);
return this.$element;
},
/**
* Delete a target row.
*/
deleteRow: function(row) {
this._deleteRow(row);
return this.$element;
},
/**
* Get the dropdown control for the collection, if one exists.
*/
getDropdown: function() {
return this.settings.dropdownControl;
},
/**
* Get the last row added in the collection.
*/
getLastRow: function () {
return this._lastRow;
},
/**
* Touch a target row.
*/
touchRow: function(row) {
this._touchRow(row);
return this.$element;
},
/**
* Create a new row and attach the handler for deletion to the js-delete-row button
*/
_createRow: function(params) {
params = $.extend(true,
{
id: "",
rownum: this._rownum
}, params);
// Generate the row and append to table
var newRowTemplate = this._rowTemplateCompiled(params),
newRow;
// Add the new row before any virgin rows in the table.
var virginRows = this.settings.rowContainer.find('.uf-collection-row-virgin').length;
if (virginRows) {
newRow = $(newRowTemplate).insertBefore(this.settings.rowContainer.find('.uf-collection-row-virgin:first'));
} else {
newRow = $(newRowTemplate).appendTo(this.settings.rowContainer);
}
this._lastRow = newRow;
// Add bindings and fire event
this._onNewRow(newRow);
return newRow;
},
/**
* Create a new, blank row with the 'virgin' status.
*/
_createVirginRow: function(params) {
// Generate the row and append to table
var newRow = this._createRow(params);
// Set the row's 'virgin' status
newRow.addClass('uf-collection-row-virgin');
newRow.find('.js-delete-row').hide();
return newRow;
},
/**
* Delete a row from the collection.
*/
_deleteRow: function(row) {
row.remove();
this.$element.trigger('rowDelete.ufCollection', row);
},
/**
* Add delete and touch bindings for a row, increment the internal row counter, and fire the rowAdd event
*/
_onNewRow: function(row) {
// Trigger to delete row
row.find('.js-delete-row').on('click', $.proxy(function(e) {
this._deleteRow($(e.target).closest('.uf-collection-row'));
}, this));
// Once the new row comes into focus for the first time, it has been "touched"
row.find(':input').on('focus', $.proxy(function() {
this._touchRow(row);
}, this));
this._rownum += 1;
// Fire event when row has been constructed
this.$element.trigger('rowAdd.ufCollection', row);
},
/**
* Remove a row's virgin status, show the delete button, and add a new virgin row if needed
*/
_touchRow: function(row) {
row.removeClass('uf-collection-row-virgin');
row.find('.js-delete-row').show();
this.$element.trigger('rowTouch.ufCollection', row);
// If we're not using dropdowns, assert that the table doesn't already have a virgin row. If not, create a new virgin row.
if (!this.settings.useDropdown) {
var virginRows = this.settings.rowContainer.find('.uf-collection-row-virgin').length;
if (!virginRows) {
this._createVirginRow();
}
}
},
/**
* Initialize the select2 dropdown for this collection on a specified control element.
*/
_initDropdownField: function(field) {
var options = this.settings.dropdown;
if (!("templateResult" in options)) {
options.templateResult = $.proxy(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 $(this._dropdownTemplateCompiled(item));
}, this);
}
// Legacy options (<= v4.0.9)
if ("dataUrl" in this.settings) {
options.ajax.url = this.settings.dataUrl;
}
if ("ajaxDelay" in this.settings) {
options.ajax.delay = this.settings.ajaxDelay;
}
if ("dropdownTheme" in this.settings) {
options.theme = this.settings.dropdownTheme;
}
if ("placeholder" in this.settings) {
options.placeholder = this.settings.placeholder;
}
if ("selectOnClose" in this.settings) {
options.selectOnClose = this.settings.selectOnClose;
}
if ("width" in this.settings) {
options.width = this.settings.width;
}
return field.select2(options);
}
});
// Handles instantiation and access to non-private methods.
$.fn[pluginName] = function(methodOrOptions) {
// Grab plugin instance
var instance = $(this).data(pluginName);
// If undefined or object, initialize plugin.
if (methodOrOptions === undefined || typeof methodOrOptions === 'object') {
// Only initialize if not previously done.
if (!instance) {
$(this).data(pluginName, new Plugin(this, methodOrOptions));
}
return this;
}
// Otherwise ensure first parameter is a valid string, and is the name of an actual function.
else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') {
// Ensure not a private function
if (methodOrOptions.indexOf('_') !== 0) {
return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1));
}
else {
console.warn( 'Method ' + methodOrOptions + ' is private!' );
}
}
else {
console.warn( 'Method ' + methodOrOptions + ' does not exist.' );
}
};
})(jQuery, window, document);