Source: controller.js

/**
 * @constructor
 * @param {Septima.Search.Controller.SearcherReg[]} searcherRegs An array of {@link Septima.Search.Controller.SearcherReg}
 * @param {Object} [options]
 * @param [options.onError] {Septima.Search.Controller~errorCallback}
 * @param [options.blankBehavior="none"] {string} "none"|"search" Determines whether the controller issues an empty querystring to its searchers
 * @example 
 * var controller = new Septima.Search.Controller(
 *   searcherRegs, // Previously created array of searcherRegs
 *   {onError : function(searcher, errorThrown) { alert('Error: '+ searcher.getId() + ': '+ errorThrown); },
 *    blankBehavior: "search"
 *   }
 *  );
 * controller.go();
 * controller.addSearcher(searcherReg); // Additional searchers may be added to the controller after go();
 *
 * */
Septima.Search.Controller = function (searcherRegs, options){
	this.idSerial = 0;
    var searchersArg = searcherRegs; // [{"title": "Kortindhold", "searcher" : new FeatureSearcher(featureSearchOptions)},]
    //Vars
    this.searchers = [];
    
    this.activeDataFetcher = null;
    this.onErrorHandlers = [];
    this.onAddSearcherHandlers = [];
    this.onSelectHandlers = [];
	this.blankBehavior = "none"; //none, search
    //Options
    if (options !== null){
    	if (jQuery.isFunction(options.onError)){
    		this.addErrorHandler(options.onError);
    	}
    	if (options.blankBehavior){
    		this.blankBehavior = options.blankBehavior;
    	}
    }
    for (var i=0;i < searchersArg.length;i++){
    	this.addSearcher(searchersArg[i]);
    }
};

//Select related stuff
/**
 * register an onSelect handler.
 * @callback A function which is called when a result is selected
 * @param {Septima.Search.Result}
 */

Septima.Search.Controller.prototype.addOnSelectHandler = function(callback){
	this.onSelectHandlers.push(callback);
};

Septima.Search.Controller.prototype.onSelect = function(result){
	if (result.searcher){
    	result.searcher._onSelect(result);
//	    setTimeout(Septima.bind(function(result){
//	    	result.searcher._onSelect(result);
//		}, this, result), 100);
	}
	for (var i = 0;i<this.onSelectHandlers.length;i++){
		this.onSelectHandlers[i](result);
	}
};

//Error related stuff

/**
 * register an error handler.
 * @callback A function which is called when an error is thrown in a searcher
 * @param {Septima.Search.Searcher} searcher The searcher in which the error was reported
 * @param errorThrown {string}
 */

Septima.Search.Controller.prototype.addErrorHandler = function(callback){
	this.onErrorHandlers.push(callback);
};

/**
 * call error handlers
 */
Septima.Search.Controller.prototype.onError = function(searcher, errorThrown){
	for (var i = 0;i<this.onErrorHandlers.length;i++){
		this.onErrorHandlers[i](searcher, errorThrown);
	}
};

//Add searcher related stuff
	/**
	 * Add a searcher
	 * @param {Septima.Search.Controller.SearcherReg} searcherReg
	 */
	Septima.Search.Controller.prototype.addSearcher = function(searcherReg){
		var searcher = searcherReg.searcher;
		searcher.title = searcherReg.title;
		searcher.id = this.idSerial++;
		this.searchers.push(searcher);
		this.onAddSearcher();
		return searcher.id;
	};
	
	Septima.Search.Controller.prototype.addOnAddSearcherHandler = function(callback){
		this.onAddSearcherHandlers.push(callback);
	};
	
	/**
	 * call addSearcher handlers
	 */
	Septima.Search.Controller.prototype.onAddSearcher = function(){
		for (var i = 0;i<this.onAddSearcherHandlers.length;i++){
			this.onAddSearcherHandlers[i]();
		}
	};

/**
 * Activate View and searchers
 */
Septima.Search.Controller.prototype.go = function(){
	throw ("controller.go is outdated.");
};

/**
 * A searcher registration.
 * @typedef {Object} Septima.Search.Controller.SearcherReg
 * @property {string} title The title of the searcher as presented in the view
 * @property {Septima.Search.Searcher} searcher An implementation of {@link Septima.Search.Searcher}
 */

/**
 * Called by the view
 * @private
 * 
 * @param {string} searcherId 1
 * @param {Query} callQuery 
 * @param {function} completeCallback
 */
Septima.Search.Controller.prototype.fetchData = function (queryString, completeCallback, limit, timeout){
	//search, none
	var query = Septima.Search.Query.Builder().queryString(queryString).limit(limit).timeout(timeout).type("list").targets(this.getAllTargets()).build();
	if ((query.queryString.length > 0) || (query.queryString.length === 0 && this.blankBehavior === "search")){
		this.fetchDataFromQueryObject(query, completeCallback);
	}else{
	    var queryResult = new Septima.Search.QueryResult(null);
		completeCallback(queryResult.getAllResults(), true);
	} 
	
};

Septima.Search.Controller.prototype.isValidTarget = function (potentialTarget){
	var allTargets = this.getAllTargets();
	for (var i=0;i<allTargets.length;i++){
		if (allTargets[i].toLowerCase() == potentialTarget.toLowerCase()){
			return true;
		}
	}
	return false;
};

Septima.Search.Controller.prototype.getAllTargets = function (){
	var allTargets = []; //
    for (var i = 0;i<this.searchers.length;i++){
    	var thisSearcher = this.searchers[i];
    	allTargets.push(thisSearcher.title);
   		allTargets = allTargets.concat(thisSearcher.getTargets());
    }
    return allTargets;
};


Septima.Search.Controller.prototype.fetchDataCallback = function (dataFetcher, queryResult, isLast){
	if (dataFetcher === this.activeDataFetcher){
		if (jQuery.isFunction(dataFetcher.completeCallback)){
			dataFetcher.completeCallback(queryResult, isLast);
		}
		if (isLast){
			this.activeDataFetcher = null;
		}
	}
};

Septima.Search.Controller.prototype.getSearcherSuggestions = function(queryString){
	var queryResult =  new Septima.Search.QueryResult(null); 
	for (var i = 0;i<this.searchers.length;i++){
		var searcher = this.searchers[i]; 
		var title = searcher.title;
		var testTitle = title.toLowerCase();
		var testQueryString = queryString.toLowerCase();
		if (this.searchers.length > 1 && testTitle !== "" && testTitle.indexOf(testQueryString) === 0){
			queryResult.addSuggestion(title, title + ":");
		}
		searcher.getSuggestions(queryResult, queryString);
	}
	return queryResult;
};

Septima.Search.Controller.prototype.cancelFetches = function (){
	//Is there an active DataFetcher? then cancel it
	if (this.activeDataFetcher !== null){
		this.activeDataFetcher.cancel();
		this.activeDataFetcher.completeCallback([], true);
		this.activeDataFetcher = null;
	}
};

/**
 * Initiates a query
 * @private
 * 
 * @param {string} searcherId 
 * @param {Query} callQuery 
 * @param {function} completeCallback
 */
Septima.Search.Controller.prototype.fetchDataFromQueryObject = function (callQuery, completeCallback){
	//Is there an active DataFetcher? then cancel it
	if (this.activeDataFetcher !== null){
		this.activeDataFetcher.cancel();
		this.activeDataFetcher.completeCallback([], true);
		this.activeDataFetcher = null;
	}
	
    var searchersToFetchFrom = [];
	var targetedSearchers = this.getTargetedSearchersFromQuery(callQuery);

	if (targetedSearchers.length > 0){
    	searchersToFetchFrom = searchersToFetchFrom.concat(targetedSearchers);
    }else{
    	searchersToFetchFrom = searchersToFetchFrom.concat(this.searchers);
    }
    
    var fetchQueryBuilder = Septima.Search.Query.Builder().copy(callQuery);
    
    if (searchersToFetchFrom.length == 1){
    	var searcher = searchersToFetchFrom[0];
       	if ((callQuery.hasTarget && searcher.hasTarget(callQuery.target)) || !searcher.hasTargets()){
        	fetchQueryBuilder = fetchQueryBuilder.type("list.force").limit(100);
    	}else{
            fetchQueryBuilder = fetchQueryBuilder.type("list").limit(Math.round(callQuery.limit/searchersToFetchFrom.length));
    	}
    }else{
        fetchQueryBuilder = fetchQueryBuilder.type("list").limit(Math.round(callQuery.limit/searchersToFetchFrom.length));
    }

	var fetchQuery = fetchQueryBuilder.targets(this.getAllTargets()).build();

	this.activeDataFetcher = new Septima.Search.DataFetcher(this, searchersToFetchFrom);
	if (completeCallback !== null && jQuery.isFunction(completeCallback)){
		this.activeDataFetcher.completeCallback = completeCallback;
	}
	this.activeDataFetcher.fetch(fetchQuery);
	
	if (!callQuery.hasTarget && callQuery.queryString.length > 0){
		var queryResult = this.getSearcherSuggestions(callQuery.queryString);
		//To be replaced with completeCallback(queryResult); 
		//this.view.setSuggestions(null, queryResult.getSuggestions());
	}
    
};

Septima.Search.Controller.prototype.select = function(result, view){
	if(typeof result !== 'undefined'){
		var searcher = result.searcher;
	    if (result.hasOwnProperty("newquery") && result.newquery !== null){
	    	if (result.newquery.indexOf(":") == -1){
	    		//The new query doesn't contain a target.
				if (searcher && this.searchers.length > 1){
	                view.doTextSearch(searcher.title + ": " + result.newquery);
				}else{
	                view.doTextSearch(result.newquery);
				}
	    	}else{
	            view.doTextSearch(result.newquery);
	    	}
	    	this.onSelect(result);
	    }else{
	    	//It's not a newquery, but a real result
	    	if (searcher){
	    			result.searcher.completeResult(result).done(Septima.bind(function(result){
	    	    		this.onSelect(result);
	    	        	if (result.searcher.hasdetailHandlerDefs(result)){
	    	            	view.doDetail(result);
	    	        	}
	    			}, this));
	    	}
	    }
	}
};

Septima.Search.Controller.prototype.setSearchText = function(text){
	throw ("controller.setSearchText is outdated. Set the text in the view instead (view.doTextSearch('" + text +"'))");
};

//Private functions
Septima.Search.Controller.prototype.getTargetedSearchersFromQuery = function(query){
	var searchers = [];
	if (query.hasTarget){
    	var target = query.target.toLowerCase();
	    for (var i = 0;i<this.searchers.length;i++){
	    	var thisSearcher = this.searchers[i];
	    	if (thisSearcher.hasTarget(target) || target == thisSearcher.title.toLowerCase()){
	    		searchers.push(thisSearcher);
	    	}
	    }
	}
	return searchers;
};

Septima.Search.Controller.prototype.getSearchers = function(){
    	return this.searchers;
};

Septima.Search.Controller.prototype.getSearcherFromId = function(id){
	    for (var i = 0;i<this.searchers.length;i++){
	    	if (this.searchers[i].getId() == id){
	    		return this.searchers[i];
	    	}
	    }
	return null;
};

Septima.Search.Controller.prototype.fetchError = function(dataFetcher, searcher, errorThrown){
	if (dataFetcher === this.activeDataFetcher){
		this.onError(searcher, errorThrown);
	}
};