UNPKG

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.

423 lines (311 loc) 10.8 kB
/* Sortable Collection This collection should be used in conjunction with ListController It allows for ascending and descending, sorts and filters. Sorts and filters are saved to localstorage @author Kevn Jantzer @since 2012-12-20 */ var SortableCollection = Backbone.Collection.extend({ // override the default backbone constructor constructor: function(data, opts){ // make sure setup has happened this.setupFilters(); // call normal backbone constructor Backbone.Collection.prototype.constructor.apply(this, arguments); }, //key : 'LocalStorageKey', // define a key for saving sort info to local storage //defaultSort : 'some_key', // the default key to sort by defaultDesc : false, dbSort : false, // should database do the sorting instead? _key: function(){ return _.isFunction(this.key) ? this.key() : this.key; }, /* SORTS - if custom sorting is needed for a specific key (or all of them), put that logic here By default, sorting uses the key, say "title" and looks for that key on the model. But if you want to call a method on the model instead, you can do that here */ /*sorts: { 'sort-key': function(model, key, isDesc){ return model.get(key); // default behavior return model.someCustomFn(); // this would be a reason for this sort-key } }*/ /* FILTERS filters listed here will be sent to the server via .fetch() (db:true) the selected value is also saved to local storage to persist the users selection (unless localStorage:false) */ /*filters: [{ key: 'key-string', val: 'the val', // if this is set, it will be the "default" unless found in local storage db: true, // defaults to false localStorage: false, // defaults to true optional: false }],*/ /* ======================================================================================================================================= "Locked" methods... dont override the methods below except rare cases */ localStoreKey: function(extra){ return 'list:'+this._key()+(extra?':'+extra:''); }, /* Sort Key - sets or gets the key used to sort */ sortKey: function(newKey){ // SET if(newKey !== undefined) _.store('list:'+this._key()+':sort', newKey); // GET else return _.store('list:'+this._key()+':sort') || this.defaultSort; }, /* Sort Descending - sets or gets sort ascending order */ sortDesc: function(desc){ // SET if(desc !== undefined) return _.store('list:'+this._key()+':sort:desc', desc); // GET else return _.store('list:'+this._key()+':sort:desc') !== undefined ? _.store('list:'+this._key()+':sort:desc') : this.defaultDesc; }, /* Sort By - overriding default sort by behaviour to add in reverse order logic */ sortBy: function(){ if( this.dbSort ) return this.models; var models = _.sortBy(this.models, this.comparator, this); if(this.sortDesc()) models.reverse(); return models; }, //rootComparator: function(model){} /* Comparator - How to sort This method should NOT be overridden in the inherited class; instead, use this.sorts to specify custom logic per sort key */ comparator: function(model){ if( this.dbSort ) return true; var sortKey = this.sortKey(); var sortBy = null; // look for this sortkey on the "sorts" object if( this.sorts && this.sorts[sortKey] ) sortBy = this.sorts[sortKey].call(this, model, sortKey, this.sortDesc()) // else, is the key "title"? then lets do a little special sorting else if( sortKey === 'title' ) sortBy = _.sortString( model.get( this.sortKey() )) else if( model[sortKey] && _.isFunction(model[sortKey]) ) sortBy = (model[sortKey])(); // call method // else, just look for the key on the model else if( model.has(sortKey) ) sortBy = model.get(sortKey); if( this.rootComparator ){ if( !_.isArray(sortBy) ) sortBy = [sortBy]; var root = this.rootComparator(model); if( !_.isArray(root) ) root = [root]; sortBy = root.concat(sortBy); } return sortBy; }, /* Change how this collection is sorted */ changeSort: function(sortKey){ // if same sort key was clicked, lets reverse the sort direction if(this.sortKey() == sortKey) this.sortDesc( !this.sortDesc() ); // set direction to opposite of what it is now // set the new sort key this.sortKey( sortKey ); this.trigger('sort:change'); this.sort(); }, /* Fetch - overriding fetch so we can send data (filters) to the server */ fetch: function(opts){ var data = {}; if( this.filters ){ data = this.filterVals(null, 'db'); // maintain backwards compatibility; TODO: remove this ASAP data.filters = this.filterVals(null, 'db'); // this data should be used by PHP and not the data above data.sortKey = this.sortKey(); data.sortDesc = this.sortDesc(); data.optionalFilters = this.optionalFilters(); } opts = opts || {}; opts.data = opts.data ? _.extend(opts.data, data) : data; if( this.url ) Backbone.Collection.prototype.fetch.call(this, opts) else opts.success(this, []) }, refresh: function(update){ update = update == false ? false : true; // with dbSort, a `DBListControllerModel` will do the fetching, but we need to trigger a `reset` for that to happen if( this.dbSort && update){ this.trigger('reset') return; } this.trigger('spin'); // for Backone 0.9.10/0.9.9, 'update' is needed to register change events on the model, rather than mass "reset" (see line 815 in backbone source) this.fetch({update:update, add:false, remove:false, merge:true, success: function(coll, models){ // if we "updated" the collection, trigger reset to things add/remove correctly // this was made apparent when coding Profit Report if( update ) coll.reset(models); // NOTE: this can cause issues when views have bound to their model this.trigger('fetch:success', coll, models) this.trigger('spin', false); }.bind(this), error: function(coll, xhr){ this.trigger('fetch:failed', this, xhr.statusText, this.refresh.bind(this, update)) this.trigger('spin', false); }.bind(this)}); }, /* Setup Filters - this is called automatically by the contructor */ setupFilters: function(){ if( this._setupFiltersDone ) return; this._setupFiltersDone = true; if( !this.filters ) return; var that = this; _.each(this.filters, function(filter){ if( !filter.key ) return console.error('!! SortableCollection: filters must have a key specified; this one does not:', filter); if( filter.localStorage !== false ) var storedVal = _.store( that.localStoreKey(filter.key) ) if( storedVal ){ if( _.isObject(storedVal) ){ filter.val = storedVal.val; filter.optional = storedVal.optional; }else{ filter.val = storedVal; } }else if( _.isFunction(filter.val) ){ filter.val = filter.val(); } }) }, /* Apply Filter - use this to set the filter values opts= { silent: false, fetch: true } */ applyFilter: function(key, val, opts){ opts = opts || {}; // are any filters specifed, if not, then we can't apply the filter if( !this.filters ) return //console.error('!! SortableCollection: no filters are specifed'); // find the filter object based on the key given var filter = this.getFilter(key); // did we find the filter based on the key? if( !filter ) return console.error('!! SortableCollection: no filter was found for key: “'+key+'”', this.filters); // update the filters value filter.val = val; // store it locally unless specifically told not too if( filter.localStore !== false ) _.store( this.localStoreKey(key), {val: filter.val, optional: filter.optional} ); // trigger event change if( opts.silent !== true){ this.trigger('filter:change:'+key, val); this.trigger('filter:change', key, val); } // if this is a DB filter, then refetch the collection if( filter.db && opts.fetch !== false ){ this.refresh(); } return filter; }, toggleFilterOptional: function(key, opts){ opts = opts || {}; // are any filters specifed, if not, then we can't apply the filter if( !this.filters ) return console.error('!! SortableCollection: no filters are specifed'); // find the filter object based on the key given var filter = this.getFilter(key); // did we find the filter based on the key? if( !filter ) return console.error('!! SortableCollection: no filter was found for key: “'+key+'”', this.filters); // update the filters value filter.optional = !filter.optional; // store it locally unless specifically told not too if( filter.localStore !== false ) _.store( this.localStoreKey(key), {val: filter.val, optional: filter.optional} ); // trigger event change if( opts.silent !== true){ this.trigger('filter:change:'+key, filter.optional); this.trigger('filter:change', key, filter.optional); } // if this is a DB filter, then refetch the collection if( filter.db && opts.fetch !== false ){ this.refresh(); } }, /* Set Filters - given an array of filters, they will be silently applied */ setFilters: function(filters, doRefresh){ var needsRefresh = false; _.each(filters, function(val, key){ filter = this.applyFilter(key, val, {silent:true, fetch:false}); if( !needsRefresh ) needsRefresh = filter && filter.db == true; }, this) doRefresh !== false && needsRefresh && this.refresh(); }, getFiltered: function(){ // this method will be set by ListController return this; }, /* Get Filter - returns a filter object based on the key given */ getFilter: function(key){ if( this.filters ) return _.findWhere(this.filters, {key: key}); else return null; }, /* Gets object of filters and their values, ex: {key:val, key2:val2} use this to figure out which filters are in use when first loading */ filterVals: function(filters, forWhat){ if( filters == undefined ) filters = this.filters; var data = {}; if( filters ){ _.each(filters, function(o){ if( forWhat !== 'db' || o.db !== false) data[o.key] = o.val; }) } return data; }, /* Gets object of filters and their optional values, ex: {key:val, key2:val2} */ filterOptionals: function(filters){ if( filters == undefined ) filters = this.filters; var data = {}; if( filters ){ _.each(filters, function(o){ data[o.key] = o.optional; }) } return data; }, optionalFilters: function(filters){ if( filters == undefined ) filters = this.filters; var data = []; if( filters ){ _.each(filters, function(o){ o.optional && data.push(o.key) }) } return data; }, });