fuelux
Version:
Base Fuel UX styles and controls
856 lines (721 loc) • 24.6 kB
JavaScript
/*
* Fuel UX Repeater
* https://github.com/ExactTarget/fuelux
*
* Copyright (c) 2014 ExactTarget
* Licensed under the BSD New license.
*/
// -- BEGIN UMD WRAPPER PREFACE --
// For more information on UMD visit:
// https://github.com/umdjs/umd/blob/master/jqueryPlugin.js
(function (factory) {
if (typeof define === 'function' && define.amd) {
// if AMD loader is available, register as an anonymous module.
define(['jquery', 'fuelux/combobox', 'fuelux/infinite-scroll', 'fuelux/search', 'fuelux/selectlist'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS
module.exports = factory(require('jquery'), require('./combobox'), require('./infinite-scroll'),
require('./search'), require('./selectlist'));
} else {
// OR use browser globals if AMD is not present
factory(jQuery);
}
}(function ($) {
// -- END UMD WRAPPER PREFACE --
// -- BEGIN MODULE CODE HERE --
var old = $.fn.repeater;
// REPEATER CONSTRUCTOR AND PROTOTYPE
var Repeater = function (element, options) {
var self = this;
var $btn, currentView;
this.$element = $(element);
this.$canvas = this.$element.find('.repeater-canvas');
this.$count = this.$element.find('.repeater-count');
this.$end = this.$element.find('.repeater-end');
this.$filters = this.$element.find('.repeater-filters');
this.$loader = this.$element.find('.repeater-loader');
this.$pageSize = this.$element.find('.repeater-itemization .selectlist');
this.$nextBtn = this.$element.find('.repeater-next');
this.$pages = this.$element.find('.repeater-pages');
this.$prevBtn = this.$element.find('.repeater-prev');
this.$primaryPaging = this.$element.find('.repeater-primaryPaging');
this.$search = this.$element.find('.repeater-search').find('.search');
this.$secondaryPaging = this.$element.find('.repeater-secondaryPaging');
this.$start = this.$element.find('.repeater-start');
this.$viewport = this.$element.find('.repeater-viewport');
this.$views = this.$element.find('.repeater-views');
this.currentPage = 0;
this.currentView = null;
this.isDisabled = false;
this.infiniteScrollingCallback = function () {};
this.infiniteScrollingCont = null;
this.infiniteScrollingEnabled = false;
this.infiniteScrollingEnd = null;
this.infiniteScrollingOptions = {};
this.lastPageInput = 0;
this.options = $.extend({}, $.fn.repeater.defaults, options);
this.pageIncrement = 0;// store direction navigated
this.resizeTimeout = {};
this.stamp = new Date().getTime() + (Math.floor(Math.random() * 100) + 1);
this.storedDataSourceOpts = null;
this.syncingViewButtonState = false;
this.viewOptions = {};
this.viewType = null;
this.$filters.selectlist();
this.$pageSize.selectlist();
this.$primaryPaging.find('.combobox').combobox();
this.$search.search({
searchOnKeyPress: this.options.searchOnKeyPress
});
this.$filters.on('changed.fu.selectlist', function (e, value) {
self.$element.trigger('filtered.fu.repeater', value);
self.render({
clearInfinite: true,
pageIncrement: null
});
});
this.$nextBtn.on('click.fu.repeater', $.proxy(this.next, this));
this.$pageSize.on('changed.fu.selectlist', function (e, value) {
self.$element.trigger('pageSizeChanged.fu.repeater', value);
self.render({
pageIncrement: null
});
});
this.$prevBtn.on('click.fu.repeater', $.proxy(this.previous, this));
this.$primaryPaging.find('.combobox').on('changed.fu.combobox', function (evt, data) {
self.$element.trigger('pageChanged.fu.repeater', [data.text, data]);
self.pageInputChange(data.text);
});
this.$search.on('searched.fu.search cleared.fu.search', function (e, value) {
self.$element.trigger('searchChanged.fu.repeater', value);
self.render({
clearInfinite: true,
pageIncrement: null
});
});
this.$secondaryPaging.on('blur.fu.repeater', function (e) {
self.pageInputChange(self.$secondaryPaging.val());
});
this.$secondaryPaging.on('keyup', function (e) {
if (e.keyCode === 13) {
self.pageInputChange(self.$secondaryPaging.val());
}
});
this.$views.find('input').on('change.fu.repeater', $.proxy(this.viewChanged, this));
// ID needed since event is bound to instance
$(window).on('resize.fu.repeater.' + this.stamp, function (event) {
clearTimeout(self.resizeTimeout);
self.resizeTimeout = setTimeout(function () {
self.resize();
self.$element.trigger('resized.fu.repeater');
}, 75);
});
this.$loader.loader();
this.$loader.loader('pause');
if (this.options.defaultView !== -1) {
currentView = this.options.defaultView;
} else {
$btn = this.$views.find('label.active input');
currentView = ($btn.length > 0) ? $btn.val() : 'list';
}
this.setViewOptions(currentView);
this.initViewTypes(function () {
self.resize();
self.$element.trigger('resized.fu.repeater');
self.render({
changeView: currentView
});
});
};
Repeater.prototype = {
constructor: Repeater,
clear: function (options) {
var viewChanged, viewTypeObj;
function scan (cont) {
var keep = [];
cont.children().each(function () {
var item = $(this);
var pres = item.attr('data-preserve');
if (pres === 'deep') {
item.detach();
keep.push(item);
} else if (pres === 'shallow') {
scan(item);
item.detach();
keep.push(item);
}
});
cont.empty();
cont.append(keep);
}
options = options || {};
if (!options.preserve) {
//Just trash everything because preserve is false
this.$canvas.empty();
} else if (!this.infiniteScrollingEnabled || options.clearInfinite) {
//Preserve clear only if infiniteScrolling is disabled or if specifically told to do so
scan(this.$canvas);
} //Otherwise don't clear because infiniteScrolling is enabled
//If viewChanged and current viewTypeObj has a cleared function, call it
viewChanged = (options.viewChanged !== undefined) ? options.viewChanged : false;
viewTypeObj = $.fn.repeater.viewTypes[this.viewType] || {};
if (!viewChanged && viewTypeObj.cleared) {
viewTypeObj.cleared.call(this, {
options: options
});
}
},
clearPreservedDataSourceOptions: function () {
this.storedDataSourceOpts = null;
},
destroy: function () {
var markup;
// set input value attrbute in markup
this.$element.find('input').each(function () {
$(this).attr('value', $(this).val());
});
// empty elements to return to original markup
this.$canvas.empty();
markup = this.$element[0].outerHTML;
// destroy components and remove leftover
this.$element.find('.combobox').combobox('destroy');
this.$element.find('.selectlist').selectlist('destroy');
this.$element.find('.search').search('destroy');
if (this.infiniteScrollingEnabled) {
$(this.infiniteScrollingCont).infinitescroll('destroy');
}
this.$element.remove();
// any external events
$(window).off('resize.fu.repeater.' + this.stamp);
return markup;
},
disable: function() {
var disable = 'disable';
var disabled = 'disabled';
var viewTypeObj = $.fn.repeater.viewTypes[this.viewType] || {};
this.$search.search(disable);
this.$filters.selectlist(disable);
this.$views.find('label, input').addClass(disabled).attr(disabled, disabled);
this.$pageSize.selectlist(disable);
this.$primaryPaging.find('.combobox').combobox(disable);
this.$secondaryPaging.attr(disabled, disabled);
this.$prevBtn.attr(disabled, disabled);
this.$nextBtn.attr(disabled, disabled);
if (viewTypeObj.enabled) {
viewTypeObj.enabled.call(this, {
status: false
});
}
this.isDisabled = true;
this.$element.addClass('disabled');
this.$element.trigger('disabled.fu.repeater');
},
enable: function() {
var disabled = 'disabled';
var enable = 'enable';
var pageEnd = 'page-end';
var viewTypeObj = $.fn.repeater.viewTypes[this.viewType] || {};
this.$search.search(enable);
this.$filters.selectlist(enable);
this.$views.find('label, input').removeClass(disabled).removeAttr(disabled);
this.$pageSize.selectlist('enable');
this.$primaryPaging.find('.combobox').combobox(enable);
this.$secondaryPaging.removeAttr(disabled);
if(!this.$prevBtn.hasClass(pageEnd)){
this.$prevBtn.removeAttr(disabled);
}
if(!this.$nextBtn.hasClass(pageEnd)){
this.$nextBtn.removeAttr(disabled);
}
// is 0 or 1 pages, if using $primaryPaging (combobox)
// if using selectlist allow user to use selectlist to select 0 or 1
if (this.$prevBtn.hasClass(pageEnd) && this.$nextBtn.hasClass(pageEnd)) {
this.$primaryPaging.combobox('disable');
}
//if there are no items
if (parseInt(this.$count.html()) !== 0) {
this.$pageSize.selectlist('enable');
}
else {
this.$pageSize.selectlist('disable');
}
if (viewTypeObj.enabled) {
viewTypeObj.enabled.call(this, {
status: true
});
}
this.isDisabled = false;
this.$element.removeClass('disabled');
this.$element.trigger('enabled.fu.repeater');
},
getDataOptions: function (options) {
var dataSourceOptions = {};
var opts = {};
var val, viewDataOpts;
options = options || {};
opts.filter = (this.$filters.length > 0) ? this.$filters.selectlist('selectedItem') : {
text: 'All',
value: 'all'
};
opts.view = this.currentView;
if (!this.infiniteScrollingEnabled) {
opts.pageSize = (this.$pageSize.length > 0) ? parseInt(this.$pageSize.selectlist('selectedItem').value, 10) : 25;
}
if (options.pageIncrement !== undefined) {
if (options.pageIncrement === null) {
this.currentPage = 0;
} else {
this.currentPage += options.pageIncrement;
}
}
opts.pageIndex = this.currentPage;
val = (this.$search.length > 0) ? this.$search.find('input').val() : '';
if (val !== '') {
opts.search = val;
}
if (options.dataSourceOptions) {
dataSourceOptions = options.dataSourceOptions;
if (options.preserveDataSourceOptions) {
this.storedDataSourceOpts = (this.storedDataSourceOpts) ? $.extend(this.storedDataSourceOpts, dataSourceOptions) : dataSourceOptions;
}
}
if (this.storedDataSourceOpts) {
dataSourceOptions = $.extend(this.storedDataSourceOpts, dataSourceOptions);
}
viewDataOpts = $.fn.repeater.viewTypes[this.viewType] || {};
viewDataOpts = viewDataOpts.dataOptions;
if (viewDataOpts) {
viewDataOpts = viewDataOpts.call(this, opts);
opts = $.extend(viewDataOpts, dataSourceOptions);
} else {
opts = $.extend(opts, dataSourceOptions);
}
return opts;
},
infiniteScrolling: function (enable, options) {
var footer = this.$element.find('.repeater-footer');
var viewport = this.$element.find('.repeater-viewport');
var cont, data;
options = options || {};
if (enable) {
this.infiniteScrollingEnabled = true;
this.infiniteScrollingEnd = options.end;
delete options.dataSource;
delete options.end;
this.infiniteScrollingOptions = options;
viewport.css({
height: viewport.height() + footer.outerHeight()
});
footer.hide();
} else {
cont = this.infiniteScrollingCont;
data = cont.data();
delete data.infinitescroll;
cont.off('scroll');
cont.removeClass('infinitescroll');
this.infiniteScrollingCont = null;
this.infiniteScrollingEnabled = false;
this.infiniteScrollingEnd = null;
this.infiniteScrollingOptions = {};
viewport.css({
height: viewport.height() - footer.outerHeight()
});
footer.show();
}
},
infiniteScrollPaging: function (data, options) {
var end = (this.infiniteScrollingEnd !== true) ? this.infiniteScrollingEnd : undefined;
var page = data.page;
var pages = data.pages;
this.currentPage = (page !== undefined) ? page : NaN;
if (data.end === true || (this.currentPage + 1) >= pages) {
this.infiniteScrollingCont.infinitescroll('end', end);
}
},
initInfiniteScrolling: function () {
var cont = this.$canvas.find('[data-infinite="true"]:first');
var opts, self;
cont = (cont.length < 1) ? this.$canvas : cont;
if (cont.data('fu.infinitescroll')) {
cont.infinitescroll('enable');
} else {
self = this;
opts = $.extend({}, this.infiniteScrollingOptions);
opts.dataSource = function (helpers, callback) {
self.infiniteScrollingCallback = callback;
self.render({
pageIncrement: 1
});
};
cont.infinitescroll(opts);
this.infiniteScrollingCont = cont;
}
},
initViewTypes: function (callback) {
var self = this;
var viewTypes = [];
var i, viewTypesLength;
function init (index) {
function next () {
index++;
if (index < viewTypesLength) {
init(index);
} else {
callback();
}
}
if (viewTypes[index].initialize) {
viewTypes[index].initialize.call(self, {}, function () {
next();
});
} else {
next();
}
}
for (i in $.fn.repeater.viewTypes) {
viewTypes.push($.fn.repeater.viewTypes[i]);
}
viewTypesLength = viewTypes.length;
if (viewTypesLength > 0) {
init(0);
} else {
callback();
}
},
itemization: function (data) {
this.$count.html((data.count!==undefined) ? data.count : '?');
this.$end.html((data.end!==undefined) ? data.end : '?');
this.$start.html((data.start!==undefined) ? data.start : '?');
},
next: function (e) {
var d = 'disabled';
this.$nextBtn.attr(d, d);
this.$prevBtn.attr(d, d);
this.pageIncrement = 1;
this.$element.trigger('nextClicked.fu.repeater');
this.render({
pageIncrement: this.pageIncrement
});
},
pageInputChange: function (val) {
var pageInc;
if (val !== this.lastPageInput) {
this.lastPageInput = val;
val = parseInt(val, 10) - 1;
pageInc = val - this.currentPage;
this.$element.trigger('pageChanged.fu.repeater', val);
this.render({
pageIncrement: pageInc
});
}
},
pagination: function (data) {
var act = 'active';
var dsbl = 'disabled';
var page = data.page;
var pageEnd = 'page-end';
var pages = data.pages;
var dropMenu, i, l;
var currenPageOutput;
this.currentPage = (page !== undefined) ? page : NaN;
this.$primaryPaging.removeClass(act);
this.$secondaryPaging.removeClass(act);
// set paging to 0 if total pages is 0, otherwise use one-based index
currenPageOutput = pages === 0 ? 0 : this.currentPage + 1;
if (pages <= this.viewOptions.dropPagingCap) {
this.$primaryPaging.addClass(act);
dropMenu = this.$primaryPaging.find('.dropdown-menu');
dropMenu.empty();
for (i = 0; i < pages; i++) {
l = i + 1;
dropMenu.append('<li data-value="' + l + '"><a href="#">' + l + '</a></li>');
}
this.$primaryPaging.find('input.form-control').val(currenPageOutput);
} else {
this.$secondaryPaging.addClass(act);
this.$secondaryPaging.val(currenPageOutput);
}
this.lastPageInput = this.currentPage + 1 + '';
this.$pages.html('' + pages);
// this is not the last page
if ((this.currentPage + 1) < pages) {
this.$nextBtn.removeAttr(dsbl);
this.$nextBtn.removeClass(pageEnd);
} else {
this.$nextBtn.attr(dsbl, dsbl);
this.$nextBtn.addClass(pageEnd);
}
// this is not the first page
if ((this.currentPage - 1) >= 0) {
this.$prevBtn.removeAttr(dsbl);
this.$prevBtn.removeClass(pageEnd);
} else {
this.$prevBtn.attr(dsbl, dsbl);
this.$prevBtn.addClass(pageEnd);
}
// return focus to next/previous buttons after navigating
if (this.pageIncrement !== 0) {
if (this.pageIncrement > 0) {
if (this.$nextBtn.is(':disabled')) {
// if you can't focus, go the other way
this.$prevBtn.focus();
} else {
this.$nextBtn.focus();
}
} else {
if (this.$prevBtn.is(':disabled')) {
// if you can't focus, go the other way
this.$nextBtn.focus();
} else {
this.$prevBtn.focus();
}
}
}
},
previous: function () {
var d = 'disabled';
this.$nextBtn.attr(d, d);
this.$prevBtn.attr(d, d);
this.pageIncrement = -1;
this.$element.trigger('previousClicked.fu.repeater');
this.render({
pageIncrement: this.pageIncrement
});
},
render: function (options) {
var self = this;
var viewChanged = false;
var viewTypeObj = $.fn.repeater.viewTypes[this.viewType] || {};
var dataOptions, prevView;
options = options || {};
this.disable();
if (options.changeView && (this.currentView !== options.changeView)) {
prevView = this.currentView;
this.currentView = options.changeView;
this.viewType = this.currentView.split('.')[0];
this.setViewOptions(this.currentView);
this.$element.attr('data-currentview', this.currentView);
this.$element.attr('data-viewtype', this.viewType);
viewChanged = true;
options.viewChanged = viewChanged;
this.$element.trigger('viewChanged.fu.repeater', this.currentView);
if (this.infiniteScrollingEnabled) {
self.infiniteScrolling(false);
}
viewTypeObj = $.fn.repeater.viewTypes[this.viewType] || {};
if (viewTypeObj.selected) {
viewTypeObj.selected.call(this, {
prevView: prevView
});
}
}
this.syncViewButtonState();
options.preserve = (options.preserve !== undefined) ? options.preserve : !viewChanged;
this.clear(options);
if (!this.infiniteScrollingEnabled || (this.infiniteScrollingEnabled && viewChanged)) {
this.$loader.show().loader('play');
}
dataOptions = this.getDataOptions(options);
this.viewOptions.dataSource(dataOptions, function (data) {
data = data || {};
if (self.infiniteScrollingEnabled) {
// pass empty object because data handled in infiniteScrollPaging method
self.infiniteScrollingCallback({});
} else {
self.itemization(data);
self.pagination(data);
}
self.runRenderer(viewTypeObj, data, function () {
if (self.infiniteScrollingEnabled) {
if (viewChanged || options.clearInfinite) {
self.initInfiniteScrolling();
}
self.infiniteScrollPaging(data, options);
}
self.$loader.hide().loader('pause');
self.enable();
self.$element.trigger('rendered.fu.repeater', {
data: data,
options: dataOptions,
renderOptions: options
});
//for maintaining support of 'loaded' event
self.$element.trigger('loaded.fu.repeater', dataOptions);
});
});
},
resize: function () {
var staticHeight = (this.viewOptions.staticHeight === -1) ? this.$element.attr('data-staticheight') : this.viewOptions.staticHeight;
var viewTypeObj = {};
var height, viewportMargins;
if (this.viewType) {
viewTypeObj = $.fn.repeater.viewTypes[this.viewType] || {};
}
if (staticHeight !== undefined && staticHeight !== false && staticHeight !== 'false') {
this.$canvas.addClass('scrolling');
viewportMargins = {
bottom: this.$viewport.css('margin-bottom'),
top: this.$viewport.css('margin-top')
};
var staticHeightValue = (staticHeight === 'true' || staticHeight === true) ? this.$element.height() : parseInt(staticHeight, 10);
var headerHeight = this.$element.find('.repeater-header').outerHeight();
var footerHeight = this.$element.find('.repeater-footer').outerHeight();
var bottomMargin = (viewportMargins.bottom === 'auto') ? 0 : parseInt(viewportMargins.bottom, 10);
var topMargin = (viewportMargins.top === 'auto') ? 0 : parseInt(viewportMargins.top, 10);
height = staticHeightValue - headerHeight - footerHeight - bottomMargin - topMargin;
this.$viewport.outerHeight(height);
} else {
this.$canvas.removeClass('scrolling');
}
if (viewTypeObj.resize) {
viewTypeObj.resize.call(this, {
height: this.$element.outerHeight(),
width: this.$element.outerWidth()
});
}
},
runRenderer: function (viewTypeObj, data, callback) {
var $container, i, l, response, repeat, subset;
function addItem ($parent, resp) {
var action;
if (resp) {
action = (resp.action) ? resp.action : 'append';
if (action !== 'none' && resp.item !== undefined) {
$parent = (resp.container !== undefined) ? $(resp.container) : $parent;
$parent[action](resp.item);
}
}
}
if (!viewTypeObj.render) {
if (viewTypeObj.before) {
response = viewTypeObj.before.call(this, {
container: this.$canvas,
data: data
});
addItem(this.$canvas, response);
}
$container = this.$canvas.find('[data-container="true"]:last');
$container = ($container.length > 0) ? $container : this.$canvas;
if (viewTypeObj.renderItem) {
repeat = viewTypeObj.repeat || 'data.items';
repeat = repeat.split('.');
if (repeat[0] === 'data' || repeat[0] === 'this') {
subset = (repeat[0] === 'this') ? this : data;
repeat.shift();
} else {
repeat = [];
subset = [];
if (window.console && window.console.warn) {
window.console.warn('WARNING: Repeater plugin "repeat" value must start with either "data" or "this"');
}
}
for (i = 0, l = repeat.length; i < l; i++) {
if (subset[repeat[i]] !== undefined){
subset = subset[repeat[i]];
} else {
subset = [];
if (window.console && window.console.warn) {
window.console.warn('WARNING: Repeater unable to find property to iterate renderItem on.');
}
break;
}
}
for (i = 0, l = subset.length; i < l; i++) {
response = viewTypeObj.renderItem.call(this, {
container: $container,
data: data,
index: i,
subset: subset
});
addItem($container, response);
}
}
if (viewTypeObj.after) {
response = viewTypeObj.after.call(this, {
container: this.$canvas,
data: data
});
addItem(this.$canvas, response);
}
callback();
} else {
viewTypeObj.render.call(this, {
container: this.$canvas,
data: data
}, function(){
callback();
});
}
},
setViewOptions: function (curView) {
var opts = {};
var viewName = curView.split('.')[1];
if (this.options.views) {
opts = this.options.views[viewName] || this.options.views[curView] || {};
} else {
opts = {};
}
this.viewOptions = $.extend({}, this.options, opts);
},
viewChanged: function (e) {
var $selected = $(e.target);
var val = $selected.val();
if (!this.syncingViewButtonState) {
if (this.isDisabled || $selected.parents('label:first').hasClass('disabled')) {
this.syncViewButtonState();
} else {
this.render({
changeView: val,
pageIncrement: null
});
}
}
},
syncViewButtonState: function () {
var $itemToCheck = this.$views.find('input[value="' + this.currentView + '"]');
this.syncingViewButtonState = true;
this.$views.find('input').prop('checked', false);
this.$views.find('label.active').removeClass('active');
if ($itemToCheck.length > 0) {
$itemToCheck.prop('checked', true);
$itemToCheck.parents('label:first').addClass('active');
}
this.syncingViewButtonState = false;
}
};
// REPEATER PLUGIN DEFINITION
$.fn.repeater = function (option) {
var args = Array.prototype.slice.call(arguments, 1);
var methodReturn;
var $set = this.each(function () {
var $this = $(this);
var data = $this.data('fu.repeater');
var options = typeof option === 'object' && option;
if (!data) {
$this.data('fu.repeater', (data = new Repeater(this, options)));
}
if (typeof option === 'string') {
methodReturn = data[option].apply(data, args);
}
});
return (methodReturn === undefined) ? $set : methodReturn;
};
$.fn.repeater.defaults = {
dataSource: function (options, callback) {
callback({ count: 0, end: 0, items: [], page: 0, pages: 1, start: 0 });
},
defaultView: -1, //should be a string value. -1 means it will grab the active view from the view controls
dropPagingCap: 10,
staticHeight: -1, //normally true or false. -1 means it will look for data-staticheight on the element
views: null, //can be set to an object to configure multiple views of the same type,
searchOnKeyPress: false
};
$.fn.repeater.viewTypes = {};
$.fn.repeater.Constructor = Repeater;
$.fn.repeater.noConflict = function () {
$.fn.repeater = old;
return this;
};
// -- BEGIN UMD WRAPPER AFTERWORD --
}));
// -- END UMD WRAPPER AFTERWORD --