/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ /** * Icinga.Events * * Event handlers */ (function (Icinga, $) { 'use strict'; Icinga.Events = function (icinga) { this.icinga = icinga; this.searchValue = ''; this.initializeModules = true; }; Icinga.Events.prototype = { /** * Icinga will call our initialize() function once it's ready */ initialize: function () { this.applyGlobalDefaults(); $('#layout').trigger('rendered'); //$('.container').trigger('rendered'); $('.container').each(function(idx, el) { icinga.ui.initializeControls($(el)); }); }, // TODO: What's this? applyHandlers: function (event) { var $target = $(event.target); var self = event.data.self; var icinga = self.icinga; if (! icinga) { // Attempt to catch a rare error, race condition, whatever console.log('Got no icinga in applyHandlers'); return; } if (self.initializeModules) { var loaded = false; var moduleName = $target.data('icingaModule'); if (moduleName) { if (icinga.hasModule(moduleName) && !icinga.isLoadedModule(moduleName)) { loaded |= icinga.loadModule(moduleName); } } $('.icinga-module', $target).each(function(idx, mod) { moduleName = $(mod).data('icingaModule'); if (icinga.hasModule(moduleName) && !icinga.isLoadedModule(moduleName)) { loaded |= icinga.loadModule(moduleName); } }); if (loaded) { // Modules may register their own handler for the 'renderend' event // so we need to ensure that it is called the first time they are // initialized event.stopImmediatePropagation(); self.initializeModules = false; var $container = $target.closest('.container'); if (! $container.length) { // The page obviously got loaded for the first time, // so we'll trigger the event for all containers $container = $('.container'); } $container.trigger('rendered'); // But since we're listening on this event by ourself, we'll have // to abort our own processing as we'll process it twice otherwise return false; } } else { self.initializeModules = true; } $('.dashboard > div', $target).each(function(idx, el) { var $element = $(el); var $url = $element.data('icingaUrl'); if (typeof $url !== 'undefined') { icinga.loader.loadUrl($url, $element).autorefresh = true; } }); var $searchField = $('#menu input.search', $target); // Remember initial search field value if any if ($searchField.length && $searchField.val().length) { self.searchValue = $searchField.val(); } }, /** * Global default event handlers */ applyGlobalDefaults: function () { // Apply element-specific behavior whenever the layout is rendered // Note: It is important that this is the first handler for this event! $(document).on('rendered', { self: this }, this.applyHandlers); $.each(self.icinga.behaviors, function (name, behavior) { behavior.bind($(document)); }); // We catch resize events $(window).on('resize', { self: this.icinga.ui }, this.icinga.ui.onWindowResize); // Trigger 'rendered' event also on page loads $(window).on('load', { self: this }, this.onLoad); // Destroy Icinga, clean up and interrupt pending requests on unload $( window ).on('unload', { self: this }, this.onUnload); $( window ).on('beforeunload', { self: this }, this.onUnload); // We catch scroll events in our containers $('.container').on('scroll', { self: this }, this.icinga.events.onContainerScroll); // Remove notifications on click $(document).on('click', '#notifications li', function () { $(this).remove(); }); // We want to catch each link click $(document).on('click', 'a', { self: this }, this.linkClicked); $(document).on('click', 'tr[href]', { self: this }, this.linkClicked); $(document).on('click', 'input[type="submit"], button[type="submit"]', this.rememberSubmitButton); // We catch all form submit events $(document).on('submit', 'form', { self: this }, this.submitForm); // We support an 'autosubmit' class on dropdown form elements $(document).on('change', 'form select.autosubmit', { self: this }, this.autoSubmitForm); $(document).on('change', 'form input.autosubmit', { self: this }, this.autoSubmitForm); // Automatically check a radio button once a specific input is focused $(document).on('focus', 'form select[data-related-radiobtn]', { self: this }, this.autoCheckRadioButton); $(document).on('focus', 'form input[data-related-radiobtn]', { self: this }, this.autoCheckRadioButton); $(document).on('keyup', '#menu input.search', {self: this}, this.autoSubmitSearch); $(document).on('click', '.tree .handle', { self: this }, this.treeNodeToggle); // TBD: a global autocompletion handler // $(document).on('keyup', 'form.auto input', this.formChangeDelayed); // $(document).on('change', 'form.auto input', this.formChanged); // $(document).on('change', 'form.auto select', this.submitForm); }, treeNodeToggle: function () { var $parent = $(this).closest('li'); if ($parent.hasClass('collapsed')) { $('li', $parent).addClass('collapsed'); $parent.removeClass('collapsed'); } else { $parent.addClass('collapsed'); } }, onLoad: function (event) { $('body').removeClass('loading'); //$('.container').trigger('rendered'); }, onUnload: function (event) { var icinga = event.data.self.icinga; icinga.logger.info('Unloading Icinga'); icinga.destroy(); }, /** * A scroll event happened in one of our containers */ onContainerScroll: function (event) { // Ugly. And PLEASE, not so often icinga.ui.fixControls(); }, autoCheckRadioButton: function (event) { var $input = $(event.currentTarget); var $radio = $('#' + $input.attr('data-related-radiobtn')); if ($radio.length) { $radio.prop('checked', true); } return true; }, autoSubmitSearch: function(event) { var self = event.data.self; if ($('#menu input.search').val() === self.searchValue) { return; } self.searchValue = $('#menu input.search').val(); return self.autoSubmitForm(event); }, rememberSubmitButton: function(e) { var $button = $(this); var $form = $button.closest('form'); $form.data('submitButton', $button); }, autoSubmitForm: function (event) { return event.data.self.submitForm(event, true); }, /** * */ submitForm: function (event, autosubmit) { var self = event.data.self; var icinga = self.icinga; // .closest is not required unless subelements to trigger this var $form = $(event.currentTarget).closest('form'); var url = $form.attr('action'); var method = $form.attr('method'); var encoding = $form.attr('enctype'); var $button = $('input[type=submit]:focus', $form).add('button[type=submit]:focus', $form); var progressTimer; var $target; var data; var $rememberedSubmittButton = $form.data('submitButton'); if (typeof $rememberedSubmittButton != 'undefined') { if ($form.has($rememberedSubmittButton)) { $button = $rememberedSubmittButton; } $form.removeData('submitButton'); } if ($button.length === 0) { var $el; if (typeof event.originalEvent !== 'undefined' && typeof event.originalEvent.explicitOriginalTarget === 'object') { // Firefox $el = $(event.originalEvent.explicitOriginalTarget); icinga.logger.debug('events/submitForm: Button is event.originalEvent.explicitOriginalTarget'); } else { $el = $(event.currentTarget); icinga.logger.debug('events/submitForm: Button is event.currentTarget'); } if ($el && ($el.is('input[type=submit]') || $el.is('button[type=submit]'))) { $button = $el; } else { icinga.logger.debug( 'events/submitForm: Can not determine submit button, using the first one in form' ); } } if (typeof method === 'undefined') { method = 'POST'; } else { method = method.toUpperCase(); } if (typeof encoding === 'undefined') { encoding = 'application/x-www-form-urlencoded'; } if (typeof autosubmit === 'undefined') { autosubmit = false; } if ($button.length === 0) { $button = $('input[type=submit]', $form).add('button[type=submit]', $form).first(); } if ($button.length) { // Activate spinner if ($button.hasClass('spinner')) { $button.addClass('active'); } $target = self.getLinkTargetFor($button); } else { $target = self.getLinkTargetFor($form); } if (! url) { // Use the URL of the target container if the form's action is not set url = $target.closest('.container').data('icinga-url'); } icinga.logger.debug('Submitting form: ' + method + ' ' + url, method); if (method === 'GET') { var dataObj = $form.serializeObject(); if (! autosubmit) { if ($button.length && $button.attr('name') !== 'undefined') { dataObj[$button.attr('name')] = $button.attr('value'); } } url = icinga.utils.addUrlParams(url, dataObj); } else { if (encoding === 'multipart/form-data') { if (typeof window.FormData === 'undefined') { icinga.loader.submitFormToIframe($form, url, $target); // Disable all form controls to prevent resubmission as early as possible. // (This relies on native form submission, so using setTimeout is the only possible solution) setTimeout(function () { if ($target.attr('id') == $form.closest('.container').attr('id')) { $form.find(':input:not(:disabled)').prop('disabled', true); } }, 0); if (autosubmit) { if ($button.length) { // We're autosubmitting the form so the button has not been clicked, however, // to be really safe, we're disabling the button explicitly, just in case.. $button.prop('disabled', true); } $form[0].submit(); // This should actually not trigger the onSubmit event, let's hope that this is true for all browsers.. event.stopPropagation(); event.preventDefault(); return false; } else { return true; } } data = new window.FormData($form[0]); } else { data = $form.serializeArray(); } if (! autosubmit) { if ($button.length && $button.attr('name') !== 'undefined') { if (encoding === 'multipart/form-data') { data.append($button.attr('name'), $button.attr('value')); } else { data.push({ name: $button.attr('name'), value: $button.attr('value') }); } } } } // Disable all form controls to prevent resubmission except for our search input // Note that disabled form inputs will not be enabled via JavaScript again if ($target.attr('id') == $form.closest('.container').attr('id')) { $form.find(':input:not(#search):not(:disabled)').prop('disabled', true); } // Show a spinner depending on how the form is being submitted if (autosubmit && typeof $el !== 'undefined' && $el.next().hasClass('spinner')) { $el.next().addClass('active'); } else if ($button.length && $button.is('button') && $button.hasClass('animated')) { $button.addClass('active'); } else if ($button.length && $button.attr('data-progress-label')) { var isInput = $button.is('input'); if (isInput) { $button.prop('value', $button.attr('data-progress-label') + '...'); } else { $button.html($button.attr('data-progress-label') + '...'); } // Use a fixed width to prevent the button from wobbling $button.css('width', $button.css('width')); progressTimer = icinga.timer.register(function () { var label = isInput ? $button.prop('value') : $button.html(); var dots = label.substr(-3); // Using empty spaces here to prevent centered labels from wobbling if (dots === '...') { label = label.slice(0, -2) + ' '; } else if (dots === '.. ') { label = label.slice(0, -1) + '.'; } else if (dots === '. ') { label = label.slice(0, -2) + '. '; } if (isInput) { $button.prop('value', label); } else { $button.html(label); } }, null, 100); } else if ($button.length && $button.next().hasClass('spinner')) { $('i', $button.next()).addClass('active'); } else if ($form.attr('data-progress-element')) { var $progressElement = $('#' + $form.attr('data-progress-element')); if ($progressElement.length) { if ($progressElement.hasClass('spinner')) { $('i', $progressElement).addClass('active'); } else { $('i.spinner', $progressElement).addClass('active'); } } } var req = icinga.loader.loadUrl(url, $target, data, method); req.forceFocus = autosubmit ? $(event.currentTarget) : $button.length ? $button : null; req.progressTimer = progressTimer; event.stopPropagation(); event.preventDefault(); return false; }, handleExternalTarget: function($node) { var linkTarget = $node.attr('target'); // TODO: Let remote links pass through. Right now they only work // combined with target="_blank" or target="_self" // window.open is used as return true; didn't work reliable if (linkTarget === '_blank' || linkTarget === '_self') { window.open($node.attr('href'), linkTarget); return true; } return false; }, /** * Someone clicked a link or tr[href] */ linkClicked: function (event) { var self = event.data.self; var icinga = self.icinga; var $a = $(this); var $eventTarget = $(event.target); var href = $a.attr('href'); var linkTarget = $a.attr('target'); var $target; var formerUrl; if (href.match(/^(?:(?:mailto|javascript|data):|[a-z]+:\/\/)/)) { event.stopPropagation(); return true; } // Special checks for link clicks in action tables if (! $a.is('tr[href]') && $a.closest('table.action').length > 0) { // ignoray clicks to ANY link with special key pressed if ($a.closest('table.multiselect').length > 0 && (event.ctrlKey || event.metaKey || event.shiftKey)) { return true; } // ignore inner links matching the row URL if ($a.attr('href') === $a.closest('tr[href]').attr('href')) { return true; } } // window.open is used as return true; didn't work reliable if (linkTarget === '_blank' || linkTarget === '_self') { window.open(href, linkTarget); return false; } if (! $eventTarget.is($a)) { if ($eventTarget.is('input') || $eventTarget.is('button')) { // Ignore form elements in action rows return; } else { var $button = $('input[type=submit]:focus').add('button[type=submit]:focus'); if ($button.length > 0 && $.contains($button[0], $eventTarget[0])) { // Ignore any descendant of form elements return; } } } // ignore multiselect table row clicks if ($a.is('tr') && $a.closest('table.multiselect').length > 0) { return; } // Handle all other links as XHR requests event.stopPropagation(); event.preventDefault(); // This is an anchor only if (href.substr(0, 1) === '#' && href.length > 1 && href.substr(1, 1) !== '!' ) { icinga.ui.focusElement(href.substr(1), $a.closest('.container')); return; } // activate spinner indicator if ($a.hasClass('spinner')) { $a.addClass('active'); } // If link has hash tag... if (href.match(/#/)) { if (href === '#') { if ($a.hasClass('close-container-control')) { if (! icinga.ui.isOneColLayout()) { var $cont = $a.closest('.container').first(); if ($cont.attr('id') === 'col1') { icinga.ui.moveToLeft(); icinga.ui.layout1col(); } else { icinga.ui.layout1col(); } icinga.history.pushCurrentState(); } } return false; } $target = self.getLinkTargetFor($a); formerUrl = $target.data('icingaUrl'); if (typeof formerUrl !== 'undefined' && formerUrl.split(/#/)[0] === href.split(/#/)[0]) { icinga.ui.focusElement(href.split(/#/)[1], $target); $target.data('icingaUrl', href); if (formerUrl !== href) { icinga.history.pushCurrentState(); } return false; } } else { $target = self.getLinkTargetFor($a); } // Load link URL icinga.loader.loadUrl(href, $target); if ($a.closest('#menu').length > 0) { // Menu links should remove all but the first layout column icinga.ui.layout1col(); } return false; }, /** * Detect the link/form target for a given element (link, form, whatever) */ getLinkTargetFor: function($el) { var targetId; // If everything else fails, our target is the first column... var $target = $('#col1'); // ...but usually we will use our own container... var $container = $el.closest('.container'); if ($container.length) { $target = $container; } // You can of course override the default behaviour: if ($el.closest('[data-base-target]').length) { targetId = $el.closest('[data-base-target]').data('baseTarget'); // Simulate _next to prepare migration to dynamic column layout // YES, there are duplicate lines right now. if (targetId === '_next') { if (this.icinga.ui.hasOnlyOneColumn()) { targetId = 'col1'; $target = $('#' + targetId); } else { if ($el.closest('#col2').length) { this.icinga.ui.moveToLeft(); } targetId = 'col2'; $target = $('#' + targetId); } } else if (targetId === '_self') { $target = $el.closest('.container'); targetId = $target.attr('id'); } else if (targetId === '_main') { targetId = 'col1'; $target = $('#' + targetId); self.icinga.ui.layout1col(); } else { $target = $('#' + targetId); if (! $target.length) { self.icinga.logger.warn('Link target "#' + targetId + '" does not exist in DOM.'); } } } // Hardcoded layout switch unless columns are dynamic if ($target.attr('id') === 'col2') { this.icinga.ui.layout2col(); } return $target; }, unbindGlobalHandlers: function () { $.each(self.icinga.behaviors, function (name, behavior) { behavior.unbind($(document)); }); $(window).off('resize', this.onWindowResize); $(window).off('load', this.onLoad); $(window).off('unload', this.onUnload); $(window).off('beforeunload', this.onUnload); $(document).off('scroll', '.container', this.onContainerScroll); $(document).off('click', 'a', this.linkClicked); $(document).off('submit', 'form', this.submitForm); $(document).off('change', 'form select.autosubmit', this.submitForm); $(document).off('change', 'form input.autosubmit', this.submitForm); $(document).off('focus', 'form select[data-related-radiobtn]', this.autoCheckRadioButton); $(document).off('focus', 'form input[data-related-radiobtn]', this.autoCheckRadioButton); }, destroy: function() { // This is gonna be hard, clean up the mess this.unbindGlobalHandlers(); this.icinga = null; } }; }(Icinga, jQuery));