UNPKG

backbone.facetr

Version:

A library to perform faceted search on Backbone collections

1,282 lines (1,072 loc) 51.3 kB
// backbone.facetr 0.4.3 // Copyright (c)2012 Arillo GmbH // Author: Francesco Macri // Distributed under MIT license // https://github.com/arillo/Backbone.Facetr (function (root, factory) { if (typeof exports === 'object') { // node.js var underscore = require('underscore'); var backbone = require('backbone'); module.exports = factory(underscore, backbone); } else if (typeof define === 'function' && define.amd) { // AMD define(['underscore', 'backbone'], factory); } else { root.Facetr = factory(_, Backbone); } }(this, function (_, Backbone, undefined) { "use strict"; // create Facetr function as Backbone property // when adding a collection, an id can be associated with it // future call to Facetr can use either the Backbone.Collection instance // as paramter or the given id to retrieve the FacetCollection Backbone.Facetr = function(collection, id) { if(collection instanceof Backbone.Collection) { return _begetCollection(collection, id); } return _getCollection(collection); }; Backbone.Facetr.VERSION = '0.4.3'; // facet collections cache var _collections = {}; // get a collection from the _collections hash based on the id // passed as argument at the first Facetr invokation var _getCollection = function(id) { var coll = _collections[id]; if(coll) { return coll; } return undefined; }; // checks if the given collection exists in the cache, if not creates a new FacetCollection with the given collection data // adds a facetrid attribute to the original collection for quick lookup var _begetCollection = function(collection, id) { var colid = collection.facetrid || id || 'fctr'+new Date().getTime()+Math.floor((Math.random()*99)+1), coll = _getCollection(colid); if(coll) { return coll; } else { collection.facetrid = colid; _collections[colid] = new FacetCollection(collection); return _collections[colid]; } }; var _getValue = function(model, attr) { var value, tokens = attr.split('.'), len = tokens.length, i = 0; // iterate over possible properties of properties in order to allow Property.Property notation // if tokens length is 1, just return the Backbone.Model property value if any is found value = model.get(tokens[i]); for(i = 1; i < len; i += 1){ if(value !== undefined){ if(value instanceof Array){ return value; } else if(value instanceof Backbone.Model){ value = value.get(tokens[i]); } else if(Object.prototype.toString.call(value) === '[object Object]'){ value = value[tokens[i]]; } else { value = value; } } else { return value; } } return value; }; var Facet = function(facetName, modelsMap, vent, extOperator) { // give facet instances ability to handle Backbone Events _.extend(this, Backbone.Events); // init local variables with default values var _self = this, _name = facetName, // corresponds to the model attribute name or 'Property.Property1.PropertyN' for deeper relations _label = facetName, // a human readable label _sortBy = 'value', // determines if facet values are sorted by 'value' or 'count' _sortDirection = 'asc', // determines facet values sort direction _values = [], // facet values computed from collection models (Ex.: { value: 'Title', count: 2 }) _activeValues = [], // currently selected facet values _activeModels = [], // active models according to currently selected values _valModelMap = {}, // a map of values to model, used for fast filtering. it exploits the unique 'cid' given to each Backbone.Model _extOperator = extOperator, // external facet operator, used by FacetrCollection when filtering the Backbone.Collection _operator = 'or', // default internal operator _selected = false, // a flag which is set to true if any of the facet values is selected, _customData = {}, // a map of custom data which can be added to the facet _isHierarchical = false, _groups = [], _groupedValues = [], _hierarchyNodesDescendants = {}, _hierarchyNodesAncestors = {}, // creates a facetValue with count 1 or increases the count by 1 of an existing faceValue in values _begetFacetValue = function(facetValues, value, cid) { var val = (value !== undefined && value !== null) ? value : 'undefined', isObj = false; // TODO - find better solution, this is just a quick fix // Problem: case of property consisting of Array of Objects was overlooked // in original implementation of Dot Notation if(typeof val != null && typeof val === 'object') { var attr = _name.split('.')[1]; val = attr && (val instanceof Backbone.Model && val.get(attr) || val[attr]) || 'undefined'; isObj = true; } var facetValue = _.find(facetValues, function(v) { return v.value === val; }); if(facetValue) { facetValue.count = facetValue.count + 1; facetValue.activeCount = facetValue.activeCount + 1; } else { // use sort index to get index where value should be put in order to keep values sorted var sortedIndex = _.sortedIndex(_.pluck(_values, 'value'), val); // note that at init values are sorted by the default sort property and direction, i.e. 'value' and 'asc' _values.splice(sortedIndex, 0, { value : val, count : 1, activeCount : 1, active : false }); // create an entry in the value to model map for the given value _valModelMap[val] = []; } if(isObj) { _valModelMap[val].push(cid); } return val; }, _parseModel = function(model) { var value = _getValue(model, _name), val; if(value instanceof Array) { if(value.length === 0){ val = _begetFacetValue(_values, 'undefined', model.cid); _valModelMap[val].push(model.cid); } else { _.each(value, function(v) { val = _begetFacetValue(_values, v, model.cid); // push the model.cid in the current value entry of the value to model map if(_valModelMap[val]) { _valModelMap[val].push(model.cid); } }); } } else { if(value != null && typeof value === 'object') { _self.remove(); throw new Error('Model property can only be a value (string,number,boolean) or Array of values, not an object'); } val = _begetFacetValue(_values, value); _valModelMap[val].push(model.cid); } }, // reads model.get(facetName) from the models in the collection and populates the array of values _computeValues = function(modelsMap) { _(modelsMap).each(function(model) { _parseModel(model); }); }, _sortFn = function(v1,v2) { if(_sortBy === 'value') { // note that facet values are always unique, so v1.value === v2.value is never true return (_sortDirection === 'asc') ? ((v1.value > v2.value)-(v1.value < v2.value)) : ((v1.value < v2.value)-(v1.value > v2.value)); } else if(_sortBy === 'activeCount') { if(_sortDirection === 'asc') { if(v1.activeCount < v2.activeCount) { return -1; } else if(v1.activeCount > v2.activeCount) { return 1; } else { // if both facet values have same activeCount, sort by value return ((v1.value > v2.value)-(v1.value < v2.value)); } } else { if(v1.activeCount < v2.activeCount) { return 1; } else if(v1.activeCount > v2.activeCount) { return -1; } else { return ((v1.value < v2.value)-(v1.value > v2.value)); } } } else { if(_sortDirection === 'asc') { if(v1.count < v2.count) { return -1; } else if(v1.count > v2.count) { return 1; } else { // if both facet values have same count, sort by value return ((v1.value > v2.value)-(v1.value < v2.value)); } } else { if(v1.count < v2.count) { return 1; } else if(v1.count > v2.count) { return -1; } else { return ((v1.value < v2.value)-(v1.value > v2.value)); } } } }, // sort method sorts the facet values according to the given sortBy and sortDirection _sort = function() { _values.sort(_sortFn); if(_isHierarchical){ _groupedValues.sort(_sortFn); } }, // checks if any of the facet values is currently selected by testing if _activeValues is empty _isSelected = function() { return _activeValues.length !== 0; }, // compute active facet values count _computeActiveValuesCount = function(filteredModels) { for(var i = 0, len = _values.length; i < len; i += 1) { // if(filteredModels.length !== 0) { // compute intersection of value models and filtered models and store the length var intersectLen = _.intersection(_valModelMap[_values[i].value], filteredModels).length; // if any filtered model is in the value models if(intersectLen !== 0) { // value active count is the length of the intersection _values[i].activeCount = intersectLen; } else { // if no values are selected for the facet and the value is not in any of the filtered models // then the value active count is equal 0 _values[i].activeCount = 0; } } }, // invoked whenever a Backbone collection is reset _resetFacet = function(modelsMap) { // empty current select values and copy them in a new array var activeValCopy = _activeValues.splice(0, _activeValues.length); // empty _values and_activeModels arrays _values.splice(0, _values.length); _activeModels.splice(0, _activeModels.length); // reset _valModelMap _valModelMap = {}; // recompute values _computeValues(modelsMap); // readd each value which was selected before the reset to the facet for(var i = 0, len = activeValCopy.length; i < len; i += 1) { _self.value(activeValCopy[i]); } }, // invoked whenever a new Model instance is added to the Backbone collection _addModel = function(model) { // parse the new model properties to update _values and other local data structures _parseModel(model); // reapply active values to account the new model for(var i = 0, len = _activeValues.length; i < len; i += 1) { _self.value(_activeValues[i]); } }, // invoked whenever a model is removed to the Backbone collection _removeModel = function(model) { var cid = model.cid, index, value, decrementValue = function(value) { index = _values.length-1; while(index !== -1) { var v = _values[index]; if(v.value === value) { // update count and activeCount v.count -= 1; v.activeCount -= 1; // remove value if count is 0 meaning no models have this value anymore if(v.count === 0) { _values.splice(index,1); // remove the value from active values var j = _.indexOf(_activeValues,v.value); if(j !== -1) { _activeValues.splice(j,1); // check if facet is still selected after removing the value _selected = _isSelected(); } } // stop the loop as the value was already found break; } index -= 1; } }; // remove model cid from val to model map for(var val in _valModelMap) { if(_valModelMap.hasOwnProperty(val)) { index = _.indexOf(_valModelMap[val], cid); if(index !== -1) { _valModelMap[val].splice(index, 1); } } } value = _getValue(model, _name); // decrement count and active count for the value if(value) { if(value instanceof Array) { for(var i = 0, len = value.length; i < len; i++) { decrementValue(value[i]); } } else { decrementValue(value); } } // remove model from active models index = _.indexOf(_activeModels, cid); if(index !== -1) { _activeModels.splice(index, 1); } }, // invoked whenever a model is changed _changeModel = function(model) { var prev, prevVal; // check if the value changed was a value of this facet, if not nothing needs to be done if(_getValue(new Backbone.Model(model.changedAttributes()), _name)) { // create a clone of model previous state prev = new Backbone.Model(model.previousAttributes()); prevVal = _getValue(prev, _name); prev.cid = model.cid; // remove the old state of the model _removeModel(prev); // add the new state of the model _addModel(model); } }, // returns an array with all the values of descendant nodes of the hierarchy node argument _computeHierarchyNodeDescendants = function(hierarchyNode){ var values, getDescendants; if(_hierarchyNodesDescendants[hierarchyNode.value] == null){ _hierarchyNodesDescendants[hierarchyNode.value] = []; } values = []; getDescendants = function(node){ if(node.value !== hierarchyNode.value){ values.push(node.value); } if(node.groups){ _.each(node.groups, getDescendants); } }; getDescendants(hierarchyNode); _hierarchyNodesDescendants[hierarchyNode.value] = _.union(_hierarchyNodesDescendants[hierarchyNode.value], values); }, _computeHierarchyNodeAncestors = function(hierarchyNode){ var populate = function(values, parent, node){ if(_hierarchyNodesAncestors[node.value] == null){ _hierarchyNodesAncestors[node.value] = []; } if(parent){ values.push(parent.value); _hierarchyNodesAncestors[node.value] = _.union(_hierarchyNodesAncestors[node.value], values); } if(node.groups){ _.each(node.groups, function(n){ return populate(values, node, n); }); } }; populate([], undefined, hierarchyNode); }, // computes the values for each node in the groups property of the hierarchy node argument _computeHierarchyGroup = function(hierarchyNode){ var i, len, root, groups, val; groups = hierarchyNode.groups; root = { value: hierarchyNode.value, label: hierarchyNode.label, active: false, activeCount: 0, count: 0 }; val = _.find(_values, function(v){ return v.value === root.value; }); if(val){ _.extend(root, val); } // check if hierarchy node as a groups property if(groups && Object.prototype.toString.call(groups) === "[object Array]" && (len = groups.length) > 0){ root.groups = []; for(i = 0; i < len; i += 1){ root.groups.push(_computeHierarchyGroup(groups[i])); } root.activeCount = _.reduce(root.groups, function(memo, num){ return memo + num.activeCount; }, root.activeCount); root.count = _.reduce(root.groups, function(memo, num){ return memo + num.count; }, root.count); } _computeHierarchyNodeDescendants(root); return root; }, // compute all hierarchy groups _computeHierarchyGroups = function() { var i, len; _groupedValues.length = 0; for(i = 0, len = _groups.length; i < len; i += 1){ _groupedValues.push(_computeHierarchyGroup(_groups[i])); _computeHierarchyNodeAncestors(_groups[i]); } _groupedValues.sort(_sortFn); }, // a FacetExp object is returned by the Facet.value method to enable logical filters chaining FacetExp = function() { // and method this.and = function(facetValue, silent) { _operator = 'and'; _self.value(facetValue, silent); return this; }; // or method this.or = function(facetValue, silent) { _operator = 'or'; _self.value(facetValue, silent); return this; }; }; // setters return always this Facet instance for method chaining this.label = function(label) { _label = label; return this; }; // sets the label to the given string this.asc = function() { _sortDirection = 'asc'; _sort(); return this; }; // sets sort direction to asc this.desc = function() { _sortDirection = 'desc'; _sort(); return this; }; // sets sort direction to desc this.sortByValue = function() { _sortBy = 'value'; _sort(); return this; }; // sets sortBy facet name this.sortByCount = function() { _sortBy = 'count'; _sort(); return this; }; // sets sortBy facet value count this.sortByActiveCount = function() { _sortBy = 'activeCount'; _sort(); return this; }; // returns a JSON object containing this Facet instance info and values this.toJSON = function() { var obj; obj = { data : { name : _name, hierarchical: _isHierarchical, label : _label, extOperator : _extOperator, intOperator : _operator, sort : { by : _sortBy, direction : _sortDirection }, selected : _selected, customData : _customData }, values : _values }; if(_isHierarchical){ obj.groupedValues = _groupedValues; } return obj; }; // removes this facet from the FacetCollection, by delegating removal operations to the FacetCollection instance this.remove = function(silent) { // detach event listeners from collection vent object vent.off('resetCollection', _computeActiveValuesCount); vent.off('resetOrigCollection', _resetFacet); vent.off('addModel', _addModel); vent.off('removeModel', _removeModel); vent.off('changeModel', _changeModel); // trigger an event to notify the FacetCollection instance of the change // which will then remove the facet from the facets object vent.trigger('removeFacet', _name, silent); }; // adds the given value // returns a FacetExp object, which can be used to chain facet value selectors with // logical operators this.value = function(facetValue, operator, silent) { var i, len, setsFn, valueIndex, value, valHierarchy; if(operator) { _operator = operator; } // get the index of the value in the _values array valueIndex = _.chain(_values).pluck('value').indexOf(facetValue).value(); // check if value exists if(valueIndex !== -1) { value = _values[valueIndex]; // set the facet value to active only if it is not already so if(!value.active) { value.active = true; _activeValues.push(value.value); } // depending on the operator setsFn = (_operator === 'or' || _activeModels.length === 0) ? _.union : _.intersection; // compute active models as the union/intersection of existing active models and facet value models _activeModels = setsFn(_activeModels, _valModelMap[facetValue]); // in case of hierarchical facet if(_isHierarchical && _hierarchyNodesDescendants[value.value] != null && (valHierarchy = _hierarchyNodesDescendants[value.value]).length > 0){ _activeValues.concat(valHierarchy); // filter collection also by all values in descendant groups of the current one for(i = 0, len = valHierarchy.length; i < len; i += 1){ _activeModels = setsFn(_activeModels, _valModelMap[valHierarchy[i]]); } } } else { // no value found, add one with no models associated only if not already present if(_activeValues.indexOf(facetValue) === -1){ _activeValues.push(facetValue); if(_operator === 'and'){ _activeModels = []; } } } // update is local _selected value _selected = _isSelected(); // trigger a value event to notify the FacetCollection about the change vent.trigger('value', _name, facetValue, _activeModels, silent); if(!silent){ this.trigger('value', facetValue); } // return a FacetExp object to allow Facetr expression chain return new FacetExp(this, _operator); }; // removes the given value this.removeValue = function(facetValue, silent) { var valueIndex = _.chain(_values).pluck('value').indexOf(facetValue).value(), value, modelsToAdd, modelsToRemove; // check if value exists if(valueIndex !== -1) { value = _values[valueIndex]; value.active = false; } // remove value from active values array _activeValues.splice(_.indexOf(_activeValues, value && value.value || facetValue), 1); // compute models to remove from the active models due to facet value deselection // need to filter out models which are included in active models due to another // facet value being selected (thing that can happen on model properties which are of type Array) modelsToRemove = _.filter(_valModelMap[facetValue], function(cid) { for(var value in _valModelMap) { if(_valModelMap.hasOwnProperty(value)) { // check if any other active value has a model which is also in the facet value being removed // if yes, the model cannot be removed from the result set if(_.indexOf(_activeValues, value) !== -1 && _.indexOf(_valModelMap[value], cid) !== -1) { return false; } } } // if the model is only in the facet value being removed, then it can be removed return true; }); // remove inactive models from the active models list _activeModels = _.difference(_activeModels, modelsToRemove); if(_operator === 'and') { modelsToAdd = []; for(var i = 0, len = _activeValues.length; i < len; i += 1) { if(modelsToAdd.length === 0) { modelsToAdd = _.union(modelsToAdd, _valModelMap[_activeValues[i]]); } else { modelsToAdd = _.intersection(modelsToAdd, _valModelMap[_activeValues[i]]); } } _activeModels = _.union(_activeModels, modelsToAdd); } // if facet is hierarchical if(_isHierarchical && _hierarchyNodesAncestors[facetValue] != null){ // check if a parent of the value being removed is selected var ancestors = _.intersection(_hierarchyNodesAncestors[facetValue], _activeValues); if(ancestors.length > 0){ // if yes, need to readd value models _activeModels = _.union(_activeModels, _valModelMap[facetValue]); } } // update local _selected value _selected = _isSelected(); // notify the FacetCollection to update this facet values vent.trigger('removeValue', _name, facetValue, _activeModels, silent); if(!silent){ this.trigger('removeValue', facetValue); } return this; }; // removes all selected values this.clear = function(silent) { while(_activeValues.length > 0) { this.removeValue(_activeValues[0], true); } if(!silent){ this.trigger('clear'); } return this; }; // attaches custom data, which can be retrieved by key this.customData = function(key, value) { if(value !== undefined) { _customData[key] = value; return this; } return _customData[key]; }; // returns true if any value is selected this.isSelected = function(){ return _selected; }; // creates hierarchical representation of the values based on groups settins this.hierarchy = function(settings){ if(Object.prototype.toString.call(settings) !== '[object Array]') { throw new Error('Facet.hierarchy: wrong settings object. Check the documentation for the right format'); } _groups = settings; _isHierarchical = true; _computeHierarchyGroups(); vent.on('resetCollection addModel removeModel changeModel', _computeHierarchyGroups); this.trigger('hierarchy', _groups); return this; }; // compute values once the facet is added to the FacetCollection _computeValues(modelsMap); // compute facet values count on collection reset vent.on('resetCollection', _computeActiveValuesCount); vent.on('resetCollection', _sort); // bind actions on Backbone Collection events, shived by the FacetCollection instance vent.on('resetOrigCollection', _resetFacet); vent.on('addModel', _addModel); vent.on('removeModel', _removeModel); vent.on('changeModel', _changeModel); }; // 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({