app/assets/javascripts/fae/navigation/_global_search.js
/* global Fae, FCH */
/**
* Fae global search
* @namespace global_search
* @memberof Fae
*/
Fae.navigation.global_search = {
init: function() {
this.$wrapper = $('#js-utility-search-wrapper');
this.$input = $('#js-global-search');
this.utilitySearch();
this.searchListener();
},
/**
* Click event of the admin search in the main nav
*/
utilitySearch: function() {
var _this = this;
var $header = $('#js-main-header');
var timer;
/**
* Hide search menu, unbind listener, and blur the input
* @private
*/
function hideSearch() {
_this.$wrapper.data('hover', 0);
_this.$wrapper.hide();
_this.$input.blur();
// Avoid duplicate/rapid bindings
$header.off('mouseleave', hideSearch);
}
// If user leaves search, remove menu
_this.$wrapper.on('mouseleave', hideSearch);
// Search mouseover effects
$('#js-utility-search')
// Ensure that the cursor is over the search menu after timeout
.hover(function() {
_this.$wrapper.data('hover', 1);
}, function() {
_this.$wrapper.data('hover', 0);
})
.mouseover(function() {
// Reset timeout
clearTimeout(timer);
$header.on('mouseleave', hideSearch);
// Hover intent
// If user gets from header to search within .8s, they can pass over other icons
// but after .8s, the search menu will disappear if they leave the area
timer = setTimeout(function() {
$header.off('mouseleave', hideSearch);
// If cursor is no long on search, collapse it
if ( !_this.$wrapper.data('hover') ) {
hideSearch();
}
}, 800);
_this.$wrapper.show();
_this.$input.focus();
});
},
/**
* Applies keyup listener on search input to handle live results and navigation
*/
searchListener: function() {
var _this = this;
// shift, ctrl, alt, caps
var ignored_keycodes = [16, 17, 18, 20];
// left, up, right, down
var arrow_keycodes = [37, 38, 39, 40];
_this.$input.on('keyup', function(ev) {
// handle arrow keys
if (arrow_keycodes.indexOf(ev.keyCode) > -1) {
_this.moveSelection(ev.keyCode);
return;
}
// handle enter key
if (ev.keyCode === 13) {
// if there's a highlighted result, redirect to its href
var $current_link = $('.js-search-results a.-current');
if ($current_link.length) {
window.location = $current_link.attr('href');
}
return;
}
// ignore keys that won't change the result set
if (ignored_keycodes.indexOf(ev.keyCode) > -1) {
return;
}
// otherwise update the live search
var val = _this.$input.val();
var post_url = Fae.path + '/search/' + val;
// Match only values that exist within links - we don't want to highlight the labels
// Positive lookahead
// Ignore if > is present (to avoid matches in href=".*">)
// End at </a> tag (to ensure that we don't highlight any other markup, like the labels)
// Case insensitive; find all
var val_regex = new RegExp(val + '(?=[^>]*<\/a>)', 'ig');
$.post(post_url, function(data) {
// Wrap query in b tags
data = data.replace(val_regex, '<b>$&</b>');
_this.$wrapper.find('ul').replaceWith(data);
var $links = _this.$wrapper.find('a');
// Initial offset spoofs items' position relative to their parent (.js-search-results)
var initial_offset = $links.first().offset().top;
// Cache the offset for use later in moveSelection
$links.each(function() {
// Have to use offset because the element is hidden
// So .position() is ineffective
$(this).data('offset', $(this).offset().top - initial_offset);
});
// If only one result, remove border from header nav item (set by the .search-nav-item class)
if ($('.js-search-results li').length === 1) {
$('.js-search-results .search-nav-item').removeClass('search-nav-item');
}
});
});
},
/**
* Updates the selected result
* @param {Number} keyCode - the key pressed
*/
moveSelection: function(keyCode) {
var $results = $('.js-search-results');
var $result_links = $('.js-search-results a');
var $current_link = $('.js-search-results a.-current');
var current_index = $.inArray($current_link[0], $result_links);
// handle up and left arrow keys
if (keyCode === 37 || keyCode === 38) {
if (current_index <= 0) {
current_index = $result_links.length - 1;
} else {
current_index -= 1
}
}
// handle down and right arrow keys
if (keyCode === 39 || keyCode === 40) {
if (current_index + 1 >= $result_links.length) {
current_index = 0;
} else {
current_index += 1
}
}
// move current class to new selection
$current_link.removeClass('-current');
var $new_current = $result_links.eq(current_index);
$new_current.addClass('-current');
// Scroll to item
var offset = $new_current.data('offset');
// Reset to start if it's in the first "page" of results
// Add height in case it's inbetween "pages"
if( (offset + $new_current.outerHeight()) < $results.outerHeight() ) {
offset = 0;
}
$results.scrollTop( offset );
},
};