js,css: introduce autosuggestion support

This commit is contained in:
Thomas Gelf 2016-11-17 20:31:58 +01:00
parent 0e7ce35380
commit 8b2aa8c2f9
2 changed files with 394 additions and 4 deletions

View File

@ -1152,7 +1152,7 @@ ul.filter-root {
padding-left: 0.5em;
list-style-type: none;
ul {
ul.filter {
padding-left: 1.5em;
list-style-type: none;
width: 100%;
@ -1251,9 +1251,37 @@ div.filter-expression {
}
}
ul.director-suggestions {
width: 20em;
max-width: 30em;
max-height: 25em;
overflow-y: auto;
overflow-x: hidden;
border: 1px solid @gray-light;
position: absolute;
z-index: 2000;
padding: 0;
margin: 0;
list-style-type: none;
background-color: rgba(255, 255, 255, 100);
li {
margin: 0;
padding: 0.5em 1em;
}
li:hover {
background-color: @gray-lighter;
cursor: pointer;
}
li.active {
background-color: @icinga-blue;
color: white;
&:hover {
color: inherit;
}
}
}
.tree li a {
display: inline-block;

View File

@ -26,6 +26,340 @@
this.module.on('click', 'input.related-action', this.extensibleSetAction);
this.module.on('focus', 'form input, form textarea, form select', this.formElementFocus);
this.module.icinga.logger.debug('Director module initialized');
this.module.on('keyup', '.director-suggest', this.autoSuggest);
this.module.on('keydown', '.director-suggest', this.suggestionKeyDown);
this.module.on('dblclick', '.director-suggest', this.suggestionDoubleClick);
this.module.on('focus', '.director-suggest', this.enterSuggestionField);
this.module.on('focusout', '.director-suggest', this.leaveSuggestionField);
this.module.on('click', '.director-suggestions li', this.clickSuggestion);
this.module.on('change', 'form input.autosubmit, form select.autosubmit', this.setAutoSubmitted);
},
/**
* Autocomplete/suggestion eventhandler
*
* Triggered when pressing a key in a form element with suggestions
*
* @param ev
*/
suggestionKeyDown: function(ev) {
var $suggestions, $active;
var $el = $(ev.currentTarget);
if (ev.keyCode === 13) {
/**
* RETURN key pressed. In case there are any suggestions:
* - let's choose the active one (if set)
* - stop the event
*
* This let's return bubble up in case there is no suggestion list shown
*/
if (this.hasSuggestions($el)) {
this.chooseActiveSuggestion($el);
ev.stopPropagation();
ev.preventDefault();
} else {
this.removeSuggestionList($el);
$el.trigger('change');
}
} else if (ev.keyCode == 27) {
// ESC key pressed. Remove suggestions if any
this.removeSuggestionList($el);
} else if (ev.keyCode == 39) {
/**
* RIGHT ARROW key pressed. In case there are any suggestions:
* - let's choose the active one (if set)
* - stop the event only if an element has been chosen
*
* This allows to use the right arrow key normally in all other situations
*/
if (this.hasSuggestions($el)) {
if (this.chooseActiveSuggestion($el)) {
ev.stopPropagation();
ev.preventDefault();
}
}
} else if (ev.keyCode == 38 ) {
/**
* UP ARROW key pressed. In any case:
* - stop the event
* - activate the previous suggestion if any
*/
ev.stopPropagation();
ev.preventDefault();
this.activatePrevSuggestion($el);
} else if (ev.keyCode == 40 ) { // down
/**
* DOWN ARROW key pressed. In any case:
* - stop the event
* - activate the next suggestion if any
*/
ev.stopPropagation();
ev.preventDefault();
this.activateNextSuggestion($el);
}
},
suggestionDoubleClick: function (ev)
{
var $el = $(ev.currentTarget);
this.getSuggestionList($el)
},
/**
* Autocomplete/suggestion eventhandler
*
* Triggered when releasing a key in a form element with suggestions
*
* @param ev
*/
autoSuggest: function(ev)
{
// Ignore special keys, most of them have already been handled on 'keydown'
if (ev.keyCode == 9 || // TAB
ev.keyCode == 13 || // RETURN
ev.keyCode == 27 || // ESC
ev.keyCode == 37 || // LEFT ARROW
ev.keyCode == 38 || // UP ARROW
ev.keyCode == 39 ) { // RIGHT ARROW
return;
}
var $el = $(ev.currentTarget);
if (ev.keyCode == 40) { // DOWN ARROW
this.getSuggestionList($el);
} else {
this.getSuggestionList($el, true);
}
},
/**
* Activate the next related suggestion if any
*
* This walks down the suggestion list, takes care about scrolling and restarts from
* top once reached the bottom
*
* @param $el
*/
activateNextSuggestion: function($el)
{
var $list = this.getSuggestionList($el);
var $next;
var $active = $list.find('li.active');
if ($active.length) {
$next = $active.next('li');
if ($next.length === 0) {
$next = $list.find('li').first();
}
} else {
$next = $list.find('li').first();
}
if ($next.length) {
// Will not happen when list is empty or last element is active
$list.find('li.active').removeClass('active');
$next.addClass('active');
$list.scrollTop($next.offset().top - $list.offset().top - 64 + $list.scrollTop());
}
},
/**
* Activate the previous related suggestion if any
*
* This walks up through the suggestion list and takes care about scrolling.
* Puts the focus back on the input field once reached the top and restarts
* from bottom when moving up from there
*
* @param $el
*/
activatePrevSuggestion: function($el)
{
var $list = this.getSuggestionList($el);
var $prev;
var $active = $list.find('li.active');
if ($active.length) {
$prev = $active.prev('li');
} else {
$prev = $list.find('li').last();
}
$list.find('li.active').removeClass('active');
if ($prev.length) {
$prev.addClass('active');
$list.scrollTop($prev.offset().top - $list.offset().top - 64 + $list.scrollTop());
} else {
$el.focus();
}
},
/**
* Whether a related suggestion list element exists
*
* @param $input
* @returns {boolean}
*/
hasSuggestionList: function($input) {
var $ul = $input.siblings('ul.director-suggestions');
return $ul.length > 0;
},
/**
* Whether any related suggestions are currently being shown
*
* @param $input
* @returns {boolean}
*/
hasSuggestions: function($input) {
var $ul = $input.siblings('ul.director-suggestions');
return $ul.length > 0 && $ul.is(':visible');
},
/**
* Get a suggestion list. Optionally force refresh
*
* @param $input
* @param $forceRefresh
*
* @returns {jQuery}
*/
getSuggestionList: function($input, $forceRefresh)
{
var $ul = $input.siblings('ul.director-suggestions');
if ($ul.length) {
if ($forceRefresh) {
return this.refreshSuggestionList($ul, $input);
} else {
return $ul;
}
} else {
$ul = $('<ul class="director-suggestions"></ul>');
$ul.insertAfter($input);
return this.refreshSuggestionList($ul, $input);
}
},
/**
* Refresh a given suggestion list
*
* @param $suggestions
*
* @param $el
* @returns {jQuery}
*/
refreshSuggestionList: function($suggestions, $el)
{
$suggestions.load(icinga.config.baseUrl + '/director/suggest', {
value: $el.val(),
context: $el.data('suggestion-context')
}, function (responseText, textStatus, jqXHR) {
var $li = $suggestions.find('li');
if ($li.length) {
$suggestions.show();
} else {
$suggestions.hide();
}
});
return $suggestions;
},
/**
* Click handler for proposed suggestions
*
* @param ev
*/
clickSuggestion: function(ev) {
this.chooseSuggestion($(ev.currentTarget));
},
/**
* Choose a specific suggestion
* @param $suggestion
*/
chooseSuggestion: function($suggestion)
{
var $el = $suggestion.closest('ul').siblings('.director-suggest');
var val = $suggestion.text();
var $list =
$el.val(val);
if (val.match(/\.$/)) {
this.getSuggestionList($el, true);
} else {
$el.focus();
$el.trigger('change');
this.getSuggestionList($el).remove();
}
},
/**
* Choose the current active suggestion related to a given element
*
* Returns true in case there was any, false otherwise
*
* @param $el
* @returns {boolean}
*/
chooseActiveSuggestion: function($el)
{
var $list = this.getSuggestionList($el);
var $active = $list.find('li.active');
if ($active.length) {
this.chooseSuggestion($active);
return true;
} else {
$list.remove();
return false;
}
},
/**
* Remove related suggestion list if any
*
* @param $el
*/
removeSuggestionList: function($el)
{
if (this.hasSuggestionList($el)) {
this.getSuggestionList($el).remove();
}
},
/**
* Show suggestions when arriving to an empte autocompletion field
*
* @param ev
*/
enterSuggestionField: function(ev) {
var $el = $(ev.currentTarget);
if ($el.val() === '' || $el.val().match(/\.$/)) {
this.getSuggestionList($el)
}
},
/**
* Close suggestions when leaving the related form element
*
* @param ev
*/
leaveSuggestionField: function(ev) {
return;
var _this = this;
setTimeout(function() {
_this.removeSuggestionList($(ev.currentTarget));
}, 100);
},
/**
* Sets an autosubmit flag on the container related to an event
*
* This will be used in beforeRender to determine whether the request has been triggered by an
* auto-submission
*
* @param ev
*/
setAutoSubmitted: function(ev) {
$(ev.currentTarget).closest('.container').data('directorAutosubmit', 'yes');
},
/**
@ -240,7 +574,6 @@
beforeRender: function(ev) {
var $container = $(ev.currentTarget);
var id = $container.attr('id');
var requests = this.module.icinga.loader.requests;
if (typeof requests[id] !== 'undefined' && requests[id].autorefresh) {
@ -248,8 +581,34 @@
} else {
$container.removeData('director-autorefreshed');
}
// Remove the temporary directorAutosubmit flag and set or remove
// the directorAutosubmitted property accordingly
if ($container.data('directorAutosubmit') === 'yes') {
$container.removeData('directorAutosubmit');
$container.data('directorAutosubmitted', 'yes');
} else {
$container.removeData('directorAutosubmitted');
}
},
/**
* Whether the given container has been autosubmitted
*
* @param $container
* @returns {boolean}
*/
containerIsAutoSubmitted: function($container)
{
return $container.data('directorAutosubmitted') === 'yes';
},
/**
* Whether the given container has been autorefreshed
*
* @param $container
* @returns {boolean}
*/
containerIsAutorefreshed: function($container)
{
return $container.data('director-autorefreshed') === 'yes';
@ -271,7 +630,7 @@
// Disabled for now
// this.alignDetailLinks();
if (! this.containerIsAutorefreshed($container)) {
if (! this.containerIsAutorefreshed($container) && ! this.containerIsAutoSubmitted($container)) {
this.putFocusOnFirstFormElement($container);
}
},
@ -326,6 +685,9 @@
$('fieldset', $form).each(function(idx, fieldset) {
var $fieldset = $(fieldset);
if ($fieldset.attr('id') === 'fieldset-assign') {
return;
}
if ($fieldset.find('.required').length == 0 && (! self.fieldsetWasOpened($fieldset))) {
$fieldset.addClass('collapsed');
self.fixFieldsetInfo($fieldset);