Source: searchers/local/searchableData.js

/** 
 *  
 * @class Makes an array of objects searchable
 * @constructs Septima.Search.SearchableData
 * @param {Object} options SearchableData expects these properties:
 * @param options.data {object} Array of data or a function that returns an array of data.
 * @param options.searchProperties {string[]} Array of property names in the data array to search in. If not added, all properties will be used
 * @param options.displaynameProperty {string} The name of the property in the data array that should be used as displayname
 * @param options.descriptionProperty {string} The name of the property in the data array that should be used as description
 * @param options.useAND {boolean} Use AND and not OR when multiple terms is added by the user. Default true
 */
Septima.Search.SearchableData = Septima.Class ( /**  @lends Septima.Search.SearchableData# */{
	/**
	 * Array of data or a function that returns an array of data. If data is a function it will be 
	 * called each time fetchData is called. This way the client could change the data on the fly 
	 * and not keep them static. This could be used when adding an external filter.
	 */
	data: [],
	
	/**
	 * Array of property names in the data array to search in. If not added, all properties will be used
	 */
	searchProperties: [],
	
	/**
	 * The name of the property in the data array that should be used as displayname.
	 */
	displaynameProperty: null,
	
	/**
	 * The name of the property in the data array that should be used as description.
	 */
	descriptionProperty: null,
	
	/**
	 * Use AND and not OR when multiple terms is added by the user. Set to false to use OR
	 */
	useAND: true,

	/**
	 * Singular phrase, eg.: "feature"
	 */
	singular: null,
	
	/**
	 * Plural phrase, eg.: "features"
	 */
	plural: null,
	
    initialize: function (options) {
    	this.singular = options.singular;
    	this.plural = options.plural;
    	this.useAND = (options.useAND === false ? false : true);
    	this.searchProperties = options.searchProperties || [];
    	this.displaynameProperty = options.displaynameProperty || null;
    	this.descriptionProperty = options.descriptionProperty || null;
    	this.data = options.data || [];
    	if (options.getDisplayname) {
    		this.getDisplayname = options.getDisplayname;
    	}
    	if (options.getDescription) {
    		this.getDescription = options.getDescription;
    	}
    },
    
    getData: function(){
		if (this.data instanceof Function) {
    		return this.data();
    	}else{
    		return this.data;
    	}
    },
    
    query: function(queryString){
    	var objectsToSearch = this.getData();
		var resultset = [];

    	var queryTerms = queryString.split(" ");
    	if (queryTerms.length>0){
    		for (var i=0;i<objectsToSearch.length;i++) {
    			var currentObject = objectsToSearch[i];
    			var hit = {
    				score: 0,
    				object: currentObject,
    				title: this.getDisplayname(currentObject)
    			};
    			if (hit.title !== null){
        			if (queryString === ''){
        				hit.score = 1;
        			}else{
            			var andcount = 0;
            			var andscore = 0;
            			for (var j=0;j<queryTerms.length;j++) {
        					var score = this.match(hit, queryTerms[j]);
        					if (score) {
        						andcount++;
        						andscore += score;
        					}
            			}
            			if (this.useAND) {
            				if (andcount == queryTerms.length) {
        						hit.score = andscore;
            				}
            			} else {
        					hit.score = andscore;
            			}
        			}
        			
        			if (hit.score > 0) {
        				hit.description = this.getDescription(currentObject);
        				resultset.push(hit);
        			}
    			}
    		}
    		if (resultset.length > 0) {
    			//Order the result
	    		resultset.sort(Septima.bind(this.compareHits, this));
    		}
    	}
    	return resultset;
    },
   
    compareHits: function(hit1, hit2){
    	if (hit2.score == hit1.score){
    		return (hit1.title.localeCompare(hit2.title));
    	}else{
        	return hit2.score-hit1.score;
    	}
    },
    
    /**
     * Used for getting the displayname
     * @return {String}
     */
	getDisplayname: function (object) {
		var displayName = null;
		if (this.displaynameProperty) {
			displayName = object[this.displaynameProperty];
		}
		return displayName;
	},
	
    /**
     * Used for getting the description
     * @return {String}
     */
	getDescription: function (object) {
		var description = '';
		if (this.descriptionProperty) {
			description = object[this.descriptionProperty];
		}
		return description;
	},
    
	/**
	 * Method that defines what to search in 
	 * @return {Integer}
	 * */
    match: function (potentialHit, str) {
    	var score = 0;
    	if (str === ""){
    		score = 1;
    	}else{
    		//Factor two for scores in title
    		score = 2 * this.getScore(potentialHit.title, str);
    		var valueToSearch;
    		if (this.searchProperties.length) {
    			for (var k=0;k<this.searchProperties.length;k++) {
    				valueToSearch = potentialHit.object[this.searchProperties[k]];
    				if (valueToSearch && valueToSearch !== null && valueToSearch !== '') {
    					score += this.getScore(valueToSearch, str);
    				}
    			}
    		} else {
    			for (var name in potentialHit.object) {
    				valueToSearch = potentialHit.object[name];
    				if (valueToSearch !== null && valueToSearch !== '') {
    					score += this.getScore(valueToSearch, str);
    				}
    			}
    		}
    	}
		return score;
    },
    
	/**
	 * Get the score for a single string 
	 * @return {Integer}
	 * */
    getScore: function (stringIn, searchstr) {
    	var val = stringIn.toString();
		if (val.toLowerCase().indexOf(searchstr.toLowerCase()) === 0) {
			return 2;
		} else if (val.toLowerCase().indexOf(' ' + searchstr.toLowerCase()) > 0){
			return 1;
		}else{
			return 0;
		}
    },
    CLASS_NAME: 'Septima.Search.SearchableData'

});