443 lines
13 KiB
JavaScript
443 lines
13 KiB
JavaScript
/*
|
|
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
/**
|
|
* @fileOverview Magic Search JS
|
|
* @requires AngularJS
|
|
*
|
|
*/
|
|
|
|
angular.module('horizon.framework.widgets.magic-search')
|
|
.controller('MagicSearchController', magicSearchController);
|
|
|
|
magicSearchController.$inject = [
|
|
'$scope', '$element', '$timeout', '$window',
|
|
'horizon.framework.widgets.magic-search.service',
|
|
'horizon.framework.widgets.magic-search.events'
|
|
];
|
|
|
|
function magicSearchController(
|
|
$scope,
|
|
$element,
|
|
$timeout,
|
|
$window,
|
|
service,
|
|
magicSearchEvents
|
|
) {
|
|
var ctrl = this;
|
|
var searchInput = $element.find('.search-input');
|
|
ctrl.mainPromptString = $scope.strings.prompt;
|
|
|
|
// currentSearch is the list of facets representing the current search
|
|
ctrl.currentSearch = [];
|
|
ctrl.isMenuOpen = false;
|
|
|
|
searchInput.on('keydown', keyDownHandler);
|
|
searchInput.on('keyup', keyUpHandler);
|
|
searchInput.on('keypress', keyPressHandler);
|
|
|
|
// enable text entry when mouse clicked anywhere in search box
|
|
$element.find('.search-main-area').on('click', searchMainClickHandler);
|
|
|
|
// when facet clicked, add 1st part of facet and set up options
|
|
ctrl.facetClicked = facetClickHandler;
|
|
|
|
// when option clicked, complete facet and send event
|
|
ctrl.optionClicked = optionClickHandler;
|
|
|
|
// remove facet and either update filter or search
|
|
ctrl.removeFacet = removeFacet;
|
|
|
|
// Controller-exposed Functions
|
|
// clear entire searchbar
|
|
ctrl.clearSearch = clearSearch;
|
|
|
|
// ctrl.textSearch is undefined, only used when a user free-enters text
|
|
|
|
// Used by the template.
|
|
ctrl.isMatchLabel = function(label) {
|
|
return angular.isArray(label);
|
|
};
|
|
|
|
// unusedFacetChoices is the list of facet types that have not been selected
|
|
ctrl.unusedFacetChoices = [];
|
|
|
|
// facetChoices is the list of all facet choices
|
|
ctrl.facetChoices = [];
|
|
|
|
initSearch(service.getSearchTermsFromQueryString($window.location.search));
|
|
emitQuery();
|
|
|
|
$scope.$on(magicSearchEvents.INIT_SEARCH, function(event, data) {
|
|
if ( data ) {
|
|
if ( data.textSearch ) {
|
|
// the requested text search will show up as a 'search in results' facet
|
|
ctrl.textSearch = data.textSearch;
|
|
} else {
|
|
// no requested text search, clear any prior text search
|
|
ctrl.textSearch = undefined;
|
|
searchInput.val('');
|
|
}
|
|
initSearch(data.magicSearchQuery || []);
|
|
}
|
|
});
|
|
|
|
function initSearch(initialSearchTerms) {
|
|
// Initializes both the unused choices and the full list of facets
|
|
ctrl.facetChoices = service.getFacetChoicesFromFacetsParam($scope.facets_param);
|
|
|
|
// resets the facets
|
|
initFacets(initialSearchTerms);
|
|
}
|
|
|
|
function keyDownHandler($event) {
|
|
var key = service.getEventCode($event);
|
|
if (key === 9) { // prevent default when we can.
|
|
$event.preventDefault();
|
|
} else if (key === 8) {
|
|
backspaceKeyDown();
|
|
}
|
|
}
|
|
|
|
function tabKeyUp() {
|
|
if (angular.isUndefined(ctrl.facetSelected)) {
|
|
if (ctrl.filteredObj.length !== 1) {
|
|
return;
|
|
}
|
|
ctrl.facetClicked(0, '', ctrl.filteredObj[0].name);
|
|
} else {
|
|
if (angular.isUndefined(ctrl.filteredOptions) ||
|
|
ctrl.filteredOptions.length !== 1) {
|
|
return;
|
|
}
|
|
ctrl.optionClicked(0, '', ctrl.filteredOptions[0].key);
|
|
resetState();
|
|
}
|
|
}
|
|
|
|
function escapeKeyUp() {
|
|
if (angular.isDefined(ctrl.facetSelected)) {
|
|
setMenuOpen(true);
|
|
} else {
|
|
setMenuOpen(false);
|
|
}
|
|
resetState();
|
|
var textFilter = ctrl.textSearch;
|
|
if (angular.isUndefined(textFilter)) {
|
|
textFilter = '';
|
|
}
|
|
emitTextSearch(textFilter);
|
|
}
|
|
|
|
function enterKeyUp() {
|
|
var searchVal = searchInput.val();
|
|
// if tag search, treat as regular facet
|
|
if (searchVal !== '') {
|
|
if (ctrl.facetSelected) {
|
|
var curr = ctrl.facetSelected;
|
|
curr.name = curr.name.split('=')[0] + '=' + searchVal;
|
|
curr.label[1] = searchVal;
|
|
ctrl.currentSearch.push(curr);
|
|
resetState();
|
|
emitQuery();
|
|
setMenuOpen(true);
|
|
} else {
|
|
// if text search treat as search
|
|
ctrl.currentSearch = ctrl.currentSearch.filter(notTextSearch);
|
|
ctrl.currentSearch.push(service.getTextFacet(searchVal, $scope.strings.text));
|
|
$scope.$apply();
|
|
setMenuOpen(true);
|
|
setSearchInput('');
|
|
emitTextSearch(searchVal);
|
|
ctrl.textSearch = searchVal;
|
|
}
|
|
} else if (ctrl.isMenuOpen) {
|
|
setMenuOpen(false);
|
|
} else {
|
|
setMenuOpen(true);
|
|
}
|
|
ctrl.filteredObj = ctrl.unusedFacetChoices;
|
|
}
|
|
|
|
function backspaceKeyDown() {
|
|
var searchVal = searchInput.val();
|
|
if (searchVal === '') {
|
|
if (ctrl.currentSearch.length > 0 && angular.isUndefined(ctrl.facetSelected)) {
|
|
ctrl.removeFacet(ctrl.currentSearch.length - 1);
|
|
setMenuOpen(true);
|
|
} else {
|
|
escapeKeyUp();
|
|
}
|
|
}
|
|
}
|
|
|
|
function backspaceKeyUp() {
|
|
var searchVal = searchInput.val();
|
|
// if there's no current search and facet selected, then clear all search
|
|
if (searchVal === '' && angular.isUndefined(ctrl.facetSelected)) {
|
|
if (ctrl.currentSearch.length === 0) {
|
|
ctrl.clearSearch();
|
|
} else {
|
|
resetState();
|
|
emitTextSearch(ctrl.textSearch || '');
|
|
}
|
|
} else {
|
|
filterFacets(searchVal);
|
|
}
|
|
}
|
|
|
|
function deleteKeyUp() {
|
|
return backspaceKeyUp();
|
|
}
|
|
|
|
function notTextSearch(item) {
|
|
return item.name.indexOf('text') !== 0;
|
|
}
|
|
|
|
function defaultKeyUp() {
|
|
var searchVal = searchInput.val();
|
|
filterFacets(searchVal);
|
|
}
|
|
|
|
function keyUpHandler($event) { // handle ctrl-char input
|
|
if ($event.metaKey === true) {
|
|
return;
|
|
}
|
|
var key = service.getEventCode($event);
|
|
var handlers = {
|
|
8: backspaceKeyUp,
|
|
9: tabKeyUp,
|
|
27: escapeKeyUp,
|
|
13: enterKeyUp,
|
|
46: deleteKeyUp
|
|
};
|
|
if (handlers[key]) {
|
|
handlers[key]();
|
|
} else {
|
|
defaultKeyUp();
|
|
}
|
|
}
|
|
|
|
function keyPressHandler($event) { // handle character input
|
|
var searchVal = searchInput.val();
|
|
var key = service.getEventCode($event);
|
|
// Backspace, Delete, Enter, Tab, Escape
|
|
if (key !== 8 && key !== 46 && key !== 13 && key !== 9 && key !== 27) {
|
|
// This builds the search term as you go.
|
|
searchVal = searchVal + String.fromCharCode(key).toLowerCase();
|
|
}
|
|
if (searchVal === ' ') { // space and field is empty, show menu
|
|
setMenuOpen(true);
|
|
setSearchInput('');
|
|
return;
|
|
}
|
|
if (searchVal === '') {
|
|
return;
|
|
}
|
|
// Backspace, Delete and arrow keys
|
|
if (key !== 8 && key !== 46 && !(key >= 37 && key <= 40)) {
|
|
filterFacets(searchVal);
|
|
}
|
|
}
|
|
|
|
function filterFacets(searchVal) {
|
|
// try filtering facets/options.. if no facets match, do text search
|
|
var filtered = [];
|
|
var isTextSearch = angular.isUndefined(ctrl.facetSelected);
|
|
if (isTextSearch) {
|
|
ctrl.filteredObj = ctrl.unusedFacetChoices;
|
|
filtered = service.getMatchingFacets(ctrl.filteredObj, searchVal);
|
|
} else { // assume option search
|
|
ctrl.filteredOptions = ctrl.facetOptions;
|
|
if (angular.isUndefined(ctrl.facetOptions)) {
|
|
// no options, assume free form text facet
|
|
return;
|
|
}
|
|
filtered = service.getMatchingOptions(ctrl.filteredOptions, searchVal);
|
|
}
|
|
if (filtered.length > 0) {
|
|
setMenuOpen(true);
|
|
$timeout(function() {
|
|
ctrl.filteredObj = filtered;
|
|
}, 0.1);
|
|
} else if (isTextSearch) {
|
|
emitTextSearch(searchVal);
|
|
setMenuOpen(false);
|
|
}
|
|
}
|
|
|
|
function searchMainClickHandler($event) {
|
|
var target = angular.element($event.target);
|
|
if (target.is('.search-main-area')) {
|
|
searchInput.trigger('focus');
|
|
setMenuOpen(true);
|
|
}
|
|
}
|
|
|
|
function facetClickHandler($index) {
|
|
var facet = ctrl.filteredObj[$index];
|
|
var label = facet.label;
|
|
if (angular.isArray(label)) {
|
|
label = label.join('');
|
|
}
|
|
var facetParts = facet.name && facet.name.split('=');
|
|
ctrl.facetSelected = service.getFacet(facetParts[0], facetParts[1], label, '');
|
|
if (angular.isDefined(facet.options)) {
|
|
ctrl.filteredOptions = ctrl.facetOptions = facet.options;
|
|
setMenuOpen(true);
|
|
} else {
|
|
setMenuOpen(false);
|
|
}
|
|
setSearchInput('');
|
|
setPrompt('');
|
|
$timeout(function() {
|
|
searchInput.focus();
|
|
});
|
|
}
|
|
|
|
function optionClickHandler($index, $event, name) {
|
|
setMenuOpen(false);
|
|
var curr = ctrl.facetSelected;
|
|
curr.name = curr.name.split('=')[0] + '=' + name;
|
|
curr.label[1] = ctrl.filteredOptions[$index].label;
|
|
if (angular.isArray(curr.label[1])) {
|
|
curr.label[1] = curr.label[1].join('');
|
|
}
|
|
ctrl.currentSearch.push(curr);
|
|
resetState();
|
|
emitQuery();
|
|
}
|
|
|
|
function emitTextSearch(val) {
|
|
$scope.$emit(magicSearchEvents.TEXT_SEARCH, val, $scope.filter_keys);
|
|
}
|
|
|
|
function emitQuery(removed) {
|
|
var query = service.getQueryPattern(ctrl.currentSearch);
|
|
if (angular.isDefined(removed) && removed.indexOf('text') === 0) {
|
|
emitTextSearch('');
|
|
delete ctrl.textSearch;
|
|
} else {
|
|
$scope.$emit(magicSearchEvents.SEARCH_UPDATED, query);
|
|
if (ctrl.currentSearch.length > 0) {
|
|
// prune facets as needed from menus
|
|
var newFacet = ctrl.currentSearch[ctrl.currentSearch.length - 1].name;
|
|
var facetParts = service.getSearchTermObject(newFacet);
|
|
service.removeChoice(facetParts, ctrl.facetChoices, ctrl.unusedFacetChoices);
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearSearch() {
|
|
ctrl.currentSearch = [];
|
|
delete ctrl.textSearch;
|
|
ctrl.unusedFacetChoices = ctrl.facetChoices.map(service.getFacetChoice);
|
|
resetState();
|
|
$scope.$emit(magicSearchEvents.SEARCH_UPDATED, '');
|
|
emitTextSearch('');
|
|
}
|
|
|
|
function resetState() {
|
|
setSearchInput('');
|
|
ctrl.filteredObj = ctrl.unusedFacetChoices;
|
|
delete ctrl.facetSelected;
|
|
delete ctrl.facetOptions;
|
|
delete ctrl.filteredOptions;
|
|
if (ctrl.currentSearch.length === 0) {
|
|
setPrompt(ctrl.mainPromptString);
|
|
}
|
|
}
|
|
|
|
function setMenuOpen(bool) {
|
|
$timeout(function setMenuOpenTimeout() {
|
|
ctrl.isMenuOpen = bool;
|
|
});
|
|
}
|
|
|
|
function setSearchInput(val) {
|
|
$timeout(function setSearchInputTimeout() {
|
|
searchInput.val(val);
|
|
});
|
|
}
|
|
|
|
function setPrompt(str) {
|
|
$timeout(function setPromptTimeout() {
|
|
$scope.strings.prompt = str;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add ability to update facet
|
|
* Broadcast event when facet options are returned via AJAX.
|
|
* Should magic_search.js absorb this?
|
|
*/
|
|
var facetsChangedWatcher = $scope.$on(magicSearchEvents.FACETS_CHANGED, function (event, data) {
|
|
$timeout(function () {
|
|
if (data && data.magicSearchQuery) {
|
|
initSearch(data.magicSearchQuery.split('&'));
|
|
} else {
|
|
initSearch(ctrl.currentSearch.map(function(x) { return x.name; }));
|
|
}
|
|
});
|
|
});
|
|
|
|
$scope.$on('$destroy', function () {
|
|
facetsChangedWatcher();
|
|
});
|
|
|
|
function initFacets(searchTerms) {
|
|
var tmpFacetChoices = ctrl.facetChoices.map(service.getFacetChoice);
|
|
if (searchTerms.length > 1 || searchTerms[0] && searchTerms[0].length > 0) {
|
|
setPrompt('');
|
|
}
|
|
ctrl.currentSearch = service.getFacetsFromSearchTerms(searchTerms,
|
|
ctrl.textSearch, $scope.strings.text, tmpFacetChoices);
|
|
ctrl.filteredObj = ctrl.unusedFacetChoices =
|
|
service.getUnusedFacetChoices(tmpFacetChoices, searchTerms);
|
|
|
|
// emit to check facets for server-side
|
|
$scope.$emit(magicSearchEvents.CHECK_FACETS, ctrl.currentSearch);
|
|
}
|
|
|
|
/**
|
|
* Override magic_search.js 'removeFacet' to emit(magicSearchEvents.CHECK_FACETS)
|
|
* to flag facets as 'isServer' after removing facet and
|
|
* either update filter or search
|
|
* @param {number} index - the index of the facet to remove. Required.
|
|
*
|
|
* @returns {number} Doesn't return anything
|
|
*/
|
|
function removeFacet(index) {
|
|
var removed = ctrl.currentSearch[index].name;
|
|
ctrl.currentSearch.splice(index, 1);
|
|
if (angular.isUndefined(ctrl.facetSelected)) {
|
|
emitQuery(removed);
|
|
} else {
|
|
resetState();
|
|
}
|
|
if (ctrl.currentSearch.length === 0) {
|
|
setPrompt(ctrl.mainPromptString);
|
|
}
|
|
// re-init to restore facets cleanly
|
|
initFacets(ctrl.currentSearch.map(service.getName));
|
|
}
|
|
|
|
}
|
|
|
|
})();
|