Files
iTop/js/layouts/navigation-menu.js

407 lines
14 KiB
JavaScript

/*
* Copyright (C) 2013-2024 Combodo SAS
*
* This file is part of iTop.
*
* iTop is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* iTop is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
*/
;
$(function()
{
$.widget( 'itop.navigation_menu',
{
// default options
options:
{
active_menu_group: null,
display_counts: false,
filter_keyup_throttle: 200, // In milliseconds
org_id: ''
},
css_classes:
{
is_hidden: 'ibo-is-hidden',
menu_expanded: 'ibo-is-expanded',
menu_active: 'ibo-is-active',
menu_filtered: 'ibo-is-filtered',
menu_group_active: 'ibo-is-active',
menu_nodes_active: 'ibo-is-active'
},
js_selectors:
{
menu_toggler: '[data-role="ibo-navigation-menu--toggler"]',
menu_group: '[data-role="ibo-navigation-menu--menu-group"]',
menu_drawer: '[data-role="ibo-navigation-menu--drawer"]',
menu_filter_placeholder: '[data-role="ibo-navigation-menu--menu--placeholder"]',
menu_filter_input: '[data-role="ibo-navigation-menu--menu-filter-input"]',
menu_filter_clear: '[data-role="ibo-navigation-menu--menu-filter-clear"]',
menu_filter_hint: '[data-role="ibo-navigation-menu--menu-filter-hint"]',
menu_filter_hint_close: '[data-role="ibo-navigation-menu--menu-filter-hint-close"]',
user_menu_toggler: '[data-role="ibo-navigation-menu--user-menu--toggler"]',
user_menu_container: '[data-role="ibo-navigation-menu--user-menu-container"]',
user_menu: '[data-role="ibo-navigation-menu--user-menu-container"] > [data-role="ibo-popover-menu"]',
menu_node: '[data-role="ibo-navigation-menu--menu-node"]',
menu_node_label: '[data-role="ibo-navigation-menu--menu-node-label"]',
},
filter_throttle_timeout: null,
// the constructor
_create: function () {
this.element.addClass('ibo-navigation-menu');
this._bindEvents();
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function () {
this.element.removeClass('ibo-navigation-menu');
},
_bindEvents: function () {
const me = this;
const oBodyElem = $('body');
// Click on collapse/expand toggler
this.element.find(this.js_selectors.menu_toggler).on('click', function (oEvent) {
me._onTogglerClick(oEvent);
});
// Click on menu group
this.element.find(this.js_selectors.menu_group).on('click', function (oEvent) {
me._onMenuGroupClick(oEvent, $(this))
});
this.element.on('filter_shortcut', function(oEvent){
me._filterShortcut();
});
// Mostly for outside clicks that should close elements
oBodyElem.on('click', function (oEvent) {
me._onBodyClick(oEvent);
});
// Mostly for hotkeys
oBodyElem.on('keyup', function (oEvent) {
me._onBodyKeyUp(oEvent);
});
// Menus filter
// - Input itself
this.element.find(this.js_selectors.menu_filter_input).on('keyup', function (oEvent) {
me._onFilterKeyUp(oEvent);
});
// - Clear icon
this.element.find(this.js_selectors.menu_filter_clear).on('click', function (oEvent) {
me._onFilterClearClick(oEvent);
});
// - Hint close
this.element.find(this.js_selectors.menu_filter_hint_close).on('click', function (oEvent) {
me._onFilterHintCloseClick(oEvent);
});
// External events
oBodyElem.on('add_shortcut_node.navigation_menu.itop', function (oEvent, oData) {
me._onAddShortcutNode(oData);
});
},
// Events callbacks
_onTogglerClick: function(oEvent)
{
// Avoid anchor glitch
oEvent.preventDefault();
// Toggle menu
this.element.toggleClass(this.css_classes.menu_expanded);
// Save state in user preferences
const sPrefValue = this.element.hasClass(this.css_classes.menu_expanded) ? 'expanded' : 'collapsed';
SetUserPreference('navigation_menu.expanded', sPrefValue, true);
},
_onMenuGroupClick: function(oEvent, oMenuGroupElem)
{
// Avoid anchor glitch
oEvent.preventDefault();
var sMenuGroupId = oMenuGroupElem.attr('data-menu-group-id');
if(this._getActiveMenuGroupId() === sMenuGroupId) {
this._closeDrawer();
}
else {
this._openDrawer(sMenuGroupId);
}
},
_onBodyClick: function(oEvent)
{
if(this._checkIfClickShouldCloseDrawer(oEvent))
{
this._closeDrawer();
}
},
_onBodyKeyUp: function(oEvent)
{
},
_filterShortcut: function()
{
if(this._getActiveMenuGroupId() === null)
{
const sFirstMenuGroupId = this.element.find(this.js_selectors.menu_group).first().attr('data-menu-group-id');
this._openDrawer(sFirstMenuGroupId);
}
this._focusFilter();
},
_onFilterKeyUp: function(oEvent)
{
const me = this;
const oInputElem = this.element.find(this.js_selectors.menu_filter_input);
const sValue = oInputElem.val();
if((sValue === '') && (oEvent.key === 'Escape'))
{
this._closeDrawer();
}
else if((sValue === '') || (oEvent.key === 'Escape'))
{
this._clearFiltering();
}
else {
// Reset throttle timeout on key stroke
clearTimeout(this.filter_throttle_timeout);
this.filter_throttle_timeout = setTimeout(function () {
me._doFiltering(sValue);
me.refreshCounts();
}, this.options.filter_keyup_throttle);
}
},
_onFilterClearClick: function(oEvent)
{
// Avoid anchor glitch
oEvent.preventDefault();
// Remove current filter value
this._clearFiltering();
// Position focus in the input for better UX
this._focusFilter();
},
_onFilterHintCloseClick: function (oEvent) {
this.element.find(this.js_selectors.menu_filter_hint).hide();
// Save state in user preferences
SetUserPreference('navigation_menu.show_filter_hint', false, true);
},
_onAddShortcutNode: function (oData) {
this._addShortcut(oData.parent_menu_node_id, oData.new_menu_node_html_rendering, oData.new_menu_name);
},
// Methods
_checkIfClickShouldCloseDrawer: function (oEvent) {
if (
$(oEvent.target.closest(this.js_selectors.menu_drawer)).length === 0
&& $(oEvent.target.closest('[data-role="ibo-navigation-menu--menu-group"]')).length === 0
&& $(oEvent.target.closest(this.js_selectors.menu_toggler)).length === 0
) {
this._closeDrawer();
}
},
/**
* Return the ID of the active menu group, or null if none (typically when the drawer is closed)
* @returns {null|*}
* @private
*/
_getActiveMenuGroupId: function()
{
const oActiveMenuGroup = this.element.find('.'+this.css_classes.menu_group_active);
if(oActiveMenuGroup.length > 0)
{
return oActiveMenuGroup.attr('data-menu-group-id');
}
else
{
return null;
}
},
/**
* Clear the current active menu group but does NOT close the drawer
* @private
*/
_clearActiveMenuGroup: function()
{
this.element.find('[data-role="ibo-navigation-menu--menu-group"]').removeClass(this.css_classes.menu_group_active);
this.element.find('[data-role="ibo-navigation-menu--menu-nodes"]').removeClass(this.css_classes.menu_nodes_active);
},
/**
* Open the drawer and set sMenuGroupId as the current active menu group
* @param sMenuGroupId string
* @private
*/
_openDrawer: function (sMenuGroupId) {
this.refreshCounts();
this._clearActiveMenuGroup();
// Note: This causes the filter to be cleared event when using the hotkey to reopen a previously filled filter
this._clearFiltering();
// Set new active group
this.element.find('[data-role="ibo-navigation-menu--menu-group"][data-menu-group-id="' + sMenuGroupId + '"]').addClass(this.css_classes.menu_group_active);
this.element.find('[data-role="ibo-navigation-menu--menu-nodes"][data-menu-group-id="' + sMenuGroupId + '"]').addClass(this.css_classes.menu_nodes_active);
// Set menu as active
this.element.addClass(this.css_classes.menu_active);
},
/**
* Close the drawer after clearing the active menu group
* @private
*/
_closeDrawer: function () {
this._clearActiveMenuGroup();
// Set menu as non active
this.element.removeClass(this.css_classes.menu_active);
},
// Menus filter methods
_focusFilter: function () {
this.element.find(this.js_selectors.menu_filter_input)
.trigger('click')
.trigger('focus');
},
/**
* Remove the current filter value and reset the menu nodes display
* @private
*/
_clearFiltering: function () {
this.element.find(this.js_selectors.menu_filter_input).val('');
// Reset display of everything
// Note: We work on the 'display' property directly as there is a CSS rule managing the visibility of the active menu group
this.element.find('[data-role="ibo-navigation-menu--menu-nodes"]').css('display', '');
this.element.find('[data-role="ibo-navigation-menu--menu-node"]').css('display', '');
this.element.find(this.js_selectors.menu_filter_placeholder).css('display', 'none');
// Mark menu as unfiltered
this.element.removeClass(this.css_classes.menu_filtered);
},
/**
* Filter the displayed menu nodes regarding the current filter value
* @param sRawFilterValue string
* @private
*/
_doFiltering: function(sRawFilterValue)
{
const me = this;
const aFilterValueParts = this._formatValueForFilterComparison(sRawFilterValue).split(' ');
let bHasAnyMatch = false;
// Mark menu as filtered
this.element.addClass(this.css_classes.menu_filtered);
// Hide everything
this.element.find('[data-role="ibo-navigation-menu--menu-nodes"]').hide();
this.element.find('[data-role="ibo-navigation-menu--menu-node"]').hide();
// Show matching menu node
this.element.find('[data-role="ibo-navigation-menu--menu-node"]').each(function () {
// Note: We don't filter on data-role="ibo-navigation-menu--menu-node-label" on purpose so we can also filter the counters
const sNodeValue = me._formatValueForFilterComparison($(this).children('[data-role="ibo-navigation-menu--menu-node-title"]').first().text());
let bMatches = true;
// On first non matching part, we consider that the menu node is not a match
for (let iIdx in aFilterValueParts) {
if (sNodeValue.indexOf(aFilterValueParts[iIdx]) === -1) {
bMatches = false;
break;
}
}
if (bMatches) {
me.element.find(me.js_selectors.menu_filter_placeholder).css('display', 'none');
bHasAnyMatch = true;
// Note: Selector must be recursive
$(this).parents('[data-role="ibo-navigation-menu--menu-nodes"], [data-role="ibo-navigation-menu--menu-node"]').show();
$(this).show();
}
});
if(!bHasAnyMatch)
{
this.element.find(this.js_selectors.menu_filter_placeholder).css('display', '');
}
},
/**
* Format sOriginalValue for an easier comparison (change accents, capitalized letters, ...)
*
* @param sOriginalValue string
* @returns string
* @private
*/
_formatValueForFilterComparison: function (sOriginalValue) {
return sOriginalValue.toLowerCase().latinise();
},
/**
* Refresh count badges for OQL menus
*/
refreshCounts: function () {
const me = this;
if (this.options.display_counts) {
$.ajax({
method: "POST",
url: GetAbsoluteUrlAppRoot() + 'pages/ajax.render.php',
data: {
operation: "get_menus_count",
c: { org_id: me.options.org_id }
},
dataType: "json"
})
.done(function (data) {
if (data.code === "done") {
for (const [key, value] of Object.entries(data.counts)) {
let menuEntry = me.element.find('[data-menu-id="'+key+'"]');
menuEntry.html(value);
menuEntry.removeClass(me.css_classes.is_hidden);
}
}
});
}
},
/**
* @param sParentMenuNodeId {string} ID of the parent menu node the shortcut should be added to
* @param sNewMenuNodeHtmlRendering {string} HTML rendering of the new menu node to add
* @param sNewMenulabel {string} Label of the menu node to add
* @return {boolean}
*/
_addShortcut: function (sParentMenuNodeId, sNewMenuNodeHtmlRendering, sNewMenulabel) {
const oNewMenuNodeContainerElem = this.element.find(this.js_selectors.menu_node+'[data-menu-node-id="'+sParentMenuNodeId+'"]');
if (oNewMenuNodeContainerElem.length === 0) {
return false;
}
let oNewMenuNodeContainerElemUL = oNewMenuNodeContainerElem.find('ul');
if (oNewMenuNodeContainerElemUL.length === 0) {
oNewMenuNodeContainerElem.append('<ul>'+sNewMenuNodeHtmlRendering+'</ul>');
} else {
let oChildrenElem = oNewMenuNodeContainerElem.find('li');
let iIndex = 0;
let bInsertToDo = true;
while (bInsertToDo && iIndex < oChildrenElem.length) {
let oCurrentChild = oChildrenElem.eq(iIndex);
if (oCurrentChild.find(this.js_selectors.menu_node_label).attr('title').toUpperCase() > sNewMenulabel.toUpperCase()) {
oCurrentChild.before(sNewMenuNodeHtmlRendering);
bInsertToDo = false;
}
iIndex++;
}
if (bInsertToDo) {
oNewMenuNodeContainerElemUL.append(sNewMenuNodeHtmlRendering);
}
}
return true;
}
});
});