backbone-list-controller
Version:
A powerful Backbone.js class that renders a collection of models as a list with infinite scrolling, sorting, filtering, and search field.
290 lines (214 loc) • 7.8 kB
JavaScript
Backbone.Model.prototype.templateData = function(){
var that = this;
var data = this.toJSON();
// for each attribute, look for models and collections to convert
_.each(this.attributes, function(attr, key){
// is this attribute a Model?
if( attr instanceof Backbone.Model && attr.displayVal )
data[key] = attr.displayVal();
// is it a collection?
else if( attr instanceof Backbone.Collection )
data[key] = attr.templateData();
})
if( this._templateData )
_.each(this._templateData, function(val, key){
data[key] = _.isFunction(val) ? val.call(that, val) : (that[val] && _.isFunction(that[val]) ? that[val].call(that) : val);
if( data[key] instanceof Backbone.Model )
data[key] = data[key].templateData()
})
return data;
}
Backbone.Collection.prototype.templateData = function(){return this.map(function(m){return m.templateData()})}
Backbone.Collection.prototype.toSelectID = function(labelKey, valKey){
return this.map(function(m){ return {'label':(labelKey?m.get(labelKey):m.id), 'val':(valKey?m.get(valKey):m.id)} })
}
Backbone.Collection.prototype.saveToCSV = function(downloadName){
downloadName = downloadName ? downloadName : 'CSV data '+(new Date()).getTime();
if( this.length == 0 )
return console.log('No data to export');
var data = [_.keys(this.first().templateData())].concat(this.map(function(m){return _.values(m.templateData())}));
data = _.map(data, function(row){
return _.map(row, function(val){
if( _.isString(val) && val.match(/,|\n|"/) )
val = '"'+val+'"';
return val;
})
})
var csvContent = "data:text/csv;charset=utf-8,";
data.forEach(function(infoArray, index){
dataString = infoArray.join(",");
csvContent += index < data.length ? dataString+ "\n" : dataString;
});
var encodedUri = encodeURI(csvContent);
var link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", downloadName+".csv");
link.click();
}
// default Render Template method
Backbone.View.prototype.renderTemplate = function(data, template){
data = !data && this.model && this.model.templateData ? this.model.templateData() : data;
this.$el.html( _.template(template||this.template, data||{}) );
}
// change underscore template system (http://underscorejs.org/#template)
// to use "mustache" syntax (http://mustache.github.com/)
_.templateSettings = {
interpolate : /\{\{(.+?)\}\}/g
};
// TEMPLATE override to alert us if the template is not found
// var _template = _.template;
// _.template = function(text, attrs){
// if( !text ){
// console.warn('Template not found', arguments);
// return 'Template not found'
// }
//
// return _template.call(_, text)(attrs)
// }
/*
Custom Underscore.js methods
*/
_.mixin({
store: amplify.store,
score: LiquidMetal.score.bind(LiquidMetal),
determineFn: function(fn, ctx){
// look for function name on the context
if( _.isString(fn) ){
if( !ctx ){
console.error('Could not bind onClick “'+fn+'”; please provide a context')
fn = null;
}else if( ctx[fn] && _.isFunction(ctx[fn]) ){
fn = ctx[fn].bind(ctx);
}else{
console.error('Method “'+fn+'” does not exist on context:', ctx)
fn = null;
}
}else if( fn && _.isFunction(fn) ){
fn = fn.bind(ctx)
}else{
fn = null;
}
return fn;
},
// get objects, such as a Backbone view, by name: 'App.Views.SomeView'
getObjectByName: function(str){
if( !str ) return;
var path = str.split('.');
var obj = window;
_.each(path, function(key){
if( obj )
obj = obj[key];
})
return obj;
},
getNodeIndex: function (node) {
var index = 0;
while ( (node = node.previousSibling) ) {
index++;
}
return index;
},
metaKey: function(e){
e = e || event;
return e && (e.ctrlKey || e.altKey || e.metaKey);
},
copyKey: function(e){
return e && e.which == 67 && _.metaKey(e);
},
// http://stackoverflow.com/a/1173319/484780
highlightText: function(el){
if (document.selection) {
var range = document.body.createTextRange();
range.moveToElementText(el);
range.select();
} else if (window.getSelection) {
var range = document.createRange();
range.selectNode(el);
window.getSelection().addRange(range);
}
},
/*
Plural - create singlular or plural sentence based on number given (all numbers but 1 return plural sentence)
var str = "Do you want to delete this? There {are|is} [num] book{s} attached."
var num = 2 // or 0, 3, 4, ...
"Do you want to delete this? There are 2 books attached."
var num = 1
"Do you want to delete this? There is 1 book attached."
*/
plural: function(str, num){
var indx = num == 1 ? 1 : 0;
num = _.isNumber(num) ? _.numberFormat(parseFloat(num)) : num;
str = str.replace(/\[num\]/, num);
str = str.replace(/{(.[^}]*)}/g, function(wholematch,firstmatch){
var values = firstmatch.split('|');
return values[indx] || '';
});
return str;
},
sortString: function(str){
// strip single and double quotes and titles starting with "The " and "A "
return str ? str.replace(/^The |^A |"|“|”|'|‘|’/g, '') : str;
},
splitAndTrim: function(str){
return _.map( str.split(','), function(val){ return val.trim() })
},
/*
Move - takes array and moves item at index and moves to another index; great for use with jQuery.sortable()
*/
move: function (array, fromIndex, toIndex) {
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0] );
return array;
},
/*
Relative Date
original work by: 2011 John Resig (ejohn.org)
returns relative date (just now, 5 minutes ago, yesterday, etc)
or a formatted date if over a week old. optionally pass true for "returnWeeks"
if you want number of weeks returned (up to one month) instead of a formatted date
*/
relativeDate: function(time, returnWeeks){
var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ")),
diff = (((new Date()).getTime() - date.getTime()) / 1000),
day_diff = Math.floor(diff / 86400);
if ( isNaN(day_diff) )
return '';
if( day_diff < 0 )
return _.relativeDateFuture(time, returnWeeks);
if( (day_diff > 7 && returnWeeks !== true) || day_diff > 31)
return _.readableDate(time, true);
return day_diff == 0 && (
diff < 60 && "just now" ||
diff < 120 && "1 minute ago" ||
diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
diff < 7200 && "1 hour ago" ||
diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
day_diff == 1 && "yesterday" ||
day_diff < 7 && day_diff + " days ago" ||
day_diff < 31 && (
Math.ceil( day_diff / 7 ) == 1 && '1 week ago' ||
Math.ceil( day_diff / 7 ) + " weeks ago");
},
// I don't like duplicating code, but this will work for now
relativeDateFuture: function(time, returnWeeks){
var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ")),
diff = ((date.getTime() - (new Date()).getTime()) / 1000),
day_diff = Math.floor(diff / 86400);
if ( isNaN(day_diff) )
return '';
if( day_diff < 0 )
return _.relativeDate(time, returnWeeks);
if( (day_diff > 7 && returnWeeks !== true) || day_diff > 31)
return _.readableDate(time, true);
return day_diff == 0 && (
diff < 60 && "now" ||
diff < 120 && "in 1 minute" ||
diff < 3600 && "in " + Math.floor( diff / 60 ) + " minutes" ||
diff < 7200 && "1 hour ago" ||
diff < 86400 && "in " + Math.floor( diff / 3600 ) + " hours") ||
day_diff == 1 && "tomorrow" ||
day_diff < 7 && "in " + day_diff + " days" ||
day_diff < 31 && (
Math.ceil( day_diff / 7 ) == 1 && 'in 1 week' ||
"in " + Math.ceil( day_diff / 7 ) + " weeks");
}
})