From 8ca6cc32b2c789a3149861159ad258f2cb9491e3 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 14:39:39 +0200 Subject: Adding upstream version 2.11.4. Signed-off-by: Daniel Baumann --- public/js/icinga/ui.js | 634 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 634 insertions(+) create mode 100644 public/js/icinga/ui.js (limited to 'public/js/icinga/ui.js') diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js new file mode 100644 index 0000000..b41de06 --- /dev/null +++ b/public/js/icinga/ui.js @@ -0,0 +1,634 @@ +/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +/** + * Icinga.UI + * + * Our user interface + */ +(function(Icinga, $) { + + 'use strict'; + + Icinga.UI = function (icinga) { + + this.icinga = icinga; + + this.currentLayout = 'default'; + + this.debug = false; + + this.debugTimer = null; + + this.timeCounterTimer = null; + + // detect currentLayout + var classList = $('#layout').attr('class').split(/\s+/); + var _this = this; + var matched; + $.each(classList, function(index, item) { + if (null !== (matched = item.match(/^([a-z]+)-layout$/))) { + var layout = matched[1]; + if (layout !== 'fullscreen') { + _this.currentLayout = layout; + // Break loop + return false; + } + } + }); + }; + + Icinga.UI.prototype = { + + initialize: function () { + $('html').removeClass('no-js').addClass('js'); + this.enableTimeCounters(); + this.triggerWindowResize(); + this.fadeNotificationsAway(); + + $(document).on('click', '#mobile-menu-toggle', this.toggleMobileMenu); + $(document).on('keypress', '#search',{ self: this, type: 'key' }, this.closeMobileMenu); + $(document).on('mouseleave', '#sidebar', { self: this, type: 'leave' }, this.closeMobileMenu); + $(document).on('click', '#sidebar a', { self: this, type: 'navigate' }, this.closeMobileMenu); + }, + + fadeNotificationsAway: function() { + var icinga = this.icinga; + $('#notifications li') + .not('.fading-out') + .not('.persist') + .addClass('fading-out') + .delay(7000) + .fadeOut('slow', + function() { + $(this).remove(); + }); + }, + + toggleDebug: function() { + if (this.debug) { + return this.disableDebug(); + } else { + return this.enableDebug(); + } + }, + + enableDebug: function () { + if (this.debug === true) { return this; } + this.debug = true; + this.debugTimer = this.icinga.timer.register( + this.refreshDebug, + this, + 1000 + ); + this.fixDebugVisibility(); + + return this; + }, + + fixDebugVisibility: function () { + if (this.debug) { + $('#responsive-debug').css({display: 'block'}); + } else { + $('#responsive-debug').css({display: 'none'}); + } + return this; + }, + + disableDebug: function () { + if (this.debug === false) { return; } + + this.debug = false; + this.icinga.timer.unregister(this.debugTimer); + this.debugTimer = null; + this.fixDebugVisibility(); + return this; + }, + + reloadCss: function () { + var icinga = this.icinga; + icinga.logger.info('Reloading CSS'); + $('link').each(function() { + var $oldLink = $(this); + if ($oldLink.hasAttr('type') && $oldLink.attr('type').indexOf('css') > -1) { + var $newLink = $oldLink.clone().attr( + 'href', + icinga.utils.addUrlParams( + $oldLink.attr('href'), + { id: new Date().getTime() } // Only required for Firefox to reload CSS automatically + ) + ).on('load', function() { + $oldLink.remove(); + $('head').trigger('css-reloaded'); + }); + + $newLink.appendTo($('head')); + } + }); + }, + + enableTimeCounters: function () { + this.timeCounterTimer = this.icinga.timer.register( + this.refreshTimeSince, + this, + 1000 + ); + return this; + }, + + disableTimeCounters: function () { + this.icinga.timer.unregister(this.timeCounterTimer); + this.timeCounterTimer = null; + return this; + }, + + /** + * Focus the given element and scroll to its position + * + * @param {string} element The name or id of the element to focus + * @param {object} [$container] The container containing the element + * @param {boolean} [scroll] Whether the viewport should be scrolled to the focused element + */ + focusElement: function(element, $container, scroll) { + var $element = element; + + if (typeof scroll === 'undefined') { + scroll = true; + } + + if (typeof element === 'string') { + if ($container && $container.length) { + $element = $container.find('#' + element); + } else { + $element = $('#' + element); + } + + if (! $element.length) { + // The name attribute is actually deprecated, on anchor tags, + // but we'll possibly handle links from another source + // (module etc) so that's used as a fallback + if ($container && $container.length) { + $element = $container.find('[name="' + element.replace(/'/, '\\\'') + '"]'); + } else { + $element = $('[name="' + element.replace(/'/, '\\\'') + '"]'); + } + } + } + + if ($element.length) { + if (! this.isFocusable($element)) { + $element.attr('tabindex', -1); + } + + $element[0].focus(); + + if (scroll && $container && $container.length) { + if (! $container.is('.container')) { + $container = $container.closest('.container'); + } + + if ($container.css('display') === 'flex' && $container.is('.container')) { + var $controls = $container.find('.controls'); + var $content = $container.find('.content'); + $content.scrollTop($element.offsetTopRelativeTo($content) - $controls.outerHeight() - ( + $element.outerHeight(true) - $element.innerHeight() + )); + } else { + $container.scrollTop($element.first().position().top); + } + } + } + }, + + isFocusable: function ($element) { + return $element.is('*[tabindex], a[href], input:not([disabled]), button:not([disabled])' + + ', select:not([disabled]), textarea:not([disabled]), iframe, area[href], object' + + ', embed, *[contenteditable]'); + }, + + moveToLeft: function () { + var col2 = this.cutContainer($('#col2')); + var kill = this.cutContainer($('#col1')); + this.pasteContainer($('#col1'), col2); + this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl($('#col1').data('icingaUrl')); + }, + + moveToRight: function () { + let col1 = document.getElementById('col1'), + col2 = document.getElementById('col2'), + col1Backup = this.cutContainer($(col1)); + + this.cutContainer($(col2)); // Clear col2 states + this.pasteContainer($(col2), col1Backup); + this.layout2col(); + }, + + cutContainer: function ($col) { + var props = { + 'elements': $('#' + $col.attr('id') + ' > *').detach(), + 'data': { + 'data-icinga-url': $col.data('icingaUrl'), + 'data-icinga-title': $col.data('icingaTitle'), + 'data-icinga-refresh': $col.data('icingaRefresh'), + 'data-last-update': $col.data('lastUpdate'), + 'data-icinga-module': $col.data('icingaModule'), + 'data-icinga-container-id': $col[0].dataset.icingaContainerId + }, + 'class': $col.attr('class') + }; + this.icinga.loader.stopPendingRequestsFor($col); + $col.removeData('icingaUrl'); + $col.removeData('icingaTitle'); + $col.removeData('icingaRefresh'); + $col.removeData('lastUpdate'); + $col.removeData('icingaModule'); + delete $col[0].dataset.icingaContainerId; + $col.removeAttr('class').attr('class', 'container'); + return props; + }, + + pasteContainer: function ($col, backup) { + backup['elements'].appendTo($col); + $col.attr('class', backup['class']); // TODO: ie memleak? remove first? + $col.data('icingaUrl', backup['data']['data-icinga-url']); + $col.data('icingaTitle', backup['data']['data-icinga-title']); + $col.data('icingaRefresh', backup['data']['data-icinga-refresh']); + $col.data('lastUpdate', backup['data']['data-last-update']); + $col.data('icingaModule', backup['data']['data-icinga-module']); + $col[0].dataset.icingaContainerId = backup['data']['data-icinga-container-id']; + }, + + triggerWindowResize: function () { + this.onWindowResize({data: {self: this}}); + }, + + /** + * Our window got resized, let's fix our UI + */ + onWindowResize: function (event) { + var _this = event.data.self; + + if (_this.layoutHasBeenChanged()) { + _this.icinga.logger.info( + 'Layout change detected, switching to', + _this.currentLayout + ); + } + + _this.refreshDebug(); + }, + + /** + * Returns whether the layout is too small for more than one column + * + * @returns {boolean} True when more than one column is available + */ + hasOnlyOneColumn: function () { + return this.currentLayout === 'poor' || this.currentLayout === 'minimal'; + }, + + layoutHasBeenChanged: function () { + + var layout = $('html').css('fontFamily').replace(/['",]/g, ''); + var matched; + + if (null !== (matched = layout.match(/^([a-z]+)-layout$/))) { + if (matched[1] === this.currentLayout && + $('#layout').hasClass(layout) + ) { + return false; + } else { + $('#layout').removeClass(this.currentLayout + '-layout').addClass(layout); + this.currentLayout = matched[1]; + if (this.currentLayout === 'poor' || this.currentLayout === 'minimal') { + this.layout1col(); + } else if (this.icinga.initialized) { + // layout1col() also triggers this, that's why an else is required + $('#layout').trigger('layout-change'); + } + return true; + } + } + this.icinga.logger.error( + 'Someone messed up our responsiveness hacks, html font-family is', + layout + ); + return false; + }, + + /** + * Returns whether only one column is displayed + * + * @returns {boolean} True when only one column is displayed + */ + isOneColLayout: function () { + return ! $('#layout').hasClass('twocols'); + }, + + layout1col: function () { + if (this.isOneColLayout()) { return; } + this.icinga.logger.debug('Switching to single col'); + $('#layout').removeClass('twocols'); + this.closeContainer($('#col2')); + + if (this.icinga.initialized) { + $('#layout').trigger('layout-change'); + } + + // one-column layouts never have any selection active + $('#col1').removeData('icinga-actiontable-former-href'); + this.icinga.behaviors.actiontable.clearAll(); + }, + + closeContainer: function($c) { + $c.removeData('icingaUrl'); + $c.removeData('icingaTitle'); + $c.removeData('icingaRefresh'); + $c.removeData('lastUpdate'); + $c.removeData('icingaModule'); + delete $c[0].dataset.icingaContainerId; + $c.removeAttr('class').attr('class', 'container'); + this.icinga.loader.stopPendingRequestsFor($c); + $c.trigger('close-column'); + $c.html(''); + }, + + layout2col: function () { + if (! this.isOneColLayout()) { return; } + this.icinga.logger.debug('Switching to double col'); + $('#layout').addClass('twocols'); + + if (this.icinga.initialized) { + $('#layout').trigger('layout-change'); + } + }, + + prepareColumnFor: function ($el, $target) { + var explicitTarget; + + if ($target.attr('id') === 'col2') { + if ($el.closest('#col2').length) { + explicitTarget = $el.closest('[data-base-target]').data('baseTarget'); + if (typeof explicitTarget !== 'undefined' && explicitTarget === '_next') { + this.moveToLeft(); + } + } else { + this.layout2col(); + } + } else { // if ($target.attr('id') === 'col1') + explicitTarget = $el.closest('[data-base-target]').data('baseTarget'); + if (typeof explicitTarget !== 'undefined' && explicitTarget === '_main') { + this.layout1col(); + } + } + }, + + getAvailableColumnSpace: function () { + return $('#main').width() / this.getDefaultFontSize(); + }, + + setColumnCount: function (count) { + if (count === 3) { + $('#main > .container').css({ + width: '33.33333%' + }); + } else if (count === 2) { + $('#main > .container').css({ + width: '50%' + }); + } else { + $('#main > .container').css({ + width: '100%' + }); + } + }, + + setTitle: function (title) { + document.title = title; + return this; + }, + + getColumnCount: function () { + return $('#main > .container').length; + }, + + /** + * Assign a unique ID to each .container without such + * + * This usually applies to dashlets + */ + assignUniqueContainerIds: function() { + var currentMax = 0; + $('.container').each(function() { + var $el = $(this); + var m; + if (!$el.attr('id')) { + return; + } + if (m = $el.attr('id').match(/^ciu_(\d+)$/)) { + if (parseInt(m[1]) > currentMax) { + currentMax = parseInt(m[1]); + } + } + }); + $('.container').each(function() { + var $el = $(this); + if (!!$el.attr('id')) { + return; + } + currentMax++; + $el.attr('id', 'ciu_' + currentMax); + }); + }, + + refreshDebug: function () { + if (! this.debug) { + return; + } + + var size = this.getDefaultFontSize().toString(); + var winWidth = $( window ).width(); + var winHeight = $( window ).height(); + var loading = ''; + + $.each(this.icinga.loader.requests, function (el, req) { + if (loading === '') { + loading = '
Loading:
'; + } + loading += el + ' => ' + encodeURI(req.url); + }); + + $('#responsive-debug').html( + ' Time: ' + + this.icinga.utils.formatHHiiss(new Date()) + + '
1em: ' + + size + + 'px
Win: ' + + winWidth + + 'x'+ + winHeight + + 'px
' + + ' Layout: ' + + this.currentLayout + + loading + ); + }, + + /** + * Refresh partial time counters + * + * This function runs every second. + */ + refreshTimeSince: function () { + $('.time-ago, .time-since').each(function (idx, el) { + var partialTime = /(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML); + if (partialTime !== null) { + var minute = parseInt(partialTime[1], 10), + second = parseInt(partialTime[2], 10); + if (second < 59) { + ++second; + } else { + ++minute; + second = 0; + } + el.innerHTML = el.innerHTML.substr(0, partialTime.index) + minute.toString() + 'm ' + + second.toString() + 's' + el.innerHTML.substr(partialTime.index + partialTime[0].length); + } + }); + + $('.time-until').each(function (idx, el) { + var partialTime = /(-?)(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML); + if (partialTime !== null) { + var minute = parseInt(partialTime[2], 10), + second = parseInt(partialTime[3], 10), + invert = partialTime[1]; + if (invert.length) { + // Count up because partial time is negative + if (second < 59) { + ++second; + } else { + ++minute; + second = 0; + } + } else { + // Count down because partial time is positive + if (second === 0) { + if (minute === 0) { + // Invert counter + minute = 0; + second = 1; + invert = '-'; + } else { + --minute; + second = 59; + } + } else { + --second; + } + } + el.innerHTML = el.innerHTML.substr(0, partialTime.index) + invert + minute.toString() + 'm ' + + second.toString() + 's' + el.innerHTML.substr(partialTime.index + partialTime[0].length); + } + }); + }, + + createFontSizeCalculator: function () { + var $el = $('
 
'); + $('#layout').append($el); + return $el; + }, + + getDefaultFontSize: function () { + var $calc = $('#fontsize-calc'); + if (! $calc.length) { + $calc = this.createFontSizeCalculator(); + } + return $calc.width() / 1000; + }, + + /** + * Toggle mobile menu + * + * @param {object} e Event + */ + toggleMobileMenu: function(e) { + $('#sidebar').toggleClass('expanded'); + }, + + /** + * Close mobile menu when the enter key is pressed during search or the user leaves the sidebar + * + * @param {object} e Event + */ + closeMobileMenu: function(e) { + if (e.data.self.currentLayout !== 'minimal') { + return; + } + + if (e.data.type === 'key') { + if (e.which === 13) { + $('#sidebar').removeClass('expanded'); + $(e.target)[0].blur(); + } + } else { + $('#sidebar').removeClass('expanded'); + } + }, + + toggleFullscreen: function () { + $('#layout').toggleClass('fullscreen-layout'); + }, + + getUniqueContainerId: function (container) { + if (typeof container.jquery !== 'undefined') { + if (! container.length) { + return null; + } + + container = container[0]; + } else if (typeof container === 'undefined') { + return null; + } + + var containerId = container.dataset.icingaContainerId || null; + if (containerId === null) { + /** + * Only generate an id if it's not for col1 or the menu (which are using the non-suffixed window id). + * This is based on the assumption that the server only knows about the menu and first column + * and therefore does not need to protect its ids. (As the menu is most likely part of the sidebar) + */ + var col1 = document.getElementById('col1'); + if (container.id !== 'menu' && col1 !== null && ! col1.contains(container)) { + containerId = this.icinga.utils.generateId(6); // Random because the content may move + container.dataset.icingaContainerId = containerId; + } + } + + return containerId; + }, + + getWindowId: function () { + if (! this.hasWindowId()) { + return undefined; + } + return window.name.match(/^Icinga-([a-zA-Z0-9]+)$/)[1]; + }, + + hasWindowId: function () { + var res = window.name.match(/^Icinga-([a-zA-Z0-9]+)$/); + return typeof res === 'object' && null !== res; + }, + + setWindowId: function (id) { + this.icinga.logger.debug('Setting new window id', id); + window.name = 'Icinga-' + id; + }, + + destroy: function () { + // This is gonna be hard, clean up the mess + this.icinga = null; + this.debugTimer = null; + this.timeCounterTimer = null; + } + }; + +}(Icinga, jQuery)); -- cgit v1.2.3