UNPKG

backbone.facetr

Version:

A library to perform faceted search on Backbone collections

582 lines (479 loc) 18.2 kB
// FacetCollection class var FacetCollection = function(collection) { // give FacetCollection instances ability to handle Backbone Events _.extend(this, Backbone.Events); // init local variables var _self = this, _facets = {}, // facets list _cidModelMap = {}, // an hash containing cid/model pairs used for fast model lookup _activeModels = {}, // cids of the active models for each facet (a facetName -> [cid] map) _vent = _.extend({}, Backbone.Events), // a local event handler used for local events _filters = {}, // an hashmap of custom filters _sortDir = 'asc', // default sort direction _sortAttr, _facetsOrder, _ownReset = false, // inits the models map _initModelsMap = function() { // clear content of _cidModelMap if any for(var cid in _cidModelMap) { if(_cidModelMap.hasOwnProperty(cid)) { delete _cidModelMap[cid]; } } // generate a clone for each model and add it to the map with the cid as key collection.each(function(model) { var clone = model.clone(); clone.cid = model.cid; _cidModelMap[model.cid] = clone; }); }, // deletes the entry for the facet with the given name from the facets hash _removeFacet = function(facetName, silent) { delete _facets[facetName]; delete _activeModels[facetName]; _resetCollection(); if(silent !== true) { _self.trigger('removeFacet', facetName); } }, // fetch (or create if not existing) a facetData object from the facets hash _begetFacet = function(facetName, operator) { var facetData = _facets[facetName]; if(facetData && facetData.operator === operator) { return facetData; } else { var facet = new Facet(facetName, _cidModelMap, _vent, operator); // create an entry for the new facet in the facet to active models map _activeModels[facetName] = []; // add operator property to the object along the actual facet reference return { facet: facet, operator : operator }; } }, _filter = function(facetName, cids) { // set active cids for current facet _activeModels[facetName] = cids; // refresh collection according to new facet filters _resetCollection(); }, _filterBy = function(facetName, facetValue, cids, silent) { _filter(facetName, cids); if(silent !== true){ // expose filter event _self.trigger('filter', facetName, facetValue); } }, _unfilterBy = function(facetName, facetValue, cids, silent) { _filter(facetName, cids); if(silent !== true){ // expose unfilter event _self.trigger('unfilter', facetName, facetValue); } }, _resetCollection = function() { var modelsCids = [], models = [], cid, key, filterName, filterFn; // if no values are selected, return all models for(cid in _cidModelMap) { if(_cidModelMap.hasOwnProperty(cid)) { modelsCids.push(cid); } } // otherwise merge the active models of each facet for(key in _activeModels) { if (_activeModels.hasOwnProperty(key)) { if(_facets[key].facet.toJSON().data.selected) { if(_facets[key].operator === 'or') { modelsCids = _.union(modelsCids, _activeModels[key]); } else { modelsCids = _.intersection(modelsCids, _activeModels[key]); } } } } filterFn = function(cid) { return filter(_cidModelMap[cid]); }; // filter using the added filter functions for(filterName in _filters) { if(_filters.hasOwnProperty(filterName)) { var filter = _filters[filterName]; if(filter instanceof Function) { modelsCids = _.filter(modelsCids, filterFn); } } } // sort the models by cid modelsCids.sort(); // create active models array retrieving clones from the cid to model hash for(var i = 0, len = modelsCids.length; i < len; i += 1) { models.push(_cidModelMap[modelsCids[i]]); } // notify facets to recompute active facet values count _vent.trigger('resetCollection', modelsCids); _ownReset = true; // reset the collecton with the active models collection.reset(models); _ownReset = false; }, // triggered whenever the Backbone collection is reset _resetOrigCollection = function() { if(_ownReset) { return; } _initModelsMap(); // notify facets to recompute _vent.trigger('resetOrigCollection', _cidModelMap); }, // triggered whenever a new Model is added to the Backbone collection _addModel = function(model) { // create a clone of the model and add it to the cid to model map var clone = model.clone(); clone.cid = model.cid; _cidModelMap[model.cid] = clone; // notify facets about the added model _vent.trigger('addModel', model); }, // // triggered whenever a Model instance is removed from the Backbone collection _removeModel = function(model) { // delete model clone from the cid to model map delete _cidModelMap[model.cid]; // notify facets about the removed model _vent.trigger('removeModel', model); var anyActive = _.any(_facets, function(facetData) { return facetData.facet.toJSON().data.selected; }); if(!anyActive) { _resetCollection(); } }, // triggered whenever a model is changed _modifyModel = function(model) { // delete old clone from models cache delete _cidModelMap[model.cid]; // store new model clone with the changes in models cache var clone = model.clone(); clone.cid = model.cid; _cidModelMap[model.cid] = clone; // notify facets about the changed model _vent.trigger('changeModel', model); var anyActive = _.any(_facets, function(facetData) { return facetData.facet.toJSON().data.selected; }); if(!anyActive) { _resetCollection(); } }, _sort = function(silent) { collection.comparator = function(m1,m2) { var v1 = _getValue(m1, _sortAttr), v2 = _getValue(m2, _sortAttr), val1, val2; // check if value is a number if(isNaN(v1) || isNaN(v2)) { val1 = Date.parse(v1); // check if value is a date val2 = Date.parse(v2); if(isNaN(val1) || isNaN(val2)){ val1 = v1; // otherwise is a string val2 = v2; } } else { val1 = parseFloat(v1, 10); val2 = parseFloat(v2, 10); } if(_sortDir === "asc") { if(val1 && val2) { return (val1 > val2) - (val1 < val2); } else { if(val1) { return 1; } if(val2) { return -1; } } } else { if(val1 && val2) { return (val1 < val2) - (val1 > val2); } else { if(val1) { return -1; } if(val2) { return 1; } } } }; collection.sort(); if(silent !== true) { _self.trigger('sort', _sortAttr, _sortDir); } }; // creates a Facet or fetches it from the facets map if it was already created before // use the given operator if any is given and it is a valid value, use default ('and') otherwise this.facet = function(facetName, operator, silent) { var op = (operator && (operator === 'and' || operator === 'or')) ? operator : 'and'; _facets[facetName] = _begetFacet(facetName, op); _resetCollection(); if(silent !== true){ this.trigger('facet', facetName); } return _facets[facetName].facet; }; // returns a JSON array containing facet JSON objects for each facet added to the collection this.toJSON = function() { var key, facetData, facetJSON, facetPos, facets = [], sortedFacets = []; for (key in _facets) { if (_facets.hasOwnProperty(key)) { facetData = _facets[key]; facetJSON = facetData.facet.toJSON(); // add information about the type of facet ('or' or 'and' Facet) facetJSON.data.operator = facetData.operator; if(_facetsOrder && _facetsOrder instanceof Array) { facetPos = _.indexOf(_facetsOrder, facetJSON.data.name); if(facetPos !== -1) { sortedFacets[facetPos] = facetJSON; } else { facets.push(facetJSON); } } else { facets.push(facetJSON); } } } return sortedFacets.concat(facets); }; // removes all the facets assigned to this collection this.clear = function(silent) { var key; for (key in _facets) { if (_facets.hasOwnProperty(key)) { _facets[key].facet.remove(); delete _facets[key]; } } // resets original values in the collection var models = []; for (key in _cidModelMap) { if (_cidModelMap.hasOwnProperty(key)) { models.push(_cidModelMap[key]); } } collection.reset(models); // reset active models _activeModels = {}; if(silent !== true) { this.trigger('clear'); } return this; }; // deselect all the values from all the facets this.clearValues = function(silent) { var key; for (key in _facets) { if (_facets.hasOwnProperty(key)) { _facets[key].facet.clear(); } } // resets original values in the collection var models = []; for (key in _cidModelMap) { if (_cidModelMap.hasOwnProperty(key)) { models.push(_cidModelMap[key]); } } collection.reset(models); // reset active models _activeModels = {}; if(silent !== true) { this.trigger('clearValues'); } return this; }; // removes the collection from the _collections cache and // removes the facetrid property from the collection this.remove = function() { var facetrid = collection.facetrid; this.clear(true); // detach event listeners from collection instance collection.off('reset', _resetOrigCollection); collection.off('add', _addModel); collection.off('remove', _removeModel); collection.off('change', _modifyModel); delete collection.facetrid; delete _collections[facetrid]; }; // reorders facets in the JSON output according to the array of facet names given this.facetsOrder = function(facetNames, silent) { _facetsOrder = facetNames; if(silent !== true){ this.trigger('facetsOrderChange', facetNames); } return this; }; // sorts the collection by the given attribute this.sortBy = function(attrName, silent) { _sortAttr = attrName; _sort(silent); return this; }; // sorts the collection by ascendent sort direction this.asc = function(silent) { _sortDir = 'asc'; _sort(silent); return this; }; // sorts the collection by descendent sort direction this.desc = function(silent) { _sortDir = 'desc'; _sort(silent); return this; }; // adds a filter this.addFilter = function(filterName, filter, silent) { if(filter && filterName) { _filters[filterName] = filter; _resetCollection(silent); } return this; }; // removes a filter this.removeFilter = function(filterName, silent) { if(filterName) { delete _filters[filterName]; _resetCollection(silent); } return this; }; // removes all the filters this.clearFilters = function(silent) { for(var filterName in _filters) { if(_filters.hasOwnProperty(filterName)) { delete _filters[filterName]; } } _resetCollection(silent); return this; }; // returns a reference to the Backbone.Collection instance this.collection = function(){ return collection; }; // returns the original collection length this.origLength = function() { return _.size(_cidModelMap); }; // returns the facet list, which can be used for iteration this.facets = function(){ return _.pluck(_facets, 'facet'); }; this.initFromSettingsJSON = function(json) { var facetCollection, facetr, facets, sort, facetData, attr, lab, eop, iop, fsort, cust, values, facet, i, j, k, len, len2; facetr = Backbone.Facetr; facetCollection = facetr(collection); facets = json.facets; sort = json.sort; if(facets != null) { for(i = 0, len = facets.length; i < len; i += 1) { facetData = facets[i]; attr = facetData.attr; lab = facetData.lab; eop = facetData.eop; iop = facetData.iop; fsort = facetData.sort; cust = facetData.cust; values = facetData.vals; facet = facetCollection.facet(attr, eop); if(lab) { facet.label(lab); } switch(fsort.by){ case 'count' : { facet.sortByCount(); } break; case 'activeCount' : { facet.sortByActiveCount(); } break; default:{ facet.sortByValue(); } } facet[fsort.direction](); if(cust){ for(k in cust){ if(cust.hasOwnProperty(k)){ facet.customData(k, cust[k]); } } } for(j = 0, len2 = values.length; j < len2; j += 1) { facet.value(values[j], iop); } } } if(sort != null) { var sattr = sort.by, sdir = sort.dir; if(sattr) { facetr(collection).sortBy(sattr); } if(sdir) { facetr(collection)[sdir](); } } this.trigger('initFromSettingsJSON'); return this; }; this.settingsJSON = function() { var json, facet, facetJSON, values, activeValues; json = {}; if(_sortAttr && _sortDir) { json.sort = { 'by' : _sortAttr, 'dir' : _sortDir }; } if(_.size(_facets) !== 0) { json.facets = []; for(facet in _facets) { if(_facets.hasOwnProperty(facet)) { facetJSON = _facets[facet].facet.toJSON(); values = _.pluck(facetJSON.values, 'active'); activeValues = []; for(var i = 0, len = values.length; i < len; i += 1) { if(values[i]) { activeValues.push(facetJSON.values[i].value); } } json.facets.push({ 'attr' : facetJSON.data.name, 'lab' : facetJSON.data.label, 'eop' : facetJSON.data.extOperator, 'iop' : facetJSON.data.intOperator, 'sort' : facetJSON.data.sort, 'cust' : facetJSON.data.customData, 'vals' : activeValues }); } } } return json; }; // init models map _initModelsMap(); // remove the facet from the facets hash whenever facet.remove() is invoked _vent.on('removeFacet', _removeFacet, this); // filter collection whenever facet.value(value) and facet.removeValue(value) are invoked _vent.on('value', _filterBy, this); _vent.on('removeValue clear', _unfilterBy, this); // bind Backbone Collection event listeners to FacetCollection respective actions collection.on('reset', _resetOrigCollection); collection.on('add', _addModel); collection.on('remove', _removeModel); collection.on('change', _modifyModel); };