[Search] Rewrite search controller, tidy

Rewrite the search controller, making numerous changes and using prototypical
style.

First, the search controller immediately hides previous results when a new
search is started.  Secondly, the search controller ensures that search results
displayed match the currently entered query, preventing race conditions.  Finally,
the search controller uses a poor filtering option that means it may not display
all results.
This commit is contained in:
Pete Richards 2015-10-16 12:34:47 -07:00
parent 9cc0c0b06f
commit 9ad860babd
2 changed files with 162 additions and 159 deletions

View File

@ -31,11 +31,6 @@
type="text"
ng-model="ngModel.input"
ng-keyup="controller.search()" />
<!--mct-control key="'textfield'"
class="search-input"
ng-model="ngModel.input"
ng-keyup="controller.search()">
</mct-control-->
<!-- Search icon -->
<!-- ui symbols for search are 'd' and 'M' -->
@ -78,9 +73,6 @@
Filtered by: {{ ngModel.filtersString }}
<!--div class="filter-options">
Filtered by: {{ ngModel.filtersString }}
</div-->
</div>
<!-- This div exists to determine scroll bar location -->

View File

@ -30,142 +30,153 @@ define(function () {
var INITIAL_LOAD_NUMBER = 20,
LOAD_INCREMENT = 20;
function SearchController($scope, searchService) {
// numResults is the amount of results to display. Will get increased.
// fullResults holds the most recent complete searchService response object
var numResults = INITIAL_LOAD_NUMBER,
fullResults = {hits: []};
// Scope variables are:
// Variables used only in SearchController:
// results, an array of searchResult objects
// loading, whether search() is loading
// ngModel.input, the text of the search query
// ngModel.search, a boolean of whether to display search or the tree
// Variables used also in SearchMenuController:
// ngModel.filter, the function filter defined below
// ngModel.types, an array of type objects
// ngModel.checked, a dictionary of which type filter options are checked
// ngModel.checkAll, a boolean of whether to search all types
// ngModel.filtersString, a string list of what filters on the results are active
$scope.results = [];
$scope.loading = false;
// Filters searchResult objects by type. Allows types that are
// checked. (ngModel.checked['typekey'] === true)
// If hits is not provided, will use fullResults.hits
function filter(hits) {
var newResults = [],
i = 0;
if (!hits) {
hits = fullResults.hits;
}
// If checkAll is checked, search everything no matter what the other
// checkboxes' statuses are. Otherwise filter the search by types.
if ($scope.ngModel.checkAll) {
newResults = fullResults.hits.slice(0, numResults);
} else {
while (newResults.length < numResults && i < hits.length) {
// If this is of an acceptable type, add it to the list
if ($scope.ngModel.checked[hits[i].object.getModel().type]) {
newResults.push(fullResults.hits[i]);
}
i += 1;
}
}
$scope.results = newResults;
return newResults;
}
// Make function accessible from SearchMenuController
$scope.ngModel.filter = filter;
// For documentation, see search below
function search(maxResults) {
var inputText = $scope.ngModel.input;
if (inputText !== '' && inputText !== undefined) {
// We are starting to load.
$scope.loading = true;
// Update whether the file tree should be displayed
// Hide tree only when starting search
$scope.ngModel.search = true;
}
if (!maxResults) {
// Reset 'load more'
numResults = INITIAL_LOAD_NUMBER;
}
// Send the query
searchService.query(inputText, maxResults).then(function (result) {
// Store all the results before splicing off the front, so that
// we can load more to display later.
fullResults = result;
$scope.results = filter(result.hits);
// Update whether the file tree should be displayed
// Reveal tree only when finishing search
if (inputText === '' || inputText === undefined) {
$scope.ngModel.search = false;
}
// Now we are done loading.
$scope.loading = false;
});
}
return {
/**
* Search the filetree. Assumes that any search text will
* be in ngModel.input
* Controller for search in Tree View.
*
* @param maxResults (optional) The maximum number of results
* that this function should return. If not provided, search
* service default will be used.
* Filtering is currently buggy; it filters after receiving results from
* search providers, the downside of this is that it requires search
* providers to provide objects for all possible results, which is
* potentially a hit to persistence, thus can be very very slow.
*
* Ideally, filtering should be handled before loading objects from the persistence
* store, the downside to this is that filters must be applied to object
* models, not object instances.
*
* @constructor
* @param $scope
* @param searchService
*/
search: search,
/**
* Checks to see if there are more search results to display. If the answer is
* unclear, this function will err toward saying that there are more results.
*/
areMore: function () {
var i;
// Check to see if any of the not displayed results are of an allowed type
for (i = numResults; i < fullResults.hits.length; i += 1) {
if ($scope.ngModel.checkAll || $scope.ngModel.checked[fullResults.hits[i].object.getModel().type]) {
return true;
}
}
// If none of the ones at hand are correct, there still may be more if we
// re-search with a larger maxResults
return fullResults.hits.length < fullResults.total;
},
/**
* Increases the number of search results to display, and then
* loads them, adding to the displayed results.
*/
loadMore: function () {
numResults += LOAD_INCREMENT;
if (numResults > fullResults.hits.length && fullResults.hits.length < fullResults.total) {
// Resend the query if we are out of items to display, but there are more to get
search(numResults);
} else {
// Otherwise just take from what we already have
$scope.results = filter(fullResults.hits);
}
}
function SearchController($scope, searchService) {
var controller = this;
this.$scope = $scope;
this.searchService = searchService;
this.numberToDisplay = INITIAL_LOAD_NUMBER;
this.fullResults = [];
this.filteredResults = [];
this.$scope.results = [];
this.$scope.loading = false;
this.pendingQuery = undefined;
this.$scope.ngModel.filter = function () {
return controller.onFilterChange.apply(controller, arguments);
};
}
/**
* Returns true if there are more results than currently displayed for the
* for the current query and filters.
*/
SearchController.prototype.areMore = function () {
return this.$scope.results.length < this.filteredResults.length;
};
/**
* Display more results for the currently displayed query and filters.
*/
SearchController.prototype.loadMore = function () {
this.numberToDisplay += LOAD_INCREMENT;
this.updateResults();
};
/**
* Search for the query string specified in scope.
*/
SearchController.prototype.search = function () {
var inputText = this.$scope.ngModel.input,
controller = this;
this.clearResults();
if (inputText) {
this.$scope.loading = true;
this.$scope.ngModel.search = true;
} else {
this.pendingQuery = undefined;
this.$scope.ngModel.search = false;
this.$scope.loading = false;
return;
}
if (this.pendingQuery === inputText) {
return; // don't issue multiple queries for the same term.
}
this.pendingQuery = inputText;
this
.searchService
.query(inputText, 60) // TODO: allow filter in search service.
.then(function (results) {
if (controller.pendingQuery !== inputText) {
return; // another query in progress, so skip this one.
}
controller.onSearchComplete(results);
});
};
/**
* Refilter results and update visible results when filters have changed.
*/
SearchController.prototype.onFilterChange = function () {
this.filter();
this.updateVisibleResults();
};
/**
* Filter `fullResults` based on currenly active filters, storing the result
* in `filteredResults`.
*
* @private
*/
SearchController.prototype.filter = function () {
var includeTypes = this.$scope.ngModel.checked;
if (this.$scope.ngModel.checkAll) {
this.filteredResults = this.fullResults;
return;
}
this.filteredResults = this.fullResults.filter(function (hit) {
return includeTypes[hit.object.getModel().type];
});
};
/**
* Clear the search results.
*
* @private
*/
SearchController.prototype.clearResults = function () {
this.$scope.results = [];
this.fullResults = [];
this.filteredResults = [];
this.numberToDisplay = INITIAL_LOAD_NUMBER;
};
/**
* Update search results from given `results`.
*
* @private
*/
SearchController.prototype.onSearchComplete = function (results) {
this.fullResults = results.hits;
this.filter();
this.updateVisibleResults();
this.$scope.loading = false;
this.pendingQuery = undefined;
};
/**
* Update visible results from filtered results.
*
* @private
*/
SearchController.prototype.updateVisibleResults = function () {
this.$scope.results =
this.filteredResults.slice(0, this.numberToDisplay);
};
return SearchController;
});