Last active
August 29, 2015 13:56
-
-
Save rgwozdz/9198441 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_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