From ef2f332869efc71e5ec5481f497b3d6d1c69b595 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Fri, 19 Sep 2014 15:48:44 +0200 Subject: [PATCH] Do not refresh a container when form input was changed or a form element is focused Listen for changes in form elements and abort all reloads that contain a form with at least one changed form element. Do not refresh containers that contain a focused form element, except of elements with autofocus, to preserve form elements with a dropdown. Only focus autofocus elements when there is currently no other selection. refs #7146 refs #5537 fixes #7162 --- library/Icinga/Web/JavaScript.php | 3 +- public/js/icinga/behavior/form.js | 104 ++++++++++++++++++++++++++++++ public/js/icinga/events.js | 5 +- public/js/icinga/loader.js | 20 ++++-- public/js/icinga/logger.js | 2 +- 5 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 public/js/icinga/behavior/form.js diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index 6aa36d608..6224b5b82 100644 --- a/library/Icinga/Web/JavaScript.php +++ b/library/Icinga/Web/JavaScript.php @@ -26,7 +26,8 @@ class JavaScript 'js/icinga/behavior/tooltip.js', 'js/icinga/behavior/sparkline.js', 'js/icinga/behavior/tristate.js', - 'js/icinga/behavior/navigation.js' + 'js/icinga/behavior/navigation.js', + 'js/icinga/behavior/form.js' ); protected static $vendorFiles = array( diff --git a/public/js/icinga/behavior/form.js b/public/js/icinga/behavior/form.js new file mode 100644 index 000000000..d289183d6 --- /dev/null +++ b/public/js/icinga/behavior/form.js @@ -0,0 +1,104 @@ +// {{{ICINGA_LICENSE_HEADER}}} +// {{{ICINGA_LICENSE_HEADER}}} + +/** + * Controls behavior of form elements, depending reload and + */ +(function(Icinga, $) { + + "use strict"; + + Icinga.Behaviors = Icinga.Behaviors || {}; + + var Form = function (icinga) { + Icinga.EventListener.call(this, icinga); + this.on('keyup change', 'form input', this.onChange, this); + + // store the modification state of all input fields + this.inputs = {}; + }; + Form.prototype = new Icinga.EventListener(); + + /** + * @param evt + */ + Form.prototype.onChange = function (evt) { + var el = evt.target; + var form = evt.data.self.uniqueFormName($(el).closest('form')[0] || {}); + evt.data.self.inputs[form] = evt.data.self.inputs[form] || {}; + if (el.value !== '') { + evt.data.self.inputs[form][el.name] = true; + } else { + evt.data.self.inputs[form][el.name] = false; + } + }; + + /** + * Try to generate an unique form name using the action + * and the name of the given form element + * + * @param form {HTMLFormElement} The + * @returns {String} The unique name + */ + Form.prototype.uniqueFormName = function(form) + { + return (form.name || 'undefined') + '.' + (form.action || 'undefined'); + }; + + /** + * Mutates the HTML before it is placed in the DOM after a reload + * + * @param content {String} The content to be rendered + * @param $container {jQuery} The target container where the html will be rendered in + * @param action {String} The action-url that caused the reload + * @param autorefresh {Boolean} Whether the rendering is due to an autoRefresh + * + * @returns {string|NULL} The content to be rendered, or NULL, when nothing should be changed + */ + Form.prototype.renderHook = function(content, $container, action, autorefresh) { + var origFocus = document.activeElement; + var containerId = $container.attr('id'); + var icinga = this.icinga; + var self = this.icinga.behaviors.form; + var changed = false; + $container.find('form').each(function () { + var form = self.uniqueFormName(this); + if (autorefresh) { + // check if an element in this container was changed + $(this).find('input').each(function () { + var name = this.name; + if (self.inputs[form] && self.inputs[form][name]) { + icinga.logger.debug( + 'form input: ' + form + '.' + name + ' was changed and aborts reload...' + ); + changed = true; + } + }); + } else { + // user-triggered reload, forget all changes to forms in this container + self.inputs[form] = null; + } + }); + if (changed) { + return null; + } + if ( + // is the focus among the elements to be replaced? + $container.has(origFocus).length && + // is an autorefresh + autorefresh && + + // and has focus + $(origFocus).length && + !$(origFocus).hasClass('autofocus') && + $(origFocus).closest('form').length + ) { + icinga.logger.debug('Not changing content for ' + containerId + ' form has focus'); + return null; + } + return content; + }; + + Icinga.Behaviors.Form = Form; + +}) (Icinga, jQuery); diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index f226836cc..9c889dfa8 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -75,8 +75,9 @@ } }); - $('input.autofocus', el).focus(); - + if (document.activeElement === document.body) { + $('input.autofocus', el).focus(); + } var searchField = $('#menu input.search', el); // Remember initial search field value if any if (searchField.length && searchField.val().length) { diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index 9b6c2bd00..9c3feab6e 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -661,6 +661,7 @@ // Container update happens here var scrollPos = false; var self = this; + var origFocus = document.activeElement; var containerId = $container.attr('id'); if (typeof containerId !== 'undefined') { if (autorefresh) { @@ -670,13 +671,18 @@ } } - var origFocus = document.activeElement; - if ( - // Do not reload menu when search field has content - (containerId === 'menu' && $(origFocus).length && $(origFocus).val().length) - // TODO: remove once #7146 is solved - || (containerId !== 'menu' && typeof containerId !== 'undefined' && autorefresh && origFocus && $(origFocus).closest('form').length && $container.has($(origFocus)) && $(origFocus).closest('#' + containerId).length && ! $(origFocus).hasClass('autosubmit'))) { - this.icinga.logger.debug('Not changing content for ', containerId, ' form has focus'); + var discard = false; + $.each(self.icinga.behaviors, function(name, behavior) { + if (behavior.renderHook) { + var changed = behavior.renderHook(content, $container, action, autorefresh); + if (!changed) { + discard = true; + } else { + content = changed; + } + } + }); + if (discard) { return; } diff --git a/public/js/icinga/logger.js b/public/js/icinga/logger.js index 1bdc2d718..07135e007 100644 --- a/public/js/icinga/logger.js +++ b/public/js/icinga/logger.js @@ -37,7 +37,7 @@ /** * Raise or lower current log level * - * Messages blow this threshold will be silently discarded + * Messages below this threshold will be silently discarded */ setLevel: function (level) { if ('undefined' !== typeof this.numericLevel(level)) {