diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:17:31 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:17:31 +0000 |
commit | f66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch) | |
tree | fbff2135e7013f196b891bbde54618eb050e4aaf /public/js/module.js | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip |
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | public/js/module.js | 840 |
1 files changed, 840 insertions, 0 deletions
diff --git a/public/js/module.js b/public/js/module.js new file mode 100644 index 0000000..07fe265 --- /dev/null +++ b/public/js/module.js @@ -0,0 +1,840 @@ + +(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 = $('<ul class="director-suggestions"></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($('<span class="element-count"> (' + cnt + ')</span>')); + } + } + } 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)); |