(function (Icinga) {
var Director = function (module) {
this.module = module;
this.initialize();
this.openedFieldsets = {};
this.module.icinga.logger.debug('Director module loaded');
};
Director.prototype = {
initialize: function () {
/**
* Tell Icinga about our event handlers
*/
this.module.on('rendered', this.rendered);
this.module.on('beforerender', this.beforeRender);
this.module.on('click', 'fieldset > legend', this.toggleFieldset);
// Disabled
// this.module.on('click', 'div.controls ul.tabs a', this.detailTabClick);
this.module.on('click', 'input.related-action', this.extensibleSetAction);
this.module.on('click', 'ul.filter-root input[type=submit]', this.setAutoSubmitted);
this.module.on('focus', 'form input, form textarea, form select', this.formElementFocus);
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('mousedown', '.director-suggestions li', this.clickSuggestion);
this.module.on('dblclick', 'ul.tabs a', this.tabWantsFullscreen);
this.module.on('change', 'form input.autosubmit, form select.autosubmit', this.setAutoSubmitted);
this.module.icinga.logger.debug('Director module initialized');
},
tabWantsFullscreen: function (ev) {
var icinga = this.module.icinga;
var $a, $container, id;
if (icinga.ui.isOneColLayout()) {
return;
}
$a = $(ev.currentTarget);
if ($a.hasClass('refresh-container-control')) {
return;
}
$container = $a.closest('.container');
id = $container.attr('id');
icinga.loader.stopPendingRequestsFor($container);
if (id === 'col2') {
icinga.ui.moveToLeft();
}
icinga.ui.layout1col();
icinga.history.pushCurrentState();
ev.preventDefault();
ev.stopPropagation();
},
/**
* Autocomplete/suggestion eventhandler
*
* Triggered when pressing a key in a form element with suggestions
* @param ev
*/
suggestionKeyDown: function (ev) {
var $el = $(ev.currentTarget);
var key = ev.which;
if (key === 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.hasActiveSuggestion($el)) {
this.chooseActiveSuggestion($el);
ev.stopPropagation();
ev.preventDefault();
} else {
this.removeSuggestionList($el);
if ($el.closest('.extensible-set')) {
$el.trigger('change');
} else {
$el.closest('form').submit();
}
}
} else if (key === 27) {
// ESC key pressed. Remove suggestions if any
this.removeSuggestionList($el);
} else if (key === 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 (key === 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 (key === 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'
var key = ev.which;
if (key === 9 || // TAB
key === 13 || // RETURN
key === 27 || // ESC
key === 37 || // LEFT ARROW
key === 38 || // UP ARROW
key === 39 ) { // RIGHT ARROW
return;
}
var $el = $(ev.currentTarget);
if (key === 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();
$el.val($el.val());
}
},
/**
* 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 = $('
');
$input.parent().css({
position: 'relative'
});
$ul.insertAfter($input);
var suggestionWidth = (parseInt($input.css('width')) * 2) + 'px';
$ul.css({width: suggestionWidth});
return this.refreshSuggestionList($ul, $input);
}
},
/**
* Refresh a given suggestion list
*
* @param $suggestions
*
* @param $el
* @returns {jQuery}
*/
refreshSuggestionList: function ($suggestions, $el) {
// Not sure whether we need this Accept-header
var headers = { 'X-Icinga-Accept': 'text/html' };
var icinga = this.module.icinga;
// Ask for a new window id in case we don't already have one
if (icinga.ui.hasWindowId()) {
headers['X-Icinga-WindowId'] = icinga.ui.getWindowId();
} else {
headers['X-Icinga-WindowId'] = 'undefined';
}
// var onResponse = function (data, textStatus, req) {
var onResponse = function (data) {
$suggestions.html(data);
var $li = $suggestions.find('li');
if ($li.length) {
$suggestions.show();
} else {
$suggestions.hide();
}
};
var req = $.ajax({
type: 'POST',
url: this.module.icinga.config.baseUrl + '/director/suggest',
data: {
value: $el.val(),
context: $el.data('suggestion-context'),
for_host: $el.data('suggestion-for-host')
},
headers: headers
});
req.done(onResponse);
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();
// extract label and key from key
var re = /^(.+) \[(\w+)]$/;
var withLabel = val.match(re);
if (withLabel) {
val = withLabel[2];
}
if (val.match(/\.$/)) {
$el.val(val);
this.getSuggestionList($el, true);
} else {
$el.focus();
$el.val(val);
$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 === 0) {
$active = $list.find('li:hover');
}
if ($active.length) {
this.chooseSuggestion($active);
return true;
} else {
$list.remove();
return false;
}
},
hasActiveSuggestion: function ($el) {
if (this.hasSuggestions($el)) {
var $list = this.getSuggestionList($el);
var $active = $list.find('li.active');
if ($active.length === 0) {
$active = $list.find('li:hover');
}
return $active.length > 0;
} else {
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 empty auto-completion field
*
* @param ev
*/
enterSuggestionField: function (ev) {
// Has been disabled long time ago, as we do not want to open
// extensible Sets on focus. Should we re-enable this and just
// blacklist extensible sets?
//
// 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');
},
/**
* Caused problems with differing tabs, should not be used
*
* @deprecated
*/
detailTabClick: function (ev) {
var $a = $(ev.currentTarget);
if ($a.closest('#col2').length === 0) {
return;
}
this.alignDetailLinks();
},
/**
* Caused problems with differing tabs, should not be used
*
* @deprecated
*/
alignDetailLinks: function () {
var self = this;
var $a = $('#col2').find('div.controls ul.tabs li.active a');
if ($a.length !== 1) {
return;
}
var $leftTable = $('#col1').find('> div.content').find('table.icinga-objects');
if ($leftTable.length !== 1) {
return;
}
var tabPath = self.pathFromHref($a);
$leftTable.find('tr').each(function (idx, tr) {
var $tr = $(tr);
if ($tr.is('[href]')) {
self.setHrefPath($tr, tabPath);
} else {
// Unfortunately we currently run BEFORE the action table
// handler
var $a = $tr.find('a[href].rowaction');
if ($a.length === 0) {
$a = $tr.find('a[href]').first();
}
if ($a.length) {
self.setHrefPath($a, tabPath);
}
}
});
$leftTable.find('tr[href]').each(function (idx, tr) {
var $tr = $(tr);
self.setHrefPath($tr, tabPath);
});
},
pathFromHref: function ($el) {
return this.module.icinga.utils.parseUrl($el.attr('href')).path
},
setHrefPath: function ($el, path) {
var a = this.module.icinga.utils.getUrlHelper();
a.href = $el.attr('href');
a.pathname = path;
$el.attr('href', a.href);
},
extensibleSetAction: function (ev) {
var iid, $li, $prev, $next;
var el = ev.currentTarget;
if (el.name.match(/__MOVE_UP$/)) {
$li = $(el).closest('li');
$prev = $li.prev();
// TODO: document what's going on here.
if ($li.find('input[type=text].autosubmit')) {
iid = $prev.find('input[type=text]').attr('id');
if (iid) {
$li.closest('.container').data('activeExtensibleEntry', iid);
} else {
return true;
}
}
if ($prev.length) {
$prev.before($li.detach());
this.fixRelatedActions($li.closest('ul'));
}
ev.preventDefault();
ev.stopPropagation();
return false;
} else if (el.name.match(/__MOVE_DOWN$/)) {
$li = $(el).closest('li');
$next = $li.next();
// TODO: document what's going on here.
if ($li.find('input[type=text].autosubmit')) {
iid = $next.find('input[type=text]').attr('id');
if (iid) {
$li.closest('.container').data('activeExtensibleEntry', iid);
} else {
return true;
}
}
if ($next.length && ! $next.find('.extend-set').length) {
$next.after($li.detach());
this.fixRelatedActions($li.closest('ul'));
}
ev.preventDefault();
ev.stopPropagation();
return false;
} else if (el.name.match(/__REMOVE$/)) {
$li = $(el).closest('li');
if ($li.find('.autosubmit').length) {
// Autosubmit element, let the server handle this
return true;
}
$li.remove();
this.fixRelatedActions($li.closest('ul'));
ev.preventDefault();
ev.stopPropagation();
return false;
} else if (el.name.match(/__DROP_DOWN$/)) {
ev.preventDefault();
ev.stopPropagation();
var $el = $(ev.currentTarget).closest('li').find('input[type=text]');
this.getSuggestionList($el);
return false;
}
},
fixRelatedActions: function ($ul) {
var $uls = $ul.find('li');
var last = $uls.length - 1;
if ($ul.find('.extend-set').length) {
last--;
}
$uls.each(function (idx, li) {
var $li = $(li);
if (idx === 0) {
$li.find('.action-move-up').attr('disabled', 'disabled');
if (last === 0) {
$li.find('.action-move-down').attr('disabled', 'disabled');
} else {
$li.find('.action-move-down').removeAttr('disabled');
}
} else if (idx === last) {
$li.find('.action-move-up').removeAttr('disabled');
$li.find('.action-move-down').attr('disabled', 'disabled');
} else {
$li.find('.action-move-up').removeAttr('disabled');
$li.find('.action-move-down').removeAttr('disabled');
}
});
},
formElementFocus: function (ev) {
var $input = $(ev.currentTarget);
if ($input.closest('form.editor').length) {
return;
}
var $set = $input.closest('.extensible-set');
if ($set.length) {
var $textInputs = $('input[type=text]', $set);
if ($textInputs.length > 1) {
$textInputs.not(':first').attr('tabIndex', '-1');
}
}
var $dd = $input.closest('dd');
if ($dd.attr('id') && $dd.attr('id').match(/button/)) {
return;
}
var $li = $input.closest('li');
var $dt = $dd.prev();
var $form = $dd.closest('form');
$form.find('dt, dd, li').removeClass('active');
$li.addClass('active');
$dt.addClass('active');
$dd.addClass('active');
},
highlightFormErrors: function ($container) {
$container.find('dd ul.errors').each(function (idx, ul) {
var $ul = $(ul);
var $dd = $ul.closest('dd');
var $dt = $dd.prev();
$dt.addClass('errors');
$dd.addClass('errors');
});
},
toggleFieldset: function (ev) {
ev.stopPropagation();
var $fieldset = $(ev.currentTarget).closest('fieldset');
$fieldset.toggleClass('collapsed');
this.fixFieldsetInfo($fieldset);
this.openedFieldsets[$fieldset.attr('id')] = ! $fieldset.hasClass('collapsed');
},
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) {
$container.data('director-autorefreshed', 'yes');
} 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';
},
rendered: function (ev) {
var iid;
var icinga = this.module.icinga;
var $container = $(ev.currentTarget);
if ($container.children('div.controls').first().data('directorWindowId') === '_UNDEFINED_') {
var $url = $container.data('icingaUrl');
if (typeof $url !== 'undefined') {
icinga.loader.loadUrl($url, $container).autorefresh = true;
}
$container.children('div.controls').children().hide();
$container.children('div.content').hide();
return;
}
this.restoreContainerFieldsets($container);
this.backupAllExtensibleSetDefaultValues($container);
this.highlightFormErrors($container);
this.scrollHighlightIntoView($container);
this.scrollActiveRowIntoView($container);
this.highlightActiveDashlet($container);
iid = $container.data('activeExtensibleEntry');
if (iid) {
$('#' + iid).focus();
$container.removeData('activeExtensibleEntry');
}
// Disabled for now
// this.alignDetailLinks();
if (! this.containerIsAutorefreshed($container) && ! this.containerIsAutoSubmitted($container)) {
this.putFocusOnFirstFormElement($container);
}
// Turn off autocomplete for all suggested fields
$container.find('input.director-suggest').each(this.disableAutocomplete);
},
highlightActiveDashlet: function ($container) {
if (this.module.icinga.ui.isOneColLayout()) {
return;
}
var url, $actions, $match;
var id = $container.attr('id');
if (id === 'col1') {
url = $('#col2').data('icingaUrl');
$actions = $('.main-actions', $container);
} else if (id === 'col2') {
url = $container.data('icingaUrl');
$actions = $('.main-actions', $('#col1'));
}
if (! $actions.length) {
return;
}
$match = $('li a[href*="' + url + '"]', $actions);
if ($match.length) {
$('li a.active', $actions).removeClass('active');
$match.first().addClass('active');
}
},
restoreContainerFieldsets: function ($container) {
var self = this;
$container.find('form').each(self.restoreFieldsets.bind(self));
},
putFocusOnFirstFormElement: function ($container) {
$container.find('form.autofocus').find('label').first().focus();
},
scrollHighlightIntoView: function ($container) {
var $hl = $container.find('.highlight');
var $content = $container.find('> div.content');
if ($hl.length) {
$container.animate({
scrollTop: $hl.offset().top - $content.offset().top
}, 700);
}
},
scrollActiveRowIntoView: function ($container) {
var $tr = $container.find('table.table-row-selectable > tbody > tr.active');
var $content = $container.find('> div.content');
if ($tr.length) {
$container.animate({
scrollTop: $tr.offset().top - $content.offset().top
}, 500);
}
},
backupAllExtensibleSetDefaultValues: function ($container) {
var self = this;
$container.find('.extensible-set').each(function (idx, eSet) {
$(eSet).find('input[type=text]').each(self.backupDefaultValue);
$(eSet).find('select').each(self.backupDefaultValue);
});
},
backupDefaultValue: function (idx, el) {
$(el).data('originalvalue', el.value);
},
restoreFieldsets: function (idx, form) {
var $form = $(form);
var self = this;
var $sets = $('fieldset', $form);
$sets.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);
}
});
if ($sets.length === 1) {
$sets.first().removeClass('collapsed');
}
},
fieldsetWasOpened: function ($fieldset) {
var id = $fieldset.attr('id');
if (typeof this.openedFieldsets[id] === 'undefined') {
return false;
}
return this.openedFieldsets[id];
},
fixFieldsetInfo: function ($fieldset) {
if ($fieldset.hasClass('collapsed')) {
if ($fieldset.find('legend span.element-count').length === 0) {
var cnt = $fieldset.find('dt, li').not('.extensible-set li').length;
if (cnt > 0) {
$fieldset.find('legend').append($(' (' + cnt + ')'));
}
}
} else {
$fieldset.find('legend span.element-count').remove();
}
},
disableAutocomplete: function () {
$(this)
.attr('autocomplete', 'off')
.attr('autocorrect', 'off')
.attr('autocapitalize', 'off')
.attr('spellcheck', 'false');
}
};
Icinga.availableModules.director = Director;
}(Icinga));