From 18de230a7e2b12a1615e23bfb8f55da41459bb4c Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 21 Mar 2019 09:09:27 +0100 Subject: [PATCH] Merge pull request #3504 from Icinga/fix/collapsible-sidebar-issues-3187 Fix collapsible sidebar issues (cherry picked from commit 915c7b8fe5ee9321f8e5b1977e06d005c22017d8) Signed-off-by: Johannes Meyer --- .../Icinga/Web/Navigation/NavigationItem.php | 40 ++++++ .../Renderer/BadgeNavigationItemRenderer.php | 7 -- .../Renderer/NavigationItemRenderer.php | 3 + .../Renderer/NavigationRenderer.php | 5 +- .../Renderer/NavigationRendererInterface.php | 7 ++ public/css/icinga/forms.less | 2 +- public/css/icinga/menu.less | 119 +++++++++++++----- public/js/icinga/behavior/navigation.js | 61 ++++++--- public/js/icinga/loader.js | 2 +- public/js/icinga/ui.js | 2 +- 10 files changed, 187 insertions(+), 61 deletions(-) diff --git a/library/Icinga/Web/Navigation/NavigationItem.php b/library/Icinga/Web/Navigation/NavigationItem.php index 17360a3f5..abc63ad90 100644 --- a/library/Icinga/Web/Navigation/NavigationItem.php +++ b/library/Icinga/Web/Navigation/NavigationItem.php @@ -37,6 +37,13 @@ class NavigationItem implements IteratorAggregate */ protected $active; + /** + * Whether this item is selected + * + * @var bool + */ + protected $selected; + /** * The CSS class used for the outer li element * @@ -213,6 +220,39 @@ class NavigationItem implements IteratorAggregate return $this; } + /** + * Return whether this item is selected + * + * @return bool + */ + public function getSelected() + { + if ($this->selected === null) { + $this->active = false; + if ($this->getUrl() !== null && Icinga::app()->getRequest()->getUrl()->matches($this->getUrl())) { + $this->setSelected(); + } + } + + return $this->selected; + } + + /** + * Set whether this item is active + * + * If it's active and has a parent, the parent gets activated as well. + * + * @param bool $selected + * + * @return $this + */ + public function setSelected($selected = true) + { + $this->selected = (bool) $selected; + + return $this; + } + /** * Get the CSS class used for the outer li element * diff --git a/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php index 8e03da85f..424c5489a 100644 --- a/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php +++ b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php @@ -100,15 +100,8 @@ abstract class BadgeNavigationItemRenderer extends NavigationItemRenderer $item->setCssClass('badge-nav-item'); $this->setEscapeLabel(false); $label = $this->view()->escape($item->getLabel()); - if (($icon = $item->getIcon()) !== null) { - $label = $this->view()->icon($icon) . $label; - $item->setIcon(null); - } $item->setLabel($this->renderBadge() . $label); $html = parent::render($item); - if ($icon) { - $item->setIcon(true); - } return $html; } diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php index c90e61e0b..3258d8bb4 100644 --- a/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php +++ b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php @@ -178,6 +178,9 @@ class NavigationItemRenderer : $item->getLabel(); if (($icon = $item->getIcon()) !== null) { $label = $this->view()->icon($icon) . $label; + } else { + $firstLetter = $item->getName()[0]; + $label = $this->view()->icon('letter', null, ['data-letter' => strtolower($firstLetter)]) . $label; } if (($url = $item->getUrl()) !== null) { diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php b/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php index 3e37b43fe..e2fb40b4f 100644 --- a/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php +++ b/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php @@ -322,9 +322,8 @@ class NavigationRenderer implements RecursiveIterator, NavigationRendererInterfa $cssClasses[] = static::CSS_CLASS_ACTIVE; } - if ($item->getIcon() === null) { - // @TODO(el): Add constant - $cssClasses[] = 'no-icon'; + if ($item->getSelected()) { + $cssClasses[] = static::CSS_CLASS_SELECTED; } if ($cssClass = $item->getCssClass()) { diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php b/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php index a2db929e1..4495b7379 100644 --- a/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php +++ b/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php @@ -22,6 +22,13 @@ interface NavigationRendererInterface */ const CSS_CLASS_ACTIVE = 'active'; + /** + * CSS class for selected items + * + * @var string + */ + const CSS_CLASS_SELECTED = 'selected'; + /** * CSS class for dropdown items * diff --git a/public/css/icinga/forms.less b/public/css/icinga/forms.less index e363a8c28..89211c165 100644 --- a/public/css/icinga/forms.less +++ b/public/css/icinga/forms.less @@ -60,7 +60,7 @@ input.search { &:focus { background-color: @body-bg-color; - background-image: url('../img/icons/search_icinga_blue.png') !important; + background-image: url('../img/icons/search_icinga_blue.png'); } } diff --git a/public/css/icinga/menu.less b/public/css/icinga/menu.less index 9083b65d8..4b37887fb 100644 --- a/public/css/icinga/menu.less +++ b/public/css/icinga/menu.less @@ -25,11 +25,6 @@ position: absolute; } -#layout:not(.minimal-layout).sidebar-collapsed #menu { - min-width: 6em; - width: auto; -} - #layout:not(.minimal-layout) #menu { // Space for the #toggle-sidebar button &:after { @@ -71,13 +66,8 @@ color: @menu-color; &.active { - > a { - font-weight: @font-weight-bold; - letter-spacing: .02em; - - > .badge { - display: none; - } + > a > .badge { + display: none; } background-color: @menu-active-bg-color; @@ -89,58 +79,89 @@ > a { padding: 0.5em 0.5em 0.5em .75em; + } - &:focus, &:hover { - color: @menu-highlight-color; - } + &.active:not(.selected) > a:focus, + &.active:not(.selected) > a:hover { + background-color: mix(#000, @menu-active-bg-color, 20); + } + + &:not(.selected) > a:hover, + &:not(.selected) > a:focus { + background-color: mix(#000, @menu-bg-color, 20); } // Balance icon weight for non active menu items &:not(.active) > a > i { opacity: .8; } + + & > a > .icon-letter:before { + content: attr(data-letter); + font-family: @font-family; + font-weight: 800; + text-transform: uppercase; + } +} + +ul:not(.nav-level-2) > .selected > a { + background-color: @menu-highlight-color; + color: @text-color-inverted; + + + &:hover { + color: @text-color-inverted; + } + + &:after { + .transform(rotate(45deg)); + + position: absolute; + right: -.75em; + + background-color: #fff; + box-shadow: 0 0 1em 0 rgba(0,0,0,0.6); + content: ""; + display: block; + height: 1.25em; + margin-top: -1.75em; + width: 1.25em; + } } #menu .nav-level-2 > .nav-item { // Collapse menu by default display: none; line-height: 1.833em; // 22px - padding-left: @icon-width; > a { color: @menu-2ndlvl-color; font-size: @font-size-small; - padding: 0.364em 0.545em 0.364em .8em; + padding: 0.364em 0.545em 0.364em (@icon-width + .75em); + } - &:hover, &:focus { - color: @menu-2ndlvl-highlight-color; - } + &:not(.selected):not(.active) > a:hover, + &:not(.selected):not(.active) > a:focus { + background-color: mix(#000, @menu-active-bg-color, 20); + color: @menu-2ndlvl-highlight-color; } &.active { background-color: @menu-highlight-color; overflow: hidden; position: relative; - - > a { - &:focus, &:hover { - opacity: .6; - } - } } // Little caret on active level-2 item &.active:after { + .transform(rotate(45deg)); + background-color: @body-bg-color; box-shadow: 0 0 1em 0 rgba(0,0,0,.6); content: ""; display: block; height: 1.25em; width: 1.25em; - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); position: absolute; top: .5em; right: -.75em; @@ -199,6 +220,11 @@ &.active { background-color: @menu-active-bg-color; } + + &:hover, + &:focus { + background-color: mix(#000, @menu-bg-color, 20); + } } // Badge offset correction @@ -223,6 +249,21 @@ width: 14em; position: fixed; + &:after { + .transform(rotate(45deg)); + + background-color: @body-bg-color; + border-bottom: 1px solid @gray-light; + border-left: 1px solid @gray-light; + content: ""; + display: block; + height: 1.1em; + width: 1.1em; + position: absolute; + top: 1em; + left: -.6em; + z-index: -1; + } > .nav-item { display: block; padding-left: 0; @@ -242,7 +283,6 @@ &:focus, &:hover { background-color: @menu-highlight-color; - opacity: .6; } } @@ -316,6 +356,10 @@ position: relative; } +.search-input:focus ~ .search-reset:hover { + background-color: mix(#000, @menu-active-bg-color, 20); +} + .search-reset { background: none; border: 0; @@ -329,8 +373,9 @@ right: 0; top: 0; - &:focus, &:hover { - color: @menu-highlight-color; + &:focus, + &:hover { + background-color: mix(#000, @menu-bg-color, 20); outline: none; } } @@ -390,7 +435,7 @@ input[type=text].search-input { padding: 0; color: @gray-light; position: absolute; - bottom: 0; + bottom: 0.2em; right: 0; i { @@ -418,3 +463,9 @@ input[type=text].search-input { #open-sidebar { display: none; } + +#open-sidebar:before, +#close-sidebar:before { + width: 1.4em; + margin-right: 0; +} diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js index 69386b2ee..c7c8b0907 100644 --- a/public/js/icinga/behavior/navigation.js +++ b/public/js/icinga/behavior/navigation.js @@ -52,11 +52,11 @@ var $active = _this.$menu.find('li.active'); if ($active.length) { $active.each(function() { - _this.setActive($(this)); + _this.setActiveAndSelected($(this)); }); } else { // if no item is marked as active, try to select the menu from the current URL - _this.setActiveByUrl($('#col1').data('icingaUrl')); + _this.setActiveAndSelectedByUrl($('#col1').data('icingaUrl')); } } @@ -70,7 +70,7 @@ // restore selection to current active element if (this.active) { var $el = $(this.icinga.utils.getElementByDomPath(this.active)); - this.setActive($el); + this.setActiveAndSelected($el); /* * Recreate the html content of the menu item to force the browser to update the layout, or else @@ -98,9 +98,9 @@ if (href.match(/#/)) { // ...it may be a menu section without a dedicated link. // Switch the active menu item: - _this.setActive($a); + _this.setActiveAndSelected($a); } else { - _this.setActive($(event.target)); + _this.setActiveAndSelected($(event.target)); } // update target url of the menu container to the clicked link var $menu = $('#menu'); @@ -116,7 +116,7 @@ * * @param url {String} The url to match */ - Navigation.prototype.setActiveByUrl = function(url) { + Navigation.prototype.setActiveAndSelectedByUrl = function(url) { var $menu = $('#menu'); if (! $menu.length) { @@ -124,17 +124,17 @@ } // try to active the first item that has an exact URL match - this.setActive($menu.find('[href="' + url + '"]')); + this.setActiveAndSelected($menu.find('[href="' + url + '"]')); // the url may point to the search field, which must be activated too if (! this.active) { - this.setActive($menu.find('form[action="' + this.icinga.utils.parseUrl(url).path + '"]')); + this.setActiveAndSelected($menu.find('form[action="' + this.icinga.utils.parseUrl(url).path + '"]')); } // some urls may have custom filters which won't match any menu item, in that case search // for a menu item that points to the base action without any filters if (! this.active) { - this.setActive($menu.find('[href="' + this.icinga.utils.parseUrl(url).path + '"]').first()); + this.setActiveAndSelected($menu.find('[href="' + this.icinga.utils.parseUrl(url).path + '"]').first()); } }; @@ -143,11 +143,12 @@ * * @param url */ - Navigation.prototype.trySetActiveByUrl = function(url) { + Navigation.prototype.trySetActiveAndSelectedByUrl = function(url) { var active = this.active; - this.setActiveByUrl(url); + this.setActiveAndSelectedByUrl(url); + if (! this.active && active) { - this.setActive($(this.icinga.utils.getElementByDomPath(active))); + this.setActiveAndSelected($(this.icinga.utils.getElementByDomPath(active))); } }; @@ -160,6 +161,15 @@ } }; + /** + * Remove all selected elements + */ + Navigation.prototype.clearSelected = function() { + if (this.$menu) { + this.$menu.find('.selected').removeClass('selected'); + } + }; + /** * Select all menu items in the selector as active and unfold surrounding menus when necessary * @@ -184,6 +194,11 @@ } }; + Navigation.prototype.setActiveAndSelected = function ($el) { + this.setActive($el); + this.setSelected($el); + }; + /** * Change the active menu element * @@ -202,6 +217,15 @@ // TODO: push to history }; + Navigation.prototype.setSelected = function($el) { + this.clearSelected(); + $el = $el.closest('li'); + + if ($el.length) { + $el.addClass('selected'); + } + }; + /** * Reset the active element to nothing */ @@ -210,6 +234,14 @@ this.active = null; }; + /** + * Reset the selected element to nothing + */ + Navigation.prototype.resetSelected = function() { + this.clearSelected(); + this.selected = null; + }; + /** * Show the fly-out menu * @@ -231,7 +263,7 @@ return; } - var delay = 600; + var delay = 300; if ($layout.hasClass('menu-hovered')) { delay = 0; @@ -318,9 +350,10 @@ ); return; } - this.setActive($(active)); + this.setActiveAndSelected($(active)) } else { this.resetActive(); + this.resetSelected(); } }; diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index aaccf683b..19b444e7a 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -610,7 +610,7 @@ var url = req.url; if (req.$target[0].id === 'col1') { - this.icinga.behaviors.navigation.trySetActiveByUrl(url); + this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl(url); } var $forms = $('[action="' + this.icinga.utils.parseUrl(url).path + '"]'); diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js index 31195351e..e89038380 100644 --- a/public/js/icinga/ui.js +++ b/public/js/icinga/ui.js @@ -186,7 +186,7 @@ var kill = this.cutContainer($('#col1')); this.pasteContainer($('#col1'), col2); this.fixControls(); - this.icinga.behaviors.navigation.trySetActiveByUrl($('#col1').data('icingaUrl')); + this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl($('#col1').data('icingaUrl')); }, cutContainer: function ($col) {