Skip to content

Instantly share code, notes, and snippets.

@rgwozdz
Last active August 29, 2015 13:56
Show Gist options
  • Save rgwozdz/9198441 to your computer and use it in GitHub Desktop.
Save rgwozdz/9198441 to your computer and use it in GitHub Desktop.
_SPDEV.TableWithDetails = (function (module) {
// View for the whole header
var HeaderCollectionView = Backbone.CollectionView.extend({
initialize: function (args) {
this.viewClass = (typeof args.viewClass === 'undefined') ? Backbone.View : args.viewClass;
// Create an array property that will store model views contained in this collection view
this.componentViews = [];
// Loop thru the collection
this.collection.forEach( function(colModel, index) {
// Create a view for each model contained in this view's referenced collection
var modelView = new this.viewClass({model:colModel, table: args.table});
// Store this in the collection view's componentView array
this.componentViews.push(modelView);
}, this);
},
className: 'table-headings row'
});
// View for an individual column header
var ColumnHeaderModelView = Backbone.View.extend({
initialize : function(args) {
// Bind a function to a change in sort property
this.model.bind('change:sort', this.sortStateChange, this);
// Reference the 'table' control - by 'table' I mean the whole control, headers, rows, paging footer
this.table = args.table;
// Add CSS class from model
this.$el.addClass(this.model.get('className'));
},
events: {'click': 'onClick'},
onClick: function(){
// sort if not sorted on this field; if already sorted, reverse sort
if(this.model.get('sort') === false) {
// Loop thru collection
this.model.collection.forEach(function(collModel){
// if collection model doesn't match clicked view's model. Set sort attribute to false
// if it does match, set sort = true and sort Dir to 'asc'
if(collModel !== this.model) {
collModel.set({sort: false});
} else {
collModel.set({sort: true, sortDir: 'asc'});
}
}, this);
} else {
// Reverse the sort
if(this.model.get('sortDir') === 'desc'){
this.model.set({sortDir: 'asc'});
} else {
this.model.set({sortDir: 'desc'});
}
}
// Update view for sort direction arrows
this.sortDirChange();
// Fire off the table's sort method; this is a AJAX to DB
this.table.sort({orderBy : this.model.get('modelProp') + ' ' + this.model.get('sortDir')});
},
sortStateChange: function(){
// Update css classes for sort
if(this.model.get('sort') === false) {
this.$el.removeClass('sort').removeClass('desc').removeClass('asc');
} else {
this.$el.toggleClass('sort', true);
}
},
// Adjust CSS classes for header fields on sort direction change
sortDirChange: function() {
// Update css classes for sort direction
if(this.model.get('sortDir') === 'asc') {
this.$el.toggleClass('asc', true);
this.$el.removeClass('desc');
} else {
this.$el.toggleClass('desc', true);
this.$el.removeClass('asc');
}
},
tagName: 'li',
className: 'cell',
template: _.template('<%= label %>'),
render: function(){
this.$el.html(this.template(this.model.attributes));
}
});
// Collection that holds all records returned from DB
var RowCollection = Backbone.Collection.extend({
initialize: function(){
this.sortModelProp = null;
}
});
// View of all table rows
var RowCollectionView = Backbone.CollectionView.extend({
initialize: function (args) {
this.viewClass = (typeof args.viewClass === 'undefined') ? Backbone.View : args.viewClass;
this.columnMap = args.columnMap;
// Create an array property that will store model views contained in this collection view
this.componentViews = [];
// Loop thru the collection
this.collection.forEach( function(colModel, index) {
// Create a view for each model contained in this view's referenced collection; pass in columnMap
var modelView = new this.viewClass({model:colModel, columnMap: args.columnMap, detailClass: args.detailClass});
// Store this in the collection view's componentView array
this.componentViews.push(modelView);
}, this);
this.collection.sortModelProp = _.findWhere(this.columnMap, {sort: true}).modelProp;
},
className: 'table-rows',
destroyAllViews: function(){
_.each(this.componentViews, function(view){
if (typeof view.unbindDetailView === 'function') {
view.unbindDetailView();
}
view.remove();
view.unbind();
});
this.componentViews.length = 0;
}
});
// View for a row
var RowModelView = Backbone.View.extend({
initialize: function(args){
// Make sure the column mapper is sorted properly
function comparator(a,b) {
var propName = 'colIndex';
if (a[propName] < b[propName])
return -1;
if (a[propName] > b[propName])
return 1;
return 0;
}
var columnMap, rowTemplateString;
columnMap = args.columnMap.sort(comparator);
// row view template - it's dynamic and depends on the column map, that's why it's located here
this.rowTemplate = _.template('<ul class="row">' +
'<li class="compare column-0 cell"><span></span></li>' +
'<li class="column-1 cell"><%= ' + columnMap[1].modelProp + ' %></li>' +
'<li class="column-2 cell"><%= ' + columnMap[2].modelProp + ' %></li>' +
'<li class="column-3 cell"><%= ' + columnMap[3].modelProp + ' %></li>' +
'</ul>');
// Element to hold detailed information about the row entity
this.$detailPane = $('<div class="row-detail-pane"><div class="row-detail-thumb-wrapper"><div class="row-detail-thumb"></div></div></div>');
this.$detailLoader = $('<div class="wall-to-wall module-loader"><div class="absolute-center"></div></div>');
// Is the row currently expanded so user can see details
this.expanded = false;
// Have the details for this row already been retrieved from DB on a previous user interaction
this.detailsRetrieved = false;
this.detailView = null;
this.DetailClass = args.detailClass;
this.detail
this.slideSpeed = args.slideSpeed || 0.750;
this.detailHeight = args.detailViewHeight || 250;
},
events : {'click .row' : 'onRowClick', 'click .row-detail-thumb' : 'onRowDetailThumbClick'},
onRowClick: function(){
// This animation sequence is tricky. Has to be done in a couple of stages. because of absolute posiitoning of
// content within the wrapper, the height animation of the wrapper and content have to be execute separately
var rowHeight, speed, rowWrapper, height, remainingPaneDuration, rowHeightDuration;
// Get the row height
rowHeight = this.$el.find('ul.row').outerHeight();
// Set an animate speed: px/ms
speed = this.slideSpeed;
// Final height of detail content element
height = this.detailHeight;
// Set some vars
$detailPane = this.$detailPane;
$rowWrapper = this.$el;
// Now calculate animate durations - each animatiion is a different height, but the speed is the same
remainingPaneDuration = (height - rowHeight) * 1/speed;
rowHeightDuration = rowHeight * 1/speed;
if(this.expanded === false) {
// Animate the height of details element so that it slides over the table row
$detailPane.animate({height: rowHeight}, {easing: 'linear', duration:rowHeightDuration, complete: function(){
// Now, animate the height of the wrapper, and continue the height animation of the detail element
$rowWrapper.animate({height: height + 2}, {easing: 'linear', duration:remainingPaneDuration});
$detailPane.animate({height: height}, {easing: 'linear', duration:remainingPaneDuration});
}});
// mark this as expanded
this.expanded = true;
// Only need to create the infobox the first time the row is expanded
if(this.detailsRetrieved === false) {
this.getDetail(this.detailPostData);
this.detailsRetrieved = true;
}
} else {
}
},
getDetail: function(postData){
// Instaniate an infobox
this.detailView = new this.DetailClass(postData, dataUrl, $.proxy(function(){
this.$detailLoader.hide();
},this));
$detailPane.append(this.detailView);
},
onRowDetailThumbClick : function(){
var rowHeight, speed, rowWrapper, height, remainingPaneDuration, rowHeightDuration;
// Get the row height
rowHeight = this.$el.find('ul.row').outerHeight();
// Set an animate speed: px/ms
speed = this.slideSpeed;
// Final height of detail content element
height = this.detailHeight;
// Set some vars
$detailPane = this.$detailPane;
$rowWrapper = this.$el;
// Now calculate animate durations - each animatiion is a different height, but the speed is the same
remainingPaneDuration = (height - rowHeight) * 1/speed;
rowHeightDuration = rowHeight * 1/speed;
// Slide the row closed
$detailPane.animate({height: rowHeight}, {easing: 'linear', duration:remainingPaneDuration, complete: function() {
$detailPane.animate({height: 0}, {easing: 'linear', duration:rowHeightDuration});
}});
$rowWrapper.animate({height: rowHeight}, {easing: 'linear', duration:remainingPaneDuration});
this.expanded = false;
},
className: 'table-row-wrapper',
rowTemplate: null,
render: function(){
this.$el.html(this.rowTemplate(this.model.attributes));
this.$el.append(this.$detailPane);
},
unbindInfoHeader: function(){
this.detailsInfobox.titleBar.off('click');
}
});
// Collection of view's that represent table footer's paging numbers
var PageCollectionView = Backbone.CollectionView.extend({
tagName: 'ul',
});
// View that represent one of the table footer's paging numbers
var PageModelView = Backbone.View.extend({
initialize: function(){
// Bind a function to a change in 'active' state
this.model.bind('change:active', this.onStateChange, this);
// Fire the onStateChange right away - it will set the active page
this.onStateChange(this.model);
},
tagName: 'li',
events: {'click' : 'onPageClick'},
onPageClick: function(){
// If this page already the one that is visible, exit
if(this.model.get('active') === true) {
return;
}
var table = this.model.get('table');
// set the new page
table.currentPage = this.model.get('page');
// Set the new paging offset
table.postData.pagingOffset = (table.currentPage - 1) * table.postData.limit;
// Set this view's model 'active' property
this.model.collection.forEach(function(collModel){
if (this.model === collModel) {
collModel.set({active : true});
} else {
collModel.set({active : false});
}
}, this);
// Update the table as per "page" click request
table.getRecords(table.postData);
// Adjust the footer as necessary (next or previous buttons might need to be hidden)
table.pagerAdjustment();
},
onStateChange : function(model){
// Adjust CSS when active state changes
if(model.get('active') === true) {
this.$el.addClass('active');
} else {
this.$el.removeClass('active');
}
},
render: function(){
// Only show page number if hidden attribute === false
if(this.model.get('hidden') === false) {
this.$el.html(this.model.get('page'))
}
}
});
module.Manager = function(columnMap, initialPostData, getRecsUrl, getCntUrl,detailClass) {
this.getRecsUrl = getRecsUrl;
this.getCntUrl = getCntUrl;
this.$el = $('<div class="clearfix table-wrapper"></div>');
this.$moduleLoader = $('<div class="wall-to-wall module-loader"><div class="absolute-center"></div></div>').appendTo(this.$el);
this.initialLoad = false;
this.queueFiltering = false;
this.DetailClass = detailClass;
// Create data for header from column map
this.columnData = [{colIndex: 0, label: 'Compare', className: 'compare column-0', sort: false, sortDir: 'asc'}];
_.each(columnMap, function(col){
var className = 'column-' + col.colIndex;
var sort = col.sort || false;
var sortDir = col.sortDir || 'asc';
if(sort === true) {
className = className + ' sort';
}
if(typeof col.sortDir !== 'undefined') {
className = className + ' ' + col.sortDir;
}
this.columnData.push({colIndex: col.colIndex, modelProp: col.modelProp, label: col.label, className: className, sort: sort, sortDir: sortDir});
}, this);
this.sortClause = _.findWhere(this.columnData, {sort: true}).modelProp + ' asc';
// Data when table is instantiated
this.postData = initialPostData;
var headerColl, headerCollView;
// Table Header
headerColl = new Backbone.Collection(this.columnData);
headerCollView = new HeaderCollectionView({collection: headerColl, viewClass: ColumnHeaderModelView, table: this});
headerCollView.render();
this.$el.append(headerCollView.el);
this.header = headerCollView.el;
this.rowsWrapper = $('<div class="table-rows-wrapper"></div>');
this.$el.append(this.rowsWrapper);
// Table Footer
this.footer = $('<ul class="pagination"><li class="previous">Previous</li>' +
'<li class="pages"></li><li class="records"></li>' +
'<li class="next">Next</li></ul>');
this.$el.append(this.footer);
this.previous = $(this.footer).children('.previous');
this.next = $(this.footer).children('.next');
// Table Rows
this.rColl;
this.rowCollView;
this.pages = null;
this.currentPage = 1;
$(this.previous).on('click', $.proxy(function(){
this.currentPage = this.currentPage - 1;
this.pagerAdjustment();
this.postData.pagingOffset = (this.currentPage - 1) * this.postData.limit;
this.getRecords(this.postData);
this.getRecordCount(this.postData);
this.pColl.forEach(function(model){
if(model.get('page') === this.currentPage) {
model.set({active: true});
} else {
model.set({active: false});
}
}, this);
}, this));
$(this.next).on('click', $.proxy(function(){
var changePageRange = (this.currentPage%10 === 0) ? true : false;
this.currentPage = this.currentPage + 1;
this.pagerAdjustment();
this.postData.pagingOffset = (this.currentPage - 1) * this.postData.limit;
this.getRecords(this.postData);
this.getRecordCount(this.postData);
this.pColl.forEach(function(model){
if(model.get('page') === this.currentPage) {
model.set({active: true});
} else {
model.set({active: false});
}
}, this);
},this));
};
module.Manager.prototype.loadData = function(onlyIfVisible){
if(onlyIfVisible) {
// Build the chart if this module's view is visible
if(this.$el.is(':visible')) {
this.getRecords(this.postData);
this.getRecordCount(this.postData);
this.initialLoad = true;
} else {
this.$moduleLoader.show();
this.queueFiltering = false;
}
} else {
this.getRecords(this.postData);
this.getRecordCount(this.postData);
this.initialLoad = true;
}
};
module.Manager.prototype.sort = function(sortInfo){
this.postData['orderBy'] = sortInfo.orderBy;
this.postData.pagingOffset = null;
this.currentPage = 1;
this.getRecords(this.postData);
this.getRecordCount(this.postData);
};
module.Manager.prototype.getRecords = function(postData){
this.$moduleLoader.show();
if (typeof this.rCollView !== 'undefined'){
this.rCollView.destroyAllViews();
this.rCollView.remove();
}
// Get the data for the table
$.ajax({
context: this,
type: 'POST',
data: postData,
'dataType' : "json",
'url' : this.getRecsUrl,
'success' : function(data) {
// Make the collection
this.rColl = new Backbone.Collection(data);
this.rCollView = new RowCollectionView({
collection: this.rColl,
viewClass: RowModelView,
tableId: this.dataSourceId,
columnMap: this.columnData,
detailClass: this.DetailClass});
this.rCollView.render();
this.rowsWrapper.append(this.rCollView.el);
this.$moduleLoader.hide();
},
'error' : function(response) {
// TODO: error handling
console.error(response);
}
});
};
module.Manager.prototype.getRecordCount = function(postData){
if (typeof this.pCollView !== 'undefined'){
this.pCollView.destroyAllViews();
this.pCollView.remove();
}
// Get the Count of activities
$.ajax({
context: this,
type: 'POST',
data: postData,
'dataType' : "json",
'url' : this.getCntUrl,
'success' : function(data, textStatus, jqXHR) {
var obj, num, pageArr, currentPageOffset,
range, limit, currentPage, totalPages, pagingOffset,
start, end;
pagingOffset = this.postData.pagingOffset;
limit = this.postData.limit;
num = data.records;
$(this.footer).find('.records').html('(' + num + ' total records)');
this.totalPages = Math.ceil(num/limit);
totalPages = this.totalPages;
if(totalPages <= 10){
range = [0, totalPages];
}
else if(pagingOffset === 0) {
range = [0, 9];
}
else {
start = Math.floor(pagingOffset/limit) + 1;
if(start <= 10) {
start = 0;
} else {
start = Math.floor(start/10) * 10;
}
end = start + 9;
if(totalPages - 1 <= end){
end = this.totalPages - 1;
}
//Var determine page set
range = [start, end];
}
// Create the paging data
pageArr = [];
for(var i = range[0] + 1; i <= range[1] + 1; i++) {
obj = {page: i, pagingOffset: i * this.postData.limit, table: this, active: false, hidden: false};
if(i === this.currentPage) {
obj.active = true;
}
if(i === 1 && this.pages === 1) {
obj.hidden = true;
}
pageArr.push(obj);
}
this.pColl = new Backbone.Collection(pageArr);
this.pCollView = new PageCollectionView({collection: this.pColl, viewClass: PageModelView});
this.pCollView.render();
$(this.footer).find('.pages').append(this.pCollView.el);
this.pagerAdjustment();
},
'error' : function(jqXHR, textStatus, errorThrown) {
// TODO: error handling
console.error(jqXHR);
}
});
};
module.Manager.prototype.pagerAdjustment = function(){
if(this.currentPage === 1) {
$(this.previous).css('visibility', 'hidden');
} else if(this.currentPage > 1) {
$(this.previous).css('visibility', 'visible');
}
if(this.currentPage === this.totalPages) {
$(this.next).css('visibility', 'hidden');
} else if(this.currentPage < this.totalPages) {
$(this.next).css('visibility', 'visible');
}
}
module.extend = function(obj){
var objToExt = obj || obj;
return $.extend(true, objToExt, module);
}
return module;
}( _SPDEV.TableWithDetails || {}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment