From d99d50b3c4b4086d61225f3e832103cec605ef31 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Tue, 9 Sep 2014 18:45:16 +0200 Subject: [PATCH 1/8] Add stub for javascript behaviors --- library/Icinga/Web/JavaScript.php | 1 + public/js/icinga.js | 10 ++++ public/js/icinga/behavior.js | 70 ++++++++++++++++++++++++++++ public/js/icinga/behavior/tooltip.js | 66 ++++++++++++++++++++++++++ public/js/icinga/events.js | 57 +++++++--------------- public/js/icinga/ui.js | 14 ------ 6 files changed, 163 insertions(+), 55 deletions(-) create mode 100644 public/js/icinga/behavior.js create mode 100644 public/js/icinga/behavior/tooltip.js diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index cacadb600..acb9433da 100644 --- a/library/Icinga/Web/JavaScript.php +++ b/library/Icinga/Web/JavaScript.php @@ -22,6 +22,7 @@ class JavaScript 'js/icinga/history.js', 'js/icinga/module.js', 'js/icinga/timezone.js', + 'js/icinga/behavior/tooltip.js' ); protected static $vendorFiles = array( diff --git a/public/js/icinga.js b/public/js/icinga.js index 04c7cdcba..296e6f6d3 100644 --- a/public/js/icinga.js +++ b/public/js/icinga.js @@ -60,6 +60,11 @@ */ this.utils = null; + /** + * Additional site behavior + */ + this.behaviors = {}; + /** * Loaded modules */ @@ -90,6 +95,10 @@ this.loader = new Icinga.Loader(this); this.events = new Icinga.Events(this); this.history = new Icinga.History(this); + var self = this; + $.each(Icinga.Behaviors, function(name, Behavior) { + self.behaviors[name.toLowerCase()] = new Behavior(); + }); this.timezone.initialize(); this.timer.initialize(); @@ -97,6 +106,7 @@ this.history.initialize(); this.ui.initialize(); this.loader.initialize(); + this.logger.info('Icinga is ready, running on jQuery ', $().jquery); this.initialized = true; }, diff --git a/public/js/icinga/behavior.js b/public/js/icinga/behavior.js new file mode 100644 index 000000000..4e1e570f9 --- /dev/null +++ b/public/js/icinga/behavior.js @@ -0,0 +1,70 @@ +// {{{ICINGA_LICENSE_HEADER}}} +// {{{ICINGA_LICENSE_HEADER}}} + +(function(Icinga) { + + /** + * Used to define a set of functionality that can be applied + * on a subtree of the site's DOM + * + * Behaviors + * + * @constructor + */ + Icinga.Behavior = function () { + this.handler = { + apply: [], + bind: [], + unbind: [] + }; + }; + + Icinga.Behavior.prototype.on = function(evt, fn) { + this.handler[evt].push(fn); + }; + + Icinga.Behavior.prototype.off = function(evt, fn) { + this.handler[evt].remove(fn); + }; + + Icinga.Behavior.prototype.trigger = function(evt, el) { + var handler = this.handler[evt]; + for (var i = 0; i < handler.length; i++) { + if (typeof handler[i] === 'function') { + handler[i](el); + } + } + }; + + Icinga.Behavior.prototype.onApply = function(fn) { + this.on('apply', fn); + }; + + Icinga.Behavior.prototype.onBind = function(fn) { + this.on('bind', fn); + }; + + Icinga.Behavior.prototype.onUnbind = function(fn) { + this.on('unbind', fn); + }; + + Icinga.Behavior.prototype.apply = function(el) { + this.trigger ('apply', el); + }; + + Icinga.Behavior.prototype.bind = function(el) { + this.trigger ('bind', el); + }; + + Icinga.Behavior.prototype.unbind = function(el) { + this.trigger ('apply', el); + }; + + Icinga.Behavior.prototype.off = function() { + this.handler = { + apply: [], + bind: [], + unbind: [] + }; + }; +}) (Icinga); \ No newline at end of file diff --git a/public/js/icinga/behavior/tooltip.js b/public/js/icinga/behavior/tooltip.js new file mode 100644 index 000000000..1feb6f54f --- /dev/null +++ b/public/js/icinga/behavior/tooltip.js @@ -0,0 +1,66 @@ +// {{{ICINGA_LICENSE_HEADER}}} +// {{{ICINGA_LICENSE_HEADER}}} + +(function(Icinga, $) { + + "use strict"; + + Icinga.Behaviors = Icinga.Behaviors || {}; + + var Tooltip = function () { + this.mouseX = 0; + this.mouseY = 0; + }; + + Tooltip.prototype.apply = function(el) { + var self = this; + + $('[title]').each(function () { + var $el = $(this); + $el.attr('title', $el.data('title-rich') || $el.attr('title')); + }); + $('svg rect.chart-data[title]', el).tipsy({ gravity: 'se', html: true }); + $('.historycolorgrid a[title]', el).tipsy({ gravity: 's', offset: 2 }); + $('img.icon[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, offset: 2 }); + $('[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, delayIn: 500 }); + + // migrate or remove all orphaned tooltips + $('.tipsy').each(function () { + var arrow = $('.tipsy-arrow', this)[0]; + if (!Icinga.utils.elementsOverlap(arrow, $('#main')[0])) { + $(this).remove(); + return; + } + if (!Icinga.utils.elementsOverlap(arrow, el)) { + return; + } + var title = $(this).find('.tipsy-inner').html(); + var atMouse = document.elementFromPoint(self.mouseX, self.mouseY); + var nearestTip = $(atMouse).closest('[original-title="' + title + '"]')[0]; + if (nearestTip) { + var tipsy = $.data(nearestTip, 'tipsy'); + tipsy.$tip = $(this); + $.data(this, 'tipsy-pointee', nearestTip); + } else { + // doesn't match delete + $(this).remove(); + } + }); + }; + + Tooltip.prototype.bind = function() { + var self = this; + $(document).on('mousemove', function (event) { + self.mouseX = event.pageX; + self.mouseY = event.pageY; + }); + }; + + Tooltip.prototype.unbind = function() { + $(document).off('mousemove'); + }; + + // Export + Icinga.Behaviors.Tooltip = Tooltip; + +}) (Icinga, jQuery); diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index 69a08ef56..49a2da42e 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -12,8 +12,6 @@ var activeMenuId; - var mouseX, mouseY; - Icinga.Events = function (icinga) { this.icinga = icinga; @@ -34,7 +32,22 @@ initialize: function () { this.applyGlobalDefaults(); this.applyHandlers($('#layout')); - this.icinga.ui.prepareContainers(); + var self = this; + + // define global site behavior + $.each(self.icinga.behaviors, function (name, behavior) { + behavior.bind(); + }); + + // prepare container html + $('.container').each(function(idx, el) { + // apply event handlers + icinga.events.applyHandlers($(el)); + icinga.ui.initializeControls($(el)); + $.each(self.icinga.behaviors, function (name, behavior) { + behavior.apply(el); + }); + }); }, // TODO: What's this? @@ -116,39 +129,6 @@ this.searchValue = searchField.val(); } - $('[title]').each(function () { - var $el = $(this); - $el.attr('title', $el.data('title-rich') || $el.attr('title')); - }); - $('svg rect.chart-data[title]', el).tipsy({ gravity: 'se', html: true }); - $('.historycolorgrid a[title]', el).tipsy({ gravity: 's', offset: 2 }); - $('img.icon[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, offset: 2 }); - $('[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, delayIn: 500 }); - - // migrate or remove all orphaned tooltips - $('.tipsy').each(function () { - var arrow = $('.tipsy-arrow', this)[0]; - if (!icinga.utils.elementsOverlap(arrow, $('#main')[0])) { - $(this).remove(); - return; - } - if (!icinga.utils.elementsOverlap(arrow, el)) { - return; - } - var title = $(this).find('.tipsy-inner').html(); - var atMouse = document.elementFromPoint(mouseX, mouseY); - var nearestTip = $(atMouse) - .closest('[original-title="' + title + '"]')[0]; - if (nearestTip) { - var tipsy = $.data(nearestTip, 'tipsy'); - tipsy.$tip = $(this); - $.data(this, 'tipsy-pointee', nearestTip); - } else { - // doesn't match delete - $(this).remove(); - } - }); - // restore menu state if (activeMenuId) { $('[role="navigation"] li.active', el).removeClass('active'); @@ -220,10 +200,6 @@ // $(document).on('change', 'form.auto input', this.formChanged); // $(document).on('change', 'form.auto select', this.submitForm); - $(document).on('mousemove', function (event) { - mouseX = event.pageX; - mouseY = event.pageY; - }); }, menuTitleHovered: function (event) { @@ -713,7 +689,6 @@ $(document).off('mouseenter', 'li.dropdown', this.dropdownHover); $(document).off('mouseleave', 'li.dropdown', this.dropdownLeave); $(document).off('click', 'div.tristate .tristate-dummy', this.clickTriState); - $(document).off('mousemove'); }, destroy: function() { diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js index f6ae29b35..ef5e43f70 100644 --- a/public/js/icinga/ui.js +++ b/public/js/icinga/ui.js @@ -282,20 +282,6 @@ return $('#main > .container').length; }, - prepareContainers: function () { - var icinga = this.icinga; - $('.container').each(function(idx, el) { - icinga.events.applyHandlers($(el)); - icinga.ui.initializeControls($(el)); - }); - /* - $('#icinga-main').attr( - 'icingaurl', - window.location.pathname + window.location.search - ); - */ - }, - /** * Add the given table-row to the selection of the closest * table and deselect all other rows of the closest table. From 5d2e849491e9d28ceeafca92f956c50e5b841c83 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 10 Sep 2014 10:44:22 +0200 Subject: [PATCH 2/8] Apply behaviors correctly on startup --- public/js/icinga.js | 2 +- public/js/icinga/events.js | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/public/js/icinga.js b/public/js/icinga.js index 296e6f6d3..c883a6bba 100644 --- a/public/js/icinga.js +++ b/public/js/icinga.js @@ -97,7 +97,7 @@ this.history = new Icinga.History(this); var self = this; $.each(Icinga.Behaviors, function(name, Behavior) { - self.behaviors[name.toLowerCase()] = new Behavior(); + self.behaviors[name.toLowerCase()] = new Behavior(self); }); this.timezone.initialize(); diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index 49a2da42e..6ecdd61ba 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -32,26 +32,17 @@ initialize: function () { this.applyGlobalDefaults(); this.applyHandlers($('#layout')); - var self = this; - - // define global site behavior - $.each(self.icinga.behaviors, function (name, behavior) { - behavior.bind(); - }); - - // prepare container html $('.container').each(function(idx, el) { - // apply event handlers icinga.events.applyHandlers($(el)); icinga.ui.initializeControls($(el)); - $.each(self.icinga.behaviors, function (name, behavior) { - behavior.apply(el); - }); }); }, // TODO: What's this? applyHandlers: function (el) { + $.each(this.icinga.behaviors, function (name, behavior) { + behavior.apply(el); + }); var icinga = this.icinga; @@ -152,6 +143,10 @@ * Global default event handlers */ applyGlobalDefaults: function () { + $.each(self.icinga.behaviors, function (name, behavior) { + behavior.bind(); + }); + // We catch resize events $(window).on('resize', { self: this.icinga.ui }, this.icinga.ui.onWindowResize); @@ -673,6 +668,9 @@ */ unbindGlobalHandlers: function () { + $.each(self.icinga.behaviors, function (name, behavior) { + behavior.unbind(); + }); $(window).off('resize', this.onWindowResize); $(window).off('load', this.onLoad); $(window).off('unload', this.onUnload); From f58da73e2d6165c32568f8235bdce9be99048190 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 10 Sep 2014 10:45:34 +0200 Subject: [PATCH 3/8] Move javascript behaviors into separate classes --- library/Icinga/Web/JavaScript.php | 5 +- public/js/icinga/behavior/navigation.js | 75 ++++++++++++++++ public/js/icinga/behavior/sparkline.js | 58 +++++++++++++ public/js/icinga/behavior/tooltip.js | 9 +- public/js/icinga/behavior/tristate.js | 62 +++++++++++++ public/js/icinga/events.js | 110 ------------------------ 6 files changed, 204 insertions(+), 115 deletions(-) create mode 100644 public/js/icinga/behavior/navigation.js create mode 100644 public/js/icinga/behavior/sparkline.js create mode 100644 public/js/icinga/behavior/tristate.js diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index acb9433da..01e6a55eb 100644 --- a/library/Icinga/Web/JavaScript.php +++ b/library/Icinga/Web/JavaScript.php @@ -22,7 +22,10 @@ class JavaScript 'js/icinga/history.js', 'js/icinga/module.js', 'js/icinga/timezone.js', - 'js/icinga/behavior/tooltip.js' + 'js/icinga/behavior/tooltip.js', + 'js/icinga/behavior/sparkline.js', + 'js/icinga/behavior/tristate.js', + 'js/icinga/behavior/navigation.js' ); protected static $vendorFiles = array( diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js new file mode 100644 index 000000000..d3e34733c --- /dev/null +++ b/public/js/icinga/behavior/navigation.js @@ -0,0 +1,75 @@ +// {{{ICINGA_LICENSE_HEADER}}} +// {{{ICINGA_LICENSE_HEADER}}} + +(function(Icinga, $) { + + "use strict"; + + var activeMenuId; + + Icinga.Behaviors = Icinga.Behaviors || {}; + + var Navigation = function (icinga) { + this.icinga = icinga; + + }; + + Navigation.prototype.apply = function(el) { + // restore menu state + if (activeMenuId) { + $('[role="navigation"] li.active', el).removeClass('active'); + + var $selectedMenu = $('#' + activeMenuId, el); + var $outerMenu = $selectedMenu.parent().closest('li'); + if ($outerMenu.size()) { + $selectedMenu = $outerMenu; + } + $selectedMenu.addClass('active'); + } else { + // store menu state + var $menus = $('[role="navigation"] li.active', el); + if ($menus.size()) { + activeMenuId = $menus[0].id; + } + } + }; + + Navigation.prototype.bind = function() { + $(document).on('click', 'a', { self: this }, this.linkClicked); + $(document).on('click', 'tr[href]', { self: this }, this.linkClicked); + }; + + Navigation.prototype.unbind = function() { + $(document).off('click', 'a', this.linkClicked); + $(document).off('click', 'tr[href]', this.linkClicked); + }; + + Navigation.prototype.linkClicked = function(event) { + var $a = $(this); + var href = $a.attr('href'); + var isMenuLink = $a.closest('#menu').length > 0; + var $li; + var icinga = event.data.self.icinga; + + if (href.match(/#/)) { + $li = $a.closest('li'); + if (isMenuLink) { + activeMenuId = $($li).attr('id'); + } + } else { + if (isMenuLink) { + activeMenuId = $(event.target).closest('li').attr('id'); + } + } + if (isMenuLink) { + var $menu = $('#menu'); + // update target url of the menu container to the clicked link + var menuDataUrl = icinga.utils.parseUrl($menu.data('icinga-url')); + menuDataUrl = icinga.utils.addUrlParams(menuDataUrl.path, { url: href }); + $menu.data('icinga-url', menuDataUrl); + } + }; + + Icinga.Behaviors.Navigation = Navigation; + +}) (Icinga, jQuery); diff --git a/public/js/icinga/behavior/sparkline.js b/public/js/icinga/behavior/sparkline.js new file mode 100644 index 000000000..4c29f47cc --- /dev/null +++ b/public/js/icinga/behavior/sparkline.js @@ -0,0 +1,58 @@ +// {{{ICINGA_LICENSE_HEADER}}} +// {{{ICINGA_LICENSE_HEADER}}} + +(function(Icinga, $) { + + "use strict"; + + Icinga.Behaviors = Icinga.Behaviors || {}; + + var Sparkline = function (icinga) { + this.icinga = icinga; + }; + + Sparkline.prototype.apply = function(el) { + var self = this, icinga = this.icinga; + + $('span.sparkline', el).each(function(i, element) { + // read custom options + var $spark = $(element); + var labels = $spark.attr('labels').split('|'); + var formatted = $spark.attr('formatted').split('|'); + var tooltipChartTitle = $spark.attr('sparkSparklineChartTitle') || ''; + var format = $spark.attr('tooltipformat'); + var hideEmpty = $spark.attr('hideEmptyLabel') === 'true'; + $spark.sparkline( + 'html', + { + enableTagOptions: true, + tooltipFormatter: function (sparkline, options, fields) { + var out = format; + if (hideEmpty && fields.offset === 3) { + return ''; + } + var replace = { + title: tooltipChartTitle, + label: labels[fields.offset] ? labels[fields.offset] : fields.offset, + formatted: formatted[fields.offset] ? formatted[fields.offset] : '', + value: fields.value, + percent: Math.round(fields.percent * 100) / 100 + }; + $.each(replace, function(key, value) { + out = out.replace('{{' + key + '}}', value); + }); + return out; + } + }); + }); + }; + + Sparkline.prototype.bind = function() { + }; + + Sparkline.prototype.unbind = function() { + }; + + Icinga.Behaviors.Sparkline = Sparkline; + +}) (Icinga, jQuery); diff --git a/public/js/icinga/behavior/tooltip.js b/public/js/icinga/behavior/tooltip.js index 1feb6f54f..9cf207d7f 100644 --- a/public/js/icinga/behavior/tooltip.js +++ b/public/js/icinga/behavior/tooltip.js @@ -7,13 +7,14 @@ Icinga.Behaviors = Icinga.Behaviors || {}; - var Tooltip = function () { + var Tooltip = function (icinga) { + this.icinga = icinga; this.mouseX = 0; this.mouseY = 0; }; Tooltip.prototype.apply = function(el) { - var self = this; + var self = this, icinga = this.icinga; $('[title]').each(function () { var $el = $(this); @@ -27,11 +28,11 @@ // migrate or remove all orphaned tooltips $('.tipsy').each(function () { var arrow = $('.tipsy-arrow', this)[0]; - if (!Icinga.utils.elementsOverlap(arrow, $('#main')[0])) { + if (!icinga.utils.elementsOverlap(arrow, $('#main')[0])) { $(this).remove(); return; } - if (!Icinga.utils.elementsOverlap(arrow, el)) { + if (!icinga.utils.elementsOverlap(arrow, el)) { return; } var title = $(this).find('.tipsy-inner').html(); diff --git a/public/js/icinga/behavior/tristate.js b/public/js/icinga/behavior/tristate.js new file mode 100644 index 000000000..c828d6cd7 --- /dev/null +++ b/public/js/icinga/behavior/tristate.js @@ -0,0 +1,62 @@ +// {{{ICINGA_LICENSE_HEADER}}} +// {{{ICINGA_LICENSE_HEADER}}} + +(function(Icinga, $) { + + "use strict"; + + Icinga.Behaviors = Icinga.Behaviors || {}; + + var Tristate = function (icinga) { + this.icinga = icinga; + }; + + Tristate.prototype.apply = function(el) { + var self = this, icinga = this.icinga; + }; + + Tristate.prototype.bind = function() { + // Toggle all triStateButtons + $(document).on('click', 'div.tristate .tristate-dummy', { self: this }, this.clickTriState); + }; + + Tristate.prototype.unbind = function() { + $(document).off('click', 'div.tristate .tristate-dummy', this.clickTriState); + }; + + Tristate.prototype.clickTriState = function (event) { + var self = event.data.self; + var $tristate = $(this); + var triState = parseInt($tristate.data('icinga-tristate'), 10); + + // load current values + var old = $tristate.data('icinga-old').toString(); + var value = $tristate.parent().find('input:radio:checked').first().prop('checked', false).val(); + + // calculate the new value + if (triState) { + // 1 => 0 + // 0 => unchanged + // unchanged => 1 + value = value === '1' ? '0' : (value === '0' ? 'unchanged' : '1'); + } else { + // 1 => 0 + // 0 => 1 + value = value === '1' ? '0' : '1'; + } + + // update form value + $tristate.parent().find('input:radio[value="' + value + '"]').prop('checked', true); + // update dummy + + if (value !== old) { + $tristate.parent().find('b.tristate-changed').css('visibility', 'visible'); + } else { + $tristate.parent().find('b.tristate-changed').css('visibility', 'hidden'); + } + self.icinga.ui.setTriState(value.toString(), $tristate); + }; + + Icinga.Behaviors.Tristate = Tristate; + +}) (Icinga, jQuery); diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index 6ecdd61ba..2b70cfa5b 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -10,8 +10,6 @@ 'use strict'; - var activeMenuId; - Icinga.Events = function (icinga) { this.icinga = icinga; @@ -82,61 +80,11 @@ $('input.autofocus', el).focus(); - // replace all sparklines - $('span.sparkline', el).each(function(i, element) { - // read custom options - var $spark = $(element); - var labels = $spark.attr('labels').split('|'); - var formatted = $spark.attr('formatted').split('|'); - var tooltipChartTitle = $spark.attr('sparkTooltipChartTitle') || ''; - var format = $spark.attr('tooltipformat'); - var hideEmpty = $spark.attr('hideEmptyLabel') === 'true'; - $spark.sparkline( - 'html', - { - enableTagOptions: true, - tooltipFormatter: function (sparkline, options, fields) { - var out = format; - if (hideEmpty && fields.offset === 3) { - return ''; - } - var replace = { - title: tooltipChartTitle, - label: labels[fields.offset] ? labels[fields.offset] : fields.offset, - formatted: formatted[fields.offset] ? formatted[fields.offset] : '', - value: fields.value, - percent: Math.round(fields.percent * 100) / 100 - }; - $.each(replace, function(key, value) { - out = out.replace('{{' + key + '}}', value); - }); - return out; - } - }); - }); var searchField = $('#menu input.search', el); // Remember initial search field value if any if (searchField.length && searchField.val().length) { this.searchValue = searchField.val(); } - - // restore menu state - if (activeMenuId) { - $('[role="navigation"] li.active', el).removeClass('active'); - - var $selectedMenu = $('#' + activeMenuId, el); - var $outerMenu = $selectedMenu.parent().closest('li'); - if ($outerMenu.size()) { - $selectedMenu = $outerMenu; - } - $selectedMenu.addClass('active'); - } else { - // store menu state - var $menus = $('[role="navigation"] li.active', el); - if ($menus.size()) { - activeMenuId = $menus[0].id; - } - } }, /** @@ -178,8 +126,6 @@ $(document).on('keyup', '#menu input.search', {self: this}, this.autoSubmitSearch); - $(document).on('mouseenter', '.historycolorgrid td', this.historycolorgridHover); - $(document).on('mouseleave', '.historycolorgrid td', this.historycolorgidUnhover); $(document).on('mouseenter', 'li.dropdown', this.dropdownHover); $(document).on('mouseleave', 'li.dropdown', {self: this}, this.dropdownLeave); @@ -187,14 +133,11 @@ $(document).on('mouseleave', '#sidebar', { self: this }, this.leaveSidebar); $(document).on('click', '.tree .handle', { self: this }, this.treeNodeToggle); - // Toggle all triStateButtons - $(document).on('click', 'div.tristate .tristate-dummy', { self: this }, this.clickTriState); // 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); - }, menuTitleHovered: function (event) { @@ -301,14 +244,6 @@ icinga.ui.fixControls(); }, - historycolorgridHover: function () { - $(this).addClass('hover'); - }, - - historycolorgidUnhover: function() { - $(this).removeClass('hover'); - }, - autoSubmitSearch: function(event) { var self = event.data.self; if ($('#menu input.search').val() === self.searchValue) { @@ -322,39 +257,6 @@ return event.data.self.submitForm(event, true); }, - clickTriState: function (event) { - var self = event.data.self; - var $tristate = $(this); - var triState = parseInt($tristate.data('icinga-tristate'), 10); - - // load current values - var old = $tristate.data('icinga-old').toString(); - var value = $tristate.parent().find('input:radio:checked').first().prop('checked', false).val(); - - // calculate the new value - if (triState) { - // 1 => 0 - // 0 => unchanged - // unchanged => 1 - value = value === '1' ? '0' : (value === '0' ? 'unchanged' : '1'); - } else { - // 1 => 0 - // 0 => 1 - value = value === '1' ? '0' : '1'; - } - - // update form value - $tristate.parent().find('input:radio[value="' + value + '"]').prop('checked', true); - // update dummy - - if (value !== old) { - $tristate.parent().find('b.tristate-changed').css('visibility', 'visible'); - } else { - $tristate.parent().find('b.tristate-changed').css('visibility', 'hidden'); - } - self.icinga.ui.setTriState(value.toString(), $tristate); - }, - /** * */ @@ -557,7 +459,6 @@ $li = $a.closest('li'); $('#menu .active').removeClass('active'); $li.addClass('active'); - activeMenuId = $($li).attr('id'); if ($li.hasClass('hover')) { $li.removeClass('hover'); } @@ -583,9 +484,6 @@ return false; } } else { - if (isMenuLink) { - activeMenuId = $(event.target).closest('li').attr('id'); - } $target = self.getLinkTargetFor($a); } @@ -593,11 +491,6 @@ icinga.loader.loadUrl(href, $target); if (isMenuLink) { - // update target url of the menu container to the clicked link - var menuDataUrl = icinga.utils.parseUrl($('#menu').data('icinga-url')); - menuDataUrl = icinga.utils.addUrlParams(menuDataUrl.path, { url: href }); - $('#menu').data('icinga-url', menuDataUrl); - // Menu links should remove all but the first layout column icinga.ui.layout1col(); } @@ -682,11 +575,8 @@ $(document).off('submit', 'form', this.submitForm); $(document).off('click', 'button', this.submitForm); $(document).off('change', 'form select.autosubmit', this.submitForm); - $(document).off('mouseenter', '.historycolorgrid td', this.historycolorgridHover); - $(document).off('mouseleave', '.historycolorgrid td', this.historycolorgidUnhover); $(document).off('mouseenter', 'li.dropdown', this.dropdownHover); $(document).off('mouseleave', 'li.dropdown', this.dropdownLeave); - $(document).off('click', 'div.tristate .tristate-dummy', this.clickTriState); }, destroy: function() { From c0908e32ba4fd7fd02bdcc00d1f9870e8540ad74 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 10 Sep 2014 10:47:45 +0200 Subject: [PATCH 4/8] Remove unused behavior base-class --- public/js/icinga/behavior.js | 70 ------------------------------------ 1 file changed, 70 deletions(-) delete mode 100644 public/js/icinga/behavior.js diff --git a/public/js/icinga/behavior.js b/public/js/icinga/behavior.js deleted file mode 100644 index 4e1e570f9..000000000 --- a/public/js/icinga/behavior.js +++ /dev/null @@ -1,70 +0,0 @@ -// {{{ICINGA_LICENSE_HEADER}}} -// {{{ICINGA_LICENSE_HEADER}}} - -(function(Icinga) { - - /** - * Used to define a set of functionality that can be applied - * on a subtree of the site's DOM - * - * Behaviors - * - * @constructor - */ - Icinga.Behavior = function () { - this.handler = { - apply: [], - bind: [], - unbind: [] - }; - }; - - Icinga.Behavior.prototype.on = function(evt, fn) { - this.handler[evt].push(fn); - }; - - Icinga.Behavior.prototype.off = function(evt, fn) { - this.handler[evt].remove(fn); - }; - - Icinga.Behavior.prototype.trigger = function(evt, el) { - var handler = this.handler[evt]; - for (var i = 0; i < handler.length; i++) { - if (typeof handler[i] === 'function') { - handler[i](el); - } - } - }; - - Icinga.Behavior.prototype.onApply = function(fn) { - this.on('apply', fn); - }; - - Icinga.Behavior.prototype.onBind = function(fn) { - this.on('bind', fn); - }; - - Icinga.Behavior.prototype.onUnbind = function(fn) { - this.on('unbind', fn); - }; - - Icinga.Behavior.prototype.apply = function(el) { - this.trigger ('apply', el); - }; - - Icinga.Behavior.prototype.bind = function(el) { - this.trigger ('bind', el); - }; - - Icinga.Behavior.prototype.unbind = function(el) { - this.trigger ('apply', el); - }; - - Icinga.Behavior.prototype.off = function() { - this.handler = { - apply: [], - bind: [], - unbind: [] - }; - }; -}) (Icinga); \ No newline at end of file From cb9c9c78d8bb1ac3a47ce76ab4c9004ce78eeeba Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 10 Sep 2014 14:21:15 +0200 Subject: [PATCH 5/8] Move navigation events into behavior --- public/js/icinga/behavior/navigation.js | 105 ++++++++++++++++++++++- public/js/icinga/events.js | 107 +----------------------- 2 files changed, 104 insertions(+), 108 deletions(-) diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js index d3e34733c..68b7c2e92 100644 --- a/public/js/icinga/behavior/navigation.js +++ b/public/js/icinga/behavior/navigation.js @@ -11,7 +11,6 @@ var Navigation = function (icinga) { this.icinga = icinga; - }; Navigation.prototype.apply = function(el) { @@ -37,11 +36,18 @@ Navigation.prototype.bind = function() { $(document).on('click', 'a', { self: this }, this.linkClicked); $(document).on('click', 'tr[href]', { self: this }, this.linkClicked); + $(document).on('mouseenter', 'li.dropdown', this.dropdownHover); + $(document).on('mouseleave', 'li.dropdown', {self: this}, this.dropdownLeave); + $(document).on('mouseenter', '#menu > ul > li', { self: this }, this.menuTitleHovered); + $(document).on('mouseleave', '#sidebar', { self: this }, this.leaveSidebar); }; Navigation.prototype.unbind = function() { $(document).off('click', 'a', this.linkClicked); $(document).off('click', 'tr[href]', this.linkClicked); + $(document).off('mouseenter', 'li.dropdown', this.dropdownHover); + $(document).off('mouseleave', 'li.dropdown', this.dropdownLeave); + $(document).off('mouseenter', '#menu > ul > li', this.menuTitleHovered); }; Navigation.prototype.linkClicked = function(event) { @@ -52,10 +58,26 @@ var icinga = event.data.self.icinga; if (href.match(/#/)) { - $li = $a.closest('li'); + // ...it may be a menu section without a dedicated link. + // Switch the active menu item: if (isMenuLink) { + $li = $a.closest('li'); + $('#menu .active').removeClass('active'); + $li.addClass('active'); activeMenuId = $($li).attr('id'); + if ($li.hasClass('hover')) { + $li.removeClass('hover'); + } } + if (href === '#') { + // Allow to access dropdown menu by keyboard + if ($a.hasClass('dropdown-toggle')) { + $a.closest('li').toggleClass('hover'); + } + // Ignore link, no action + return false; + } + } else { if (isMenuLink) { activeMenuId = $(event.target).closest('li').attr('id'); @@ -70,6 +92,85 @@ } }; + Navigation.prototype.menuTitleHovered = function(event) { + var $li = $(this), + delay = 800, + self = event.data.self; + + if ($li.hasClass('active')) { + $li.siblings().removeClass('hover'); + return; + } + if ($li.children('ul').children('li').length === 0) { + return; + } + if ($('#menu').scrollTop() > 0) { + return; + } + + if ($('#layout').hasClass('hoveredmenu')) { + delay = 0; + } + + setTimeout(function () { + if (! $li.is('li:hover')) { + return; + } + if ($li.hasClass('active')) { + return; + } + + $li.siblings().each(function () { + var $sibling = $(this); + if ($sibling.is('li:hover')) { + return; + } + if ($sibling.hasClass('hover')) { + $sibling.removeClass('hover'); + } + }); + + self.hoverElement($li); + }, delay); + }; + + Navigation.prototype.leaveSidebar = function (event) { + var $sidebar = $(this), + $li = $sidebar.find('li.hover'), + self = event.data.self; + if (! $li.length) { + $('#layout').removeClass('hoveredmenu'); + return; + } + + setTimeout(function () { + if ($li.is('li:hover') || $sidebar.is('sidebar:hover') ) { + return; + } + $li.removeClass('hover'); + $('#layout').removeClass('hoveredmenu'); + }, 500); + }; + + Navigation.prototype.hoverElement = function ($li) { + $('#layout').addClass('hoveredmenu'); + $li.addClass('hover'); + }; + + Navigation.prototype.dropdownHover = function () { + $(this).addClass('hover'); + }; + + Navigation.prototype.dropdownLeave = function (event) { + var $li = $(this), + self = event.data.self; + setTimeout(function () { + // TODO: make this behave well together with keyboard navigation + if (! $li.is('li:hover') /*&& ! $li.find('a:focus')*/) { + $li.removeClass('hover'); + } + }, 300); + }; Icinga.Behaviors.Navigation = Navigation; }) (Icinga, jQuery); diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index 2b70cfa5b..18c5743be 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -126,96 +126,14 @@ $(document).on('keyup', '#menu input.search', {self: this}, this.autoSubmitSearch); - $(document).on('mouseenter', 'li.dropdown', this.dropdownHover); - $(document).on('mouseleave', 'li.dropdown', {self: this}, this.dropdownLeave); - - $(document).on('mouseenter', '#menu > ul > li', { self: this }, this.menuTitleHovered); - $(document).on('mouseleave', '#sidebar', { self: this }, this.leaveSidebar); $(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); }, - menuTitleHovered: function (event) { - var $li = $(this), - delay = 800, - self = event.data.self; - - if ($li.hasClass('active')) { - $li.siblings().removeClass('hover'); - return; - } - if ($li.children('ul').children('li').length === 0) { - return; - } - if ($('#menu').scrollTop() > 0) { - return; - } - - if ($('#layout').hasClass('hoveredmenu')) { - delay = 0; - } - - setTimeout(function () { - if (! $li.is('li:hover')) { - return; - } - if ($li.hasClass('active')) { - return; - } - - $li.siblings().each(function () { - var $sibling = $(this); - if ($sibling.is('li:hover')) { - return; - } - if ($sibling.hasClass('hover')) { - $sibling.removeClass('hover'); - } - }); - - $('#layout').addClass('hoveredmenu'); - $li.addClass('hover'); - }, delay); - }, - - leaveSidebar: function (event) { - var $sidebar = $(this), - $li = $sidebar.find('li.hover'), - self = event.data.self; - if (! $li.length) { - $('#layout').removeClass('hoveredmenu'); - return; - } - - setTimeout(function () { - if ($li.is('li:hover') || $sidebar.is('sidebar:hover') ) { - return; - } - $li.removeClass('hover'); - $('#layout').removeClass('hoveredmenu'); - }, 500); - }, - - dropdownHover: function () { - $(this).addClass('hover'); - }, - - dropdownLeave: function (event) { - var $li = $(this), - self = event.data.self; - setTimeout(function () { - // TODO: make this behave well together with keyboard navigation - if (! $li.is('li:hover') /*&& ! $li.find('a:focus')*/) { - $li.removeClass('hover'); - } - }, 300); - }, - treeNodeToggle: function () { var $parent = $(this).closest('li'); if ($parent.hasClass('collapsed')) { @@ -400,9 +318,7 @@ var $a = $(this); var href = $a.attr('href'); var linkTarget = $a.attr('target'); - var $li; var $target; - var isMenuLink = $a.closest('#menu').length > 0; var formerUrl; var remote = /^(?:[a-z]+:)\/\//; if (href.match(/^(mailto|javascript):/)) { @@ -453,25 +369,6 @@ // If link has hash tag... if (href.match(/#/)) { - // ...it may be a menu section without a dedicated link. - // Switch the active menu item: - if (isMenuLink) { - $li = $a.closest('li'); - $('#menu .active').removeClass('active'); - $li.addClass('active'); - if ($li.hasClass('hover')) { - $li.removeClass('hover'); - } - } - if (href === '#') { - // Allow to access dropdown menu by keyboard - if ($a.hasClass('dropdown-toggle')) { - $a.closest('li').toggleClass('hover'); - } - // Ignore link, no action - return false; - } - $target = self.getLinkTargetFor($a); formerUrl = $target.data('icingaUrl'); @@ -490,7 +387,7 @@ // Load link URL icinga.loader.loadUrl(href, $target); - if (isMenuLink) { + if ($a.closest('#menu').length > 0) { // Menu links should remove all but the first layout column icinga.ui.layout1col(); } @@ -575,8 +472,6 @@ $(document).off('submit', 'form', this.submitForm); $(document).off('click', 'button', this.submitForm); $(document).off('change', 'form select.autosubmit', this.submitForm); - $(document).off('mouseenter', 'li.dropdown', this.dropdownHover); - $(document).off('mouseleave', 'li.dropdown', this.dropdownLeave); }, destroy: function() { From 7aadad913eef6312df04b109a1863e68d0d8ed35 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 10 Sep 2014 15:51:02 +0200 Subject: [PATCH 6/8] Do not follow empty menu links --- public/js/icinga/behavior/navigation.js | 3 --- public/js/icinga/events.js | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js index 68b7c2e92..1def32727 100644 --- a/public/js/icinga/behavior/navigation.js +++ b/public/js/icinga/behavior/navigation.js @@ -74,10 +74,7 @@ if ($a.hasClass('dropdown-toggle')) { $a.closest('li').toggleClass('hover'); } - // Ignore link, no action - return false; } - } else { if (isMenuLink) { activeMenuId = $(event.target).closest('li').attr('id'); diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index 18c5743be..a10502a82 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -369,6 +369,9 @@ // If link has hash tag... if (href.match(/#/)) { + if (href === '#') { + return false; + } $target = self.getLinkTargetFor($a); formerUrl = $target.data('icingaUrl'); From e216ba6721651e44c45cec2b3847366d0e0dc9c5 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 10 Sep 2014 15:53:30 +0200 Subject: [PATCH 7/8] Merge stash --- public/js/icinga/behavior/sparkline.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/public/js/icinga/behavior/sparkline.js b/public/js/icinga/behavior/sparkline.js index 4c29f47cc..69a16527e 100644 --- a/public/js/icinga/behavior/sparkline.js +++ b/public/js/icinga/behavior/sparkline.js @@ -12,14 +12,12 @@ }; Sparkline.prototype.apply = function(el) { - var self = this, icinga = this.icinga; - $('span.sparkline', el).each(function(i, element) { // read custom options var $spark = $(element); var labels = $spark.attr('labels').split('|'); var formatted = $spark.attr('formatted').split('|'); - var tooltipChartTitle = $spark.attr('sparkSparklineChartTitle') || ''; + var tooltipChartTitle = $spark.attr('sparkTooltipChartTitle') || ''; var format = $spark.attr('tooltipformat'); var hideEmpty = $spark.attr('hideEmptyLabel') === 'true'; $spark.sparkline( From 0ec97eb93493da5b6aecf70f298a9b38c7916c77 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 10 Sep 2014 16:25:47 +0200 Subject: [PATCH 8/8] Only apply navigation link-click handlers to menu --- public/js/icinga/behavior/navigation.js | 48 +++++++++++-------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js index 1def32727..616109eaa 100644 --- a/public/js/icinga/behavior/navigation.js +++ b/public/js/icinga/behavior/navigation.js @@ -14,16 +14,14 @@ }; Navigation.prototype.apply = function(el) { - // restore menu state + // restore old menu state if (activeMenuId) { $('[role="navigation"] li.active', el).removeClass('active'); - - var $selectedMenu = $('#' + activeMenuId, el); + var $selectedMenu = $('#' + activeMenuId, el).addClass('active'); var $outerMenu = $selectedMenu.parent().closest('li'); if ($outerMenu.size()) { - $selectedMenu = $outerMenu; + $outerMenu.addClass('active'); } - $selectedMenu.addClass('active'); } else { // store menu state var $menus = $('[role="navigation"] li.active', el); @@ -34,8 +32,8 @@ }; Navigation.prototype.bind = function() { - $(document).on('click', 'a', { self: this }, this.linkClicked); - $(document).on('click', 'tr[href]', { self: this }, this.linkClicked); + $(document).on('click', '#menu a', { self: this }, this.linkClicked); + $(document).on('click', '#menu tr[href]', { self: this }, this.linkClicked); $(document).on('mouseenter', 'li.dropdown', this.dropdownHover); $(document).on('mouseleave', 'li.dropdown', {self: this}, this.dropdownLeave); $(document).on('mouseenter', '#menu > ul > li', { self: this }, this.menuTitleHovered); @@ -43,8 +41,8 @@ }; Navigation.prototype.unbind = function() { - $(document).off('click', 'a', this.linkClicked); - $(document).off('click', 'tr[href]', this.linkClicked); + $(document).off('click', '#menu a', this.linkClicked); + $(document).off('click', '#menu tr[href]', this.linkClicked); $(document).off('mouseenter', 'li.dropdown', this.dropdownHover); $(document).off('mouseleave', 'li.dropdown', this.dropdownLeave); $(document).off('mouseenter', '#menu > ul > li', this.menuTitleHovered); @@ -53,40 +51,34 @@ Navigation.prototype.linkClicked = function(event) { var $a = $(this); var href = $a.attr('href'); - var isMenuLink = $a.closest('#menu').length > 0; var $li; var icinga = event.data.self.icinga; if (href.match(/#/)) { // ...it may be a menu section without a dedicated link. // Switch the active menu item: - if (isMenuLink) { - $li = $a.closest('li'); - $('#menu .active').removeClass('active'); - $li.addClass('active'); - activeMenuId = $($li).attr('id'); - if ($li.hasClass('hover')) { - $li.removeClass('hover'); - } + $li = $a.closest('li'); + $('#menu .active').removeClass('active'); + $li.addClass('active'); + activeMenuId = $($li).attr('id'); + if ($li.hasClass('hover')) { + $li.removeClass('hover'); } if (href === '#') { // Allow to access dropdown menu by keyboard if ($a.hasClass('dropdown-toggle')) { $a.closest('li').toggleClass('hover'); } + return; } } else { - if (isMenuLink) { - activeMenuId = $(event.target).closest('li').attr('id'); - } - } - if (isMenuLink) { - var $menu = $('#menu'); - // update target url of the menu container to the clicked link - var menuDataUrl = icinga.utils.parseUrl($menu.data('icinga-url')); - menuDataUrl = icinga.utils.addUrlParams(menuDataUrl.path, { url: href }); - $menu.data('icinga-url', menuDataUrl); + activeMenuId = $(event.target).closest('li').attr('id'); } + // update target url of the menu container to the clicked link + var $menu = $('#menu'); + var menuDataUrl = icinga.utils.parseUrl($menu.data('icinga-url')); + menuDataUrl = icinga.utils.addUrlParams(menuDataUrl.path, { url: href }); + $menu.data('icinga-url', menuDataUrl); }; Navigation.prototype.menuTitleHovered = function(event) {