mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-12 23:14:18 +01:00
407 lines
14 KiB
JavaScript
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;
|
|
}
|
|
});
|
|
});
|