Menu navigation: Improve flyout transition (#5367)

resolves #5160
This commit is contained in:
Ravi Kumar Kempapura Srinivasa 2025-05-08 16:17:14 +02:00 committed by GitHub
parent aa7a60c893
commit 63a73eab6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -6,15 +6,24 @@
Icinga.Behaviors = Icinga.Behaviors || {}; Icinga.Behaviors = Icinga.Behaviors || {};
try {
var d3 = require("icinga/icinga-php-thirdparty/mbostock/d3");
} catch (e) {
console.warn('D3.js library is unavailable. Navigation to flyout may not work as expected.');
}
var Navigation = function (icinga) { var Navigation = function (icinga) {
Icinga.EventListener.call(this, icinga); Icinga.EventListener.call(this, icinga);
this.on('click', '#menu a', this.linkClicked, this); this.on('click', '#menu a', this.linkClicked, this);
this.on('click', '#menu tr[href]', this.linkClicked, this); this.on('click', '#menu tr[href]', this.linkClicked, this);
this.on('rendered', '#menu', this.onRendered, this); this.on('rendered', '#menu', this.onRendered, this);
this.on('mouseenter', '#menu .primary-nav .nav-level-1 > .nav-item', this.showFlyoutMenu, this); if (typeof d3 !== "undefined") {
this.on('mousemove', '#menu .primary-nav .nav-level-1 > .nav-item', this.onMouseMove, this);
}
this.on('mouseenter', '#menu .primary-nav .nav-level-1 > .nav-item', this.onMouseEnter, this);
this.on('mouseleave', '#menu .primary-nav', this.hideFlyoutMenu, this); this.on('mouseleave', '#menu .primary-nav', this.hideFlyoutMenu, this);
this.on('click', '#toggle-sidebar', this.toggleSidebar, this); this.on('click', '#toggle-sidebar', this.toggleSidebar, this);
this.on('click', '#menu .config-nav-item button', this.toggleConfigFlyout, this); this.on('click', '#menu .config-nav-item button', this.toggleConfigFlyout, this);
this.on('mouseenter', '#menu .config-menu .config-nav-item', this.showConfigFlyout, this); this.on('mouseenter', '#menu .config-menu .config-nav-item', this.showConfigFlyout, this);
this.on('mouseleave', '#menu .config-menu .config-nav-item', this.hideConfigFlyout, this); this.on('mouseleave', '#menu .config-menu .config-nav-item', this.hideConfigFlyout, this);
@ -30,6 +39,21 @@
*/ */
this.active = null; this.active = null;
/**
* Represents the extended flyout zone, an area formed by the previous cursor position, and top-left
* and bottom-left flyout points.
*
* @type {Array}
*/
this.extendedFlyoutZone = new Array(3);
/**
* Timer for managing the delay in showing a flyout on mouse movement.
*
* @type {null|number}
*/
this.flyoutTimer = null;
/** /**
* The menu * The menu
* *
@ -282,40 +306,86 @@
}; };
/** /**
* Show the fly-out menu * Captures the mouse enter events to the navigation item and show the flyout.
* *
* @param e * @param e
*/ */
Navigation.prototype.showFlyoutMenu = function(e) { Navigation.prototype.onMouseEnter = function(e) {
var $layout = $('#layout'); const $layout = $('#layout');
const _this = e.data.self;
if ($layout.hasClass('minimal-layout')) { if ($layout.hasClass('minimal-layout')) {
return; return;
} }
var $target = $(this); const $target = $(this);
var $flyout = $target.find('.nav-level-2');
if (! $flyout.length) { if (
typeof d3 !== "undefined"
&& ! _this.extendedFlyoutZone.includes(undefined)
&& d3.polygonContains(_this.extendedFlyoutZone, [e.clientX, e.clientY])
) {
return;
}
if (! $target[0].matches(':has(.nav-level-2)')) {
$layout.removeClass('menu-hovered'); $layout.removeClass('menu-hovered');
$target.siblings().not($target).removeClass('hover'); $target.siblings().not($target).removeClass('hover');
return; return;
} }
var delay = 300;
if ($layout.hasClass('menu-hovered')) {
delay = 0;
}
setTimeout(function() {
try {
if (! $target.is(':hover')) { if (! $target.is(':hover')) {
return; return;
} }
} catch(e) { /* Bypass because if IE8 */ }
$layout.addClass('menu-hovered'); $layout.addClass('menu-hovered');
_this.extendedFlyoutZone[0] = [e.clientX, e.clientY];
_this.showFlyoutMenu($target);
}
/**
* Captures the mouse move events within the navigation item
* and show the flyout if needed.
*
* @param e
*/
Navigation.prototype.onMouseMove = function(e) {
const _this = e.data.self;
clearTimeout(_this.flyoutTimer);
const $target = $(this);
if (! $target[0].matches(':has(.nav-level-2)')) {
return;
}
if (! $target.hasClass('hover')) {
if (
! _this.extendedFlyoutZone.includes(undefined)
&& d3.polygonContains(_this.extendedFlyoutZone, [e.clientX, e.clientY])
) {
_this.flyoutTimer = setTimeout(function() {
_this.showFlyoutMenu($target);
}, 200);
} else {
// The extended flyout zone keeps shrinking when the mouse moves towards the target's flyout.
// Hence, if the mouse is moved and stopped over a new target, sometimes it could be slightly outside
// the extended flyout zone. This in turn will not trigger the flyoutTimer.
// Hence, the showFlyoutMenu should be manually called.
_this.showFlyoutMenu($target);
}
}
_this.extendedFlyoutZone[0] = [e.clientX, e.clientY];
};
/**
* Show the fly-out menu for the given target navigation item
*
* @param $target
*/
Navigation.prototype.showFlyoutMenu = function($target) {
const $flyout = $target.find('.nav-level-2');
$target.siblings().not($target).removeClass('hover'); $target.siblings().not($target).removeClass('hover');
$target.addClass('hover'); $target.addClass('hover');
@ -338,7 +408,9 @@
} }
$flyout.css(css); $flyout.css(css);
}, delay);
this.extendedFlyoutZone[1] = [flyoutRect.left, css.top];
this.extendedFlyoutZone[2] = [flyoutRect.left, css.top + flyoutRect.height];
}; };
/** /**
@ -350,6 +422,8 @@
var $layout = $('#layout'); var $layout = $('#layout');
var $nav = $(e.currentTarget); var $nav = $(e.currentTarget);
var $hovered = $nav.find('.nav-level-1 > .nav-item.hover'); var $hovered = $nav.find('.nav-level-1 > .nav-item.hover');
const _this = e.data.self;
_this.extendedFlyoutZone.fill(undefined);
if (! $hovered.length) { if (! $hovered.length) {
$layout.removeClass('menu-hovered'); $layout.removeClass('menu-hovered');