N°2847 - Rework of the global iTopWebPage layout (Part I)

- Rework of the iTopWebPage class
- Use of TWIG templates
- Preliminary work of the navigation menu
This commit is contained in:
Molkobain
2020-07-16 14:48:52 +02:00
parent c9b80074f4
commit ab681b0954
13 changed files with 1159 additions and 235 deletions

View File

@@ -1019,7 +1019,7 @@ class JSButtonItem extends JSPopupMenuItem
interface iPageUIExtension
{
/**
* Add content to the North pane
* Add content to the header of the page
*
* @param iTopWebPage $oPage The page to insert stuff into.
*
@@ -1028,7 +1028,7 @@ interface iPageUIExtension
public function GetNorthPaneHtml(iTopWebPage $oPage);
/**
* Add content to the South pane
* Add content to the footer of the page
*
* @param iTopWebPage $oPage The page to insert stuff into.
*

View File

@@ -551,8 +551,8 @@ EOF
$oLayout->Render($oPage, $this->aCells, $bEditMode, $aExtraParams);
if (!$bEditMode)
{
$oPage->add_linked_script('../js/dashlet.js');
$oPage->add_linked_script('../js/dashboard.js');
$oPage->add_linked_script('js/dashlet.js');
$oPage->add_linked_script('js/dashboard.js');
}
}

View File

@@ -21,11 +21,32 @@ require_once(APPROOT."/application/nicewebpage.class.inc.php");
require_once(APPROOT."/application/applicationcontext.class.inc.php");
require_once(APPROOT."/application/user.preferences.class.inc.php");
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
/**
* Web page with some associated CSS and scripts (jquery) for a fancier display
*/
class iTopWebPage extends NiceWebPage implements iTabbedPage
{
/** @var string ENUM_APP_ICON_SHAPE_SQUARE */
const ENUM_APP_ICON_SHAPE_SQUARE = 'square';
/** @var string ENUM_APP_ICON_SHAPE_FULL */
const ENUM_APP_ICON_SHAPE_FULL = 'full';
/** @var string DEFAULT_APP_ICON_SHAPE */
const DEFAULT_APP_ICON_SHAPE = self::ENUM_APP_ICON_SHAPE_FULL;
/** @var array Default and branding filenames for the app. icon in the backoffice */
protected static $aAppIconFilenames = [
self::ENUM_APP_ICON_SHAPE_SQUARE => [
'default' => 'itop-logo-square.png',
'branding' => 'backoffice-square-logo.png',
],
self::ENUM_APP_ICON_SHAPE_FULL => [
'default' => 'itop-logo.png',
'branding' => 'main-logo.png',
],
];
private $m_sMenu;
// private $m_currentOrganization;
private $m_aMessages;
@@ -71,41 +92,44 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
$this->m_aMessages = array();
$this->SetRootUrl(utils::GetAbsoluteUrlAppRoot());
$this->add_header("Content-type: text/html; charset=".self::PAGES_CHARSET);
$this->add_header("Cache-control: no-cache");
$this->add_linked_stylesheet("../css/jquery.treeview.css");
$this->add_linked_stylesheet("../css/jquery-ui-timepicker-addon.css");
$this->add_linked_stylesheet("../css/jquery.multiselect.css");
$this->add_linked_stylesheet("../css/magnific-popup.css");
$this->add_linked_stylesheet("../css/c3.min.css");
$this->add_linked_stylesheet("../css/font-awesome/css/all.min.css");
$this->add_linked_stylesheet("../js/ckeditor/plugins/codesnippet/lib/highlight/styles/obsidian.css");
// TODO: Should we keep this? Makes no sense
//$this->add_header("Cache-control: no-cache");
// TODO: Add only what's necessary
$this->add_linked_stylesheet("css/jquery.treeview.css");
$this->add_linked_stylesheet("css/jquery-ui-timepicker-addon.css");
$this->add_linked_stylesheet("css/jquery.multiselect.css");
$this->add_linked_stylesheet("css/magnific-popup.css");
$this->add_linked_stylesheet("css/c3.min.css");
$this->add_linked_stylesheet("css/font-awesome/css/all.min.css");
$this->add_linked_stylesheet("js/ckeditor/plugins/codesnippet/lib/highlight/styles/obsidian.css");
$this->add_linked_script('../js/jquery.layout.min.js');
$this->add_linked_script('../js/jquery.ba-bbq.min.js');
$this->add_linked_script("../js/jquery.treeview.js");
$this->add_linked_script("../js/date.js");
$this->add_linked_script("../js/jquery-ui-timepicker-addon.js");
$this->add_linked_script("../js/jquery-ui-timepicker-addon-i18n.min.js");
$this->add_linked_script("../js/jquery.blockUI.js");
$this->add_linked_script("../js/utils.js");
$this->add_linked_script("../js/swfobject.js");
$this->add_linked_script("../js/ckeditor/ckeditor.js");
$this->add_linked_script("../js/ckeditor/adapters/jquery.js");
$this->add_linked_script("../js/ckeditor/plugins/codesnippet/lib/highlight/highlight.pack.js");
$this->add_linked_script("../js/jquery.qtip-1.0.min.js");
$this->add_linked_script('../js/property_field.js');
$this->add_linked_script('../js/icon_select.js');
$this->add_linked_script('../js/raphael-min.js');
$this->add_linked_script('../js/d3.js');
$this->add_linked_script('../js/c3.js');
$this->add_linked_script('../js/jquery.multiselect.js');
$this->add_linked_script('../js/ajaxfileupload.js');
$this->add_linked_script('../js/jquery.mousewheel.js');
$this->add_linked_script('../js/jquery.magnific-popup.min.js');
$this->add_linked_script('../js/breadcrumb.js');
$this->add_linked_script('../js/moment-with-locales.min.js');
$this->add_linked_script('../js/showdown.min.js');
$this->add_linked_script('../js/newsroom_menu.js');
// TODO: Add only what's necessary
$this->add_linked_script('js/jquery.layout.min.js');
$this->add_linked_script('js/jquery.ba-bbq.min.js');
$this->add_linked_script("js/jquery.treeview.js");
$this->add_linked_script("js/date.js");
$this->add_linked_script("js/jquery-ui-timepicker-addon.js");
$this->add_linked_script("js/jquery-ui-timepicker-addon-i18n.min.js");
$this->add_linked_script("js/jquery.blockUI.js");
$this->add_linked_script("js/utils.js");
$this->add_linked_script("js/swfobject.js");
$this->add_linked_script("js/ckeditor/ckeditor.js");
$this->add_linked_script("js/ckeditor/adapters/jquery.js");
$this->add_linked_script("js/ckeditor/plugins/codesnippet/lib/highlight/highlight.pack.js");
$this->add_linked_script("js/jquery.qtip-1.0.min.js");
$this->add_linked_script('js/property_field.js');
$this->add_linked_script('js/icon_select.js');
$this->add_linked_script('js/raphael-min.js');
$this->add_linked_script('js/d3.js');
$this->add_linked_script('js/c3.js');
$this->add_linked_script('js/jquery.multiselect.js');
$this->add_linked_script('js/ajaxfileupload.js');
$this->add_linked_script('js/jquery.mousewheel.js');
$this->add_linked_script('js/jquery.magnific-popup.min.js');
$this->add_linked_script('js/breadcrumb.js');
$this->add_linked_script('js/moment-with-locales.min.js');
$this->add_linked_script('js/showdown.min.js');
$this->add_linked_script('js/newsroom_menu.js');
$this->add_dict_entry('UI:FillAllMandatoryFields');
@@ -180,6 +204,7 @@ EOF
*/
protected function PrepareLayout()
{
// TODO: Move this to the menu renderer
if (MetaModel::GetConfig()->Get('demo_mode'))
{
// No pin button
@@ -368,66 +393,6 @@ JS
<<< JS
try
{
var myLayout; // a var is required because this page utilizes: myLayout.allowOverflow() method
// Layout
paneSize = GetUserPreference('menu_size', 300);
if ($('body').length > 0)
{
myLayout = $('body').layout({
west : {
$sInitClosed minSize: 200, size: paneSize, spacing_open: 16, spacing_close: 16, slideTrigger_open: "click", hideTogglerOnSlide: true, enableCursorHotkey: false,
onclose_end: function(name, elt, state, options, layout)
{
if (state.isSliding == false)
{
$('.menu-pane-exclusive').show();
SetUserPreference('menu_pane', 'closed', true);
}
},
onresize_end: function(name, elt, state, options, layout)
{
if (state.isSliding == false)
{
SetUserPreference('menu_size', state.size, true);
}
},
onopen_end: function(name, elt, state, options, layout)
{
if (state.isSliding == false)
{
$('.menu-pane-exclusive').hide();
SetUserPreference('menu_pane', 'open', true);
}
}
},
center: {
onresize_end: function(name, elt, state, options, layout)
{
$('.v-resizable').each( function() {
var fixedWidth = $(this).parent().innerWidth() - 6;
$(this).width(fixedWidth);
// Make sure it cannot be resized horizontally
$(this).resizable('options', { minWidth: fixedWidth, maxWidth: fixedWidth });
// Now adjust all the child 'items'
var innerWidth = $(this).innerWidth() - 10;
$(this).find('.item').width(innerWidth);
});
$('.panel-resized').trigger('resized');
}
}
});
}
window.clearTimeout(iPaneVisWatchDog);
//myLayout.open( "west" );
$('.ui-layout-resizer-west .ui-layout-toggler').css({background: 'transparent'});
$sConfigureWestPane
if ($('#left-pane').length > 0)
{
$('#left-pane').layout({ resizable: false, spacing_open: 0, south: { size: 94 }, enableCursorHotkey: false });
}
// Tabs, using JQuery BBQ to store the history
// The "tab widgets" to handle.
var tabs = $('div[id^=tabbedContent]');
@@ -478,6 +443,7 @@ JS
JS
);
// TODO: What is this for?
$this->add_ready_script(
<<< JS
@@ -642,13 +608,17 @@ JS
});
JS
);
// TODO: To preserve
$this->add_ready_script(InlineImage::FixImagesWidth());
/*
* Not used since the sorting of the tables is always performed server-side
AttributeDateTime::InitTableSorter($this, 'custom_date_time');
AttributeDate::InitTableSorter($this, 'custom_date');
*/
// TODO: What is this for?
$sUserPrefs = appUserPreferences::GetAsJSON();
$this->add_script(
<<<JS
@@ -725,6 +695,20 @@ JS
);
}
protected function LoadTheme()
{
// TODO: Reuse theming mecanism
// $sCssThemeUrl = ThemeHandler::GetCurrentThemeUrl();
// $this->add_linked_stylesheet($sCssThemeUrl);
$sCssRelPath = utils::GetCSSFromSASS(
'css/backoffice/main.scss',
array(
APPROOT.'css/backoffice/',
)
);
$this->add_saas($sCssRelPath);
}
/**
* @param string $sId Identifies the item, to search after it in the current breadcrumb
@@ -833,15 +817,75 @@ JS
}
/**
* @throws \DictExceptionMissingString
* Return the complete revision number of the application
*
* @return string
* @since 2.8.0
*/
public function DisplayMenu()
public function GetApplicationRevisionNumber()
{
// Display the menu
$oAppContext = new ApplicationContext();
$iAccordionIndex = 0;
if (ITOP_REVISION == 'svn')
{
// This is NOT a version built using the build system, just display the main version
$sRevisionNumber = Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION);
}
else
{
// This is a build made from SVN, let display the full information
$sRevisionNumber = Dict::Format('UI:iTopVersion:Long', ITOP_APPLICATION, ITOP_VERSION, ITOP_REVISION, ITOP_BUILD_DATE);
}
ApplicationMenu::DisplayMenu($this, $oAppContext->GetAsHash());
return $sRevisionNumber;
}
/**
* Return the absolute URL to the application logo of $sShape
*
* @see static::ENUM_APP_ICON_SHAPE_SQUARE, static::ENUM_APP_ICON_SHAPE_FULL, ...
*
* @param string $sShape Shape of the icon to return
*
* @return string
* @throws \Exception
* @since 2.8.0
*/
public function GetApplicationIconUrl($sShape = self::DEFAULT_APP_ICON_SHAPE)
{
$sIconDefaultFilename = static::$aAppIconFilenames[$sShape]['default'];
$sIconBrandingFilename = static::$aAppIconFilenames[$sShape]['branding'];
$sIconAbsUrl = utils::GetAbsoluteUrlAppRoot().'images/'.$sIconDefaultFilename;
// Check if icon is overloaded by the branding
if (file_exists(MODULESROOT.'branding/'.$sIconBrandingFilename))
{
$sIconAbsUrl = utils::GetAbsoluteUrlModulesRoot().'branding/'.$sIconBrandingFilename;
}
$sIconAbsUrl .= '?t='.utils::GetCacheBusterTimestamp();
return $sIconAbsUrl;
}
/**
* Return the navigation menu data (id, menu groups, ...)
*
* @return array
* @throws \Exception
* @throws \DictExceptionMissingString
* @since 2.8.0
*/
public function GetNavigationMenuData()
{
$oAppContext = new ApplicationContext();
return [
'sId' => 'ibo-navigation-menu',
'sAppRevisionNumber' => $this->GetApplicationRevisionNumber(),
'sAppSquareIconUrl' => $this->GetApplicationIconUrl(static::ENUM_APP_ICON_SHAPE_SQUARE),
'sAppFullIconUrl' => $this->GetApplicationIconUrl(static::ENUM_APP_ICON_SHAPE_FULL),
'sAppIconLink' => MetaModel::GetConfig()->Get('app_icon_url'),
'aMenuGroups' => ApplicationMenu::GetMenuGroups($oAppContext->GetAsHash()),
];
}
/**
@@ -905,6 +949,72 @@ EOF
return $sNewsroomInitialImage;
}
/**
* Render the banner HTML which can come from both iTop itself and from extensions
*
* @see \iPageUIExtension::GetBannerHtml()
*
* @return string
* @since 2.8.0
*/
protected function RenderBannerHtml()
{
$sBannerHtml = '';
// Call the extensions to add content to the page, warning they can also add styles or scripts through as they have access to the \iTopWebPage
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sBannerHtml .= $oExtensionInstance->GetBannerHtml($this);
}
return $sBannerHtml;
}
/**
* Render the header HTML which can come from both iTop itself and from extensions
*
* @see \iPageUIExtension::GetNorthPaneHtml()
*
* @return string
* @since 2.8.0
*/
protected function RenderHeaderHtml()
{
$sHeaderHtml = '';
if (UserRights::IsAdministrator() && ExecutionKPI::IsEnabled())
{
// TODO: Don't forget this dude!
$sHeaderHtml .= '<div class="app-message"><span style="padding:5px;">'.ExecutionKPI::GetDescription().'<span></div>';
}
// Call the extensions to add content to the page, warning they can also add styles or scripts through as they have access to the \iTopWebPage
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sHeaderHtml .= $oExtensionInstance->GetNorthPaneHtml($this);
}
return $sHeaderHtml;
}
/**
* Render the footer HTML which can come from both iTop itself and from extensions
*
* @see \iPageUIExtension::GetSouthPaneHtml()
*
* @return string
* @since 2.8.0
*/
protected function RenderFooterHtml()
{
$sFooterHtml = '';
// Call the extensions to add content to the page, warning they can also add styles or scripts through as they have access to the \iTopWebPage
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sFooterHtml .= $oExtensionInstance->GetSouthPaneHtml($this);
}
}
/**
* @inheritDoc
@@ -912,36 +1022,78 @@ EOF
*/
public function output()
{
$sAbsURLAppRoot = addslashes($this->m_sRootUrl);
// Data to be passed to the view
$aData = [];
// Prepare page metadata
$sAbsoluteUrlAppRoot = addslashes($this->m_sRootUrl);
// TODO: Make it a property so it can be changed programmatically
// TODO: How to set both dark/light mode favicons
$sFaviconUrl = $sAbsoluteUrlAppRoot.'images/favicon.ico';
// TODO: Get this for the current user language
$sMetadataLanguage = 'en';
// Base structure of data to pass to the TWIG template
$aData['aPage'] = [
'sAbsoluteUrlAppRoot' => $sAbsoluteUrlAppRoot,
'sTitle' => $this->s_title,
'sFaviconUrl' => $sFaviconUrl,
'aMetadata' => [
'sCharset' => static::PAGES_CHARSET,
'sLang' => $sMetadataLanguage,
],
'aCssFiles' => $this->a_linked_stylesheets,
'aCssInline' => $this->a_styles,
'aJsFiles' => $this->a_linked_scripts,
'aJsInlineOnInit' => $this->m_aInitScript,
'aJsInlineOnDomReady' => $this->m_aReadyScripts,
'aJsInlineLive' => $this->a_scripts,
];
// Layouts
$aData['aLayouts'] = [
'sBanner' => $this->RenderBannerHtml(),
'sHeader' => $this->RenderHeaderHtml(),
'sFooter' => $this->RenderFooterHtml(),
];
// - Navigation menu
$aData['aLayouts']['aNavigationMenu'] = $this->GetNavigationMenuData();
$oTwigEnv = TwigHelper::GetTwigEnvironment(APPROOT.'templates/');
$sTemplateRelPath = 'pages/backoffice/layout';
// Send headers
if ($this->GetOutputFormat() === 'html')
{
foreach ($this->a_headers as $sHeader)
{
header($sHeader);
}
}
// Render final TWIG into global HTML
$oKpi = new ExecutionKPI();
$sHtml = TwigHelper::RenderTemplate($oTwigEnv, $aData, $sTemplateRelPath);
$oKpi->ComputeAndReport('TWIG rendering');
// Echo global HTML
$oKpi = new ExecutionKPI();
echo $sHtml;
$oKpi->ComputeAndReport('Echoing ('.round(strlen($sHtml) / 1024).' Kb)');
DBSearch::RecordQueryTrace();
ExecutionKPI::ReportStats();
return;
/////////////////////////////////////////////////////////
////////////////// ☢ DANGER ZONE ☢ /////////////////////
/////////////////////////////////////////////////////////
//$this->set_base($this->m_sRootUrl.'pages/');
$sForm = $this->GetSiloSelectionForm();
$this->DisplayMenu(); // Compute the menu
// Call the extensions to add content to the page, so that they can also add styles or scripts
$sBannerExtraHtml = '';
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sBannerExtraHtml .= $oExtensionInstance->GetBannerHtml($this);
}
$sNorthPane = '';
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sNorthPane .= $oExtensionInstance->GetNorthPaneHtml($this);
}
if (UserRights::IsAdministrator() && ExecutionKPI::IsEnabled())
{
$sNorthPane .= '<div class="app-message"><span style="padding:5px;">'.ExecutionKPI::GetDescription().'<span></div>';
}
//$sSouthPane = '<p>Peak memory Usage: '.sprintf('%.3f MB', memory_get_peak_usage(true) / (1024*1024)).'</p>';
$sSouthPane = '';
foreach (MetaModel::EnumPlugins('iPageUIExtension') as $oExtensionInstance)
{
$sSouthPane .= $oExtensionInstance->GetSouthPaneHtml($this);
}
// Render the tabs in the page (if any)
$this->s_content = $this->m_oTabs->RenderIntoContent($this->s_content, $this);
@@ -969,6 +1121,7 @@ EOF
EOF
);
// TODO: Extract this in a dedicated component
$iBreadCrumbMaxCount = utils::GetConfig()->Get('breadcrumb.max_count');
if ($iBreadCrumbMaxCount > 1)
{
@@ -1004,82 +1157,31 @@ EOF
);
}
// TODO: Extract in a dedicated component and call it in the nav menu
$sNewsRoomInitialImage = $this->InitNewsroom();
$this->outputCollapsibleSectionInit();
if ($this->GetOutputFormat() == 'html')
{
foreach ($this->a_headers as $s_header)
{
header($s_header);
}
}
// TODO: Is this for the "Debug" popup? We should do a helper to display a popup in various cases (welcome message for example)
$s_captured_output = $this->ob_get_clean_safe();
$sHtml = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n";
$sHtml .= "<html>\n";
$sHtml .= "<head>\n";
// Make sure that Internet Explorer renders the page using its latest/highest/greatest standards !
$sHtml .= "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n";
$sPageCharset = self::PAGES_CHARSET;
$sHtml .= "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=$sPageCharset\" />\n";
$sHtml .= "<title>".htmlentities($this->s_title, ENT_QUOTES, $sPageCharset)."</title>\n";
$sHtml .= $this->get_base_tag();
// Stylesheets MUST be loaded before any scripts otherwise
// jQuery scripts may face some spurious problems (like failing on a 'reload')
foreach ($this->a_linked_stylesheets as $a_stylesheet)
{
if (strpos($a_stylesheet['link'], '?') === false)
{
$s_stylesheet = $a_stylesheet['link']."?t=".utils::GetCacheBusterTimestamp();
}
else
{
$s_stylesheet = $a_stylesheet['link']."&t=".utils::GetCacheBusterTimestamp();
}
if ($a_stylesheet['condition'] != "")
{
$sHtml .= "<!--[if {$a_stylesheet['condition']}]>\n";
}
$sHtml .= "<link rel=\"stylesheet\" type=\"text/css\" href=\"{$s_stylesheet}\" />\n";
if ($a_stylesheet['condition'] != "")
{
$sHtml .= "<![endif]-->\n";
}
}
// TODO: Stylesheet for printing instead of having all those "IsPrintableVersion()" ifs
// TODO: Careful! In the print view, we can actually choose which part to print or not, so it's not just a print stylesheet...
// special stylesheet for printing, hides the navigation gadgets
$sHtml .= "<link rel=\"stylesheet\" media=\"print\" type=\"text/css\" href=\"../css/print.css?t=".utils::GetCacheBusterTimestamp()."\" />\n";
if ($this->GetOutputFormat() == 'html')
{
$sHtml .= $this->output_dict_entries(true); // before any script so that they can benefit from the translations
foreach ($this->a_linked_scripts as $s_script)
{
// Make sure that the URL to the script contains the application's version number
// so that the new script do NOT get reloaded from the cache when the application is upgraded
if (strpos($s_script, '?') === false)
{
$s_script .= "?t=".utils::GetCacheBusterTimestamp();
}
else
{
$s_script .= "&t=".utils::GetCacheBusterTimestamp();
}
$sHtml .= "<script type=\"text/javascript\" src=\"$s_script\"></script>\n";
}
if (!$this->IsPrintableVersion())
{
$this->add_script("var iPaneVisWatchDog = window.setTimeout('FixPaneVis()',5000);");
}
$sInitScripts = "";
if (count($this->m_aInitScript) > 0)
{
foreach ($this->m_aInitScript as $m_sInitScript)
{
$sInitScripts .= "$m_sInitScript\n";
}
}
$this->add_script("\$(document).ready(function() {\n{$sInitScripts};\nwindow.setTimeout('onDelayedReady()',10)\n});");
// TODO: Should we still do this init vs ready separation?
// $this->add_script("\$(document).ready(function() {\n{$sInitScripts};\nwindow.setTimeout('onDelayedReady()',10)\n});");
if ($this->IsPrintableVersion())
{
$this->add_ready_script(
@@ -1109,32 +1211,13 @@ $('legend').css('cursor', 'pointer').click(function(){
EOF
);
}
if (count($this->m_aReadyScripts) > 0)
{
$this->add_script("\nonDelayedReady = function() {\n".implode("\n", $this->m_aReadyScripts)."\n}\n");
}
if (count($this->a_scripts) > 0)
{
$sHtml .= "<script type=\"text/javascript\">\n";
foreach ($this->a_scripts as $s_script)
{
$sHtml .= "$s_script\n";
}
$sHtml .= "</script>\n";
}
}
if (count($this->a_styles) > 0)
{
$sHtml .= "<style>\n";
foreach ($this->a_styles as $s_style)
{
$sHtml .= "$s_style\n";
}
$sHtml .= "</style>\n";
}
// TODO: Does this still work?
$sHtml .= "<link rel=\"search\" type=\"application/opensearchdescription+xml\" title=\"iTop\" href=\"".utils::GetAbsoluteUrlAppRoot()."pages/opensearch.xml.php\" />\n";
$sHtml .= "<link rel=\"shortcut icon\" href=\"".utils::GetAbsoluteUrlAppRoot()."images/favicon.ico?t=".utils::GetCacheBusterTimestamp()."\" />\n";
$sHtml .= "</head>\n";
$sBodyClass = "";
@@ -1178,18 +1261,6 @@ EOF;
$sHtml .= "<div class=\"printable-content\" style=\"width: $sDefaultResolution;\">";
}
// Render the revision number
if (ITOP_REVISION == 'svn')
{
// This is NOT a version built using the buil system, just display the main version
$sVersionString = Dict::Format('UI:iTopVersion:Short', ITOP_APPLICATION, ITOP_VERSION);
}
else
{
// This is a build made from SVN, let display the full information
$sVersionString = Dict::Format('UI:iTopVersion:Long', ITOP_APPLICATION, ITOP_VERSION, ITOP_REVISION, ITOP_BUILD_DATE);
}
// Render the text of the global search form
$sText = htmlentities(utils::ReadParam('text', '', false, 'raw_data'), ENT_QUOTES, self::PAGES_CHARSET);
$sOnClick = " onclick=\"if ($('#global-search-input').val() != '') { $('#global-search form').submit(); } \"";
@@ -1206,6 +1277,7 @@ EOF;
$oAppContext = new ApplicationContext();
$sUserName = UserRights::GetUser();
// TODO: BEGIN USER MENU
$sIsAdmin = UserRights::IsAdministrator() ? '(Administrator)' : '';
if (UserRights::IsAdministrator())
{
@@ -1272,8 +1344,9 @@ EOF;
$aActions[$oAbout->GetUID()] = $oAbout->GetMenuItem();
$sLogOffMenu .= $this->RenderPopupMenuItems($aActions);
// TODO: END USER MENU
// TODO: Move this in the Header method
$sRestrictions = '';
if (!MetaModel::DBHasAccess(ACCESS_ADMIN_WRITE))
{
@@ -1305,6 +1378,7 @@ EOF;
$this->AddApplicationMessage($sRestrictions, $sIcon);
}
// TODO: Move this in the header method
$sApplicationMessages = '';
foreach ($this->m_aMessages as $aMessage)
{
@@ -1326,7 +1400,7 @@ EOF;
$sSouthPane = '<div id="bottom-pane" class="ui-layout-south">'.$sSouthPane.'</div>';
}
$sIconUrl = Utils::GetConfig()->Get('app_icon_url');
// TODO: What do we do with this?
$sOnlineHelpUrl = MetaModel::GetConfig()->Get('online_help');
//$sLogOffMenu = "<span id=\"logOffBtn\" style=\"height:55px;padding:0;margin:0;\"><img src=\"../images/onOffBtn.png\"></span>";
@@ -1438,9 +1512,9 @@ EOF;
if ($this->GetOutputFormat() == 'html')
{
$oKPI = new ExecutionKPI();
echo $sHtml;
$oKPI->ComputeAndReport('Echoing ('.round(strlen($sHtml) / 1024).' Kb)');
// $oKpi = new ExecutionKPI();
// echo $sHtml;
// $oKpi->ComputeAndReport('Echoing ('.round(strlen($sHtml) / 1024).' Kb)');
}
else
{
@@ -1474,8 +1548,8 @@ EOF;
}
}
}
DBSearch::RecordQueryTrace();
ExecutionKPI::ReportStats();
// DBSearch::RecordQueryTrace();
// ExecutionKPI::ReportStats();
}
/**

View File

@@ -200,11 +200,96 @@ class ApplicationMenu
return self::$aMenusIndex;
}
/**
* Return an array of menu groups
*
* @param array $aExtraParams
*
* @return array
* @throws \DictExceptionMissingString
* @since 2.8.0
*/
public static function GetMenuGroups($aExtraParams = array())
{
self::LoadAdditionalMenus();
// Sort the root menu based on the rank
usort(self::$aRootMenus, array('ApplicationMenu', 'CompareOnRank'));
$aMenuGroups = [];
foreach(static::$aRootMenus as $aMenuGroup)
{
if(!static::CanDisplayMenu($aMenuGroup))
{
continue;
}
$sMenuGroupIdx = $aMenuGroup['index'];
$oMenuNode = static::GetMenuNode($sMenuGroupIdx);
$aMenuGroups[] = [
'sId' => $oMenuNode->GetMenuID(),
'sIconCssClasses' => 'fas fa-fw fa-home', // TODO: Get the classes from the datamodel
'sTitle' => $oMenuNode->GetTitle(),
'aSubMenuNodes' => static::GetSubMenuNodes($sMenuGroupIdx, $aExtraParams),
];
}
return $aMenuGroups;
}
/**
* Return an array of sub-menu nodes for $sMenuGroupIdx
*
* @param string $sMenuGroupIdx
* @param array $aExtraParams
*
* @return array
* @throws \DictExceptionMissingString
* @since 2.8.0
*/
public static function GetSubMenuNodes($sMenuGroupIdx, $aExtraParams = array())
{
$aSubMenuItems = self::GetChildren($sMenuGroupIdx);
// Sort the children based on the rank
usort($aSubMenuItems, array('ApplicationMenu', 'CompareOnRank'));
$aSubMenuNodes = [];
foreach($aSubMenuItems as $aSubMenuItem)
{
if(!static::CanDisplayMenu($aSubMenuItem))
{
continue;
}
$sSubMenuItemIdx = $aSubMenuItem['index'];
$oSubMenuNode = static::GetMenuNode($sSubMenuItemIdx);
if(!$oSubMenuNode->IsEnabled())
{
continue;
}
$aSubMenuNodes[] = [
'sId' => $oSubMenuNode->GetMenuId(),
'sTitle' => $oSubMenuNode->GetTitle(),
'sUrl' => $oSubMenuNode->GetHyperlink($aExtraParams),
'bOpenInNewWindow' => $oSubMenuNode->IsHyperLinkInNewWindow(),
'aSubMenuNodes' => static::GetSubMenuNodes($sSubMenuItemIdx, $aExtraParams),
];
}
return $aSubMenuNodes;
}
/**
* Entry point to display the whole menu into the web page, used by iTopWebPage
* @param \WebPage $oPage
* @param array $aExtraParams
* @throws DictExceptionMissingString
*
* @deprecated Will be removed in 2.8.0
*/
public static function DisplayMenu($oPage, $aExtraParams)
{
@@ -280,6 +365,7 @@ EOF
* @return bool True if the currently selected menu is one of the submenus
* @throws DictExceptionMissingString
* @throws \Exception
* @deprecated Will be removed in 2.8.0
*/
protected static function DisplaySubMenu($oPage, $aMenus, $aExtraParams, $iActiveMenu = -1)
{

View File

@@ -0,0 +1,422 @@
/*!
* Copyright (C) 2013-2020 Combodo SARL
*
* 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
*/
/* SCSS variables (can be overloaded) */
$ibo-navigation-menu-height: 100vh !default;
$ibo-navigation-menu--body--padding-x: 16px !default;
$ibo-navigation-menu--body--padding-y: 16px !default;
$ibo-navigation-menu--body--width-collapsed: 60px !default;
$ibo-navigation-menu--body--width-expanded: 310px !default;
$ibo-navigation-menu--body--background-color: $ibo-color-blue-grey-900 !default;
$ibo-navigation-menu--body--text-color: $ibo-color-grey-300 !default;
$ibo-navigation-menu--top-part--padding-y: $ibo-navigation-menu--body--padding-y !default;
$ibo-navigation-menu--top-part--padding-x: $ibo-navigation-menu--body--padding-x !default;
$ibo-navigation-menu--top-part--elements-spacing: 20px !default;
$ibo-navigation-menu--middle-part--padding-top: 40px !default;
$ibo-navigation-menu--middle-part--padding-bottom: 16px !default;
$ibo-navigation-menu--middle-part--padding-x: $ibo-navigation-menu--body--padding-x !default;
$ibo-navigation-menu--middle-part--elements-spacing: 20px !default;
$ibo-navigation-menu--middle-part--scrollbar-width: 5px !default;
$ibo-navigation-menu--middle-part--scrollbar-track-background-color: $ibo-color-transparent !default;
$ibo-navigation-menu--middle-part--scrollbar-thumb-background-color: $ibo-color-grey-800 !default;
$ibo-navigation-menu--middle-part--scrollbar-thumb-border: none !default;
$ibo-navigation-menu--middle-part--scrollbar-thumb-border-radius: $ibo-border-radius-500 !default;
$ibo-navigation-menu--bottom-part--background-color: $ibo-color-grey-800 !default;
$ibo-navigation-menu--action--padding-x: 8px !default;
$ibo-navigation-menu--action--padding-y: 10px !default;
$ibo-navigation-menu--action--border-radius: $ibo-border-radius-500 !default;
$ibo-navigation-menu--action--border-radius--on-active: $ibo-border-radius-full !default;
$ibo-navigation-menu--action--border-radius--on-active-expanded: $ibo-border-radius-900 !default;
$ibo-navigation-menu--action--text-color--on-hover: $ibo-color-white-100 !default;
$ibo-navigation-menu--action--background-color--on-hover: $ibo-color-blue-grey-700 !default;
$ibo-navigation-menu--action--background-color--on-active: $ibo-color-grey-100 !default;
$ibo-navigation-menu--action-icon--width: 28px !default;
$ibo-navigation-menu--action-icon--text-color--on-hover: $ibo-color-white-100 !default;
$ibo-navigation-menu--square-company-logo--width: 38px !default;
$ibo-navigation-menu--square-company-logo--margin-top: 0 !default;
$ibo-navigation-menu--square-company-logo--margin-x: -5px !default;
$ibo-navigation-menu--square-company-logo--margin-bottom: $ibo-navigation-menu--body--padding-y * 2 !default;
$ibo-navigation-menu--toggler-icon--height: 20px !default;
$ibo-navigation-menu--toggler-icon--width: $ibo-navigation-menu--action-icon--width !default;
$ibo-navigation-menu--toggler-bar--height: 3px !default;
$ibo-navigation-menu--toggler-bar--width: 100% !default;
$ibo-navigation-menu--toggler--width: $ibo-navigation-menu--action-icon--width + $ibo-navigation-menu--top-part--padding-x !default;
$ibo-navigation-menu--menu-group--background-color--is-active: $ibo-color-grey-100 !default;
$ibo-navigation-menu--menu-group--border-radius--is-active: $ibo-border-radius-500 0 0 $ibo-border-radius-500 !default;
$ibo-navigation-menu--menu-group-icon--font-size: $ibo-font-size-350 !default;
$ibo-navigation-menu--menu-group-icon--text-color--is-active: $ibo-color-primary-500 !default;
$ibo-navigation-menu--menu-group-title--margin-left: $ibo-navigation-menu--middle-part--padding-x !default;
$ibo-navigation-menu--menu-group-title--text-color--is-active: $ibo-color-blue-grey-800 !default;
$ibo-navigation-menu--drawer--background-color: $ibo-color-grey-100 !default;
$ibo-navigation-menu--drawer--width: 312px !default;
$ibo-navigation-menu--drawer--padding-x: 20px !default;
$ibo-navigation-menu--drawer--padding-y: 32px !default;
$ibo-navigation-menu--menu-filter--margin-bottom: 55px !default;
$ibo-navigation-menu--menu-filter-input--padding-right: 64px !default;
$ibo-navigation-menu--menu-filter-hotkey--border: 1px solid $ibo-color-grey-400 !default;
/* TODO: Refactor this into the standard field input */
$ibo-navigation-menu--menu-filter-input--padding-x: 10px !default;
$ibo-navigation-menu--menu-filter-input--padding-y: 8px !default;
$ibo-navigation-menu--menu-filter-input--width: 100% !default;
$ibo-navigation-menu--menu-filter-input--placeholder-color: $ibo-color-grey-500 !default;
$ibo-navigation-menu--menu-filter-input--text-color: $ibo-color-grey-900 !default;
$ibo-navigation-menu--menu-filter-input--background-color: $ibo-color-white-100 !default;
$ibo-navigation-menu--menu-filter-input--border: 1px solid $ibo-color-grey-300 !default;
$ibo-navigation-menu--menu-filter-input--border-radius: $ibo-border-radius-300 !default;
$ibo-navigation-menu--menu-nodes-title--margin-top: 0 !default;
$ibo-navigation-menu--menu-nodes-title--margin-bottom: 32px !default;
$ibo-navigation-menu--menu-node--padding-x: 10px !default;
$ibo-navigation-menu--menu-node--padding-y: 6px !default;
$ibo-navigation-menu--menu-node--margin-x: -1 * $ibo-navigation-menu--menu-node--padding-x !default;
$ibo-navigation-menu--menu-node--margin-y: 0 !default;
$ibo-navigation-menu--menu-node--margin-top: 8px !default;
$ibo-navigation-menu--menu-node--text-color: $ibo-color-grey-700 !default;
$ibo-navigation-menu--menu-node--hyperlink-color: inherit !default;
$ibo-navigation-menu--menu-node--background-color: $ibo-color-grey-200 !default;
$ibo-navigation-menu--menu-node--border-radius: $ibo-border-radius-500 !default;
/* CSS variables (can be changed directly from the browser) */
:root {
/* TODO: Introduce variables once SCSS variables are set */
}
/* IMPORTANT: Rules are made for the collapsed mode by default */
/* Expanded rules should only be in the dedicated section */
.ibo-navigation-menu{
position: relative;
height: $ibo-navigation-menu-height;
&.ibo-navigation-menu--is-expanded{
.ibo-navigation-menu--body{
width: $ibo-navigation-menu--body--width-expanded;
.ibo-navigation-menu--toggler-bar{
&:nth-child(1){
top: 4px;
left: 7px;
width: 14px;
transform: rotateZ(-45deg);
}
&:nth-child(2){
top: 8px;
left: 7px;
width: 0;
opacity: 0;
}
&:nth-child(3){
top: 12px;
left: 7px;
width: 14px;
transform: rotateZ(45deg);
}
}
.ibo-navigation-menu--menu-group{
&:not(.ibo-navigation-menu--menu-group--is-active){
&:active{
border-radius: $ibo-navigation-menu--action--border-radius--on-active-expanded;
}
}
}
}
}
&.ibo-navigation-menu--is-active{
.ibo-navigation-menu--drawer{
right: calc(-1 * #{$ibo-navigation-menu--drawer--width});
}
}
}
.ibo-navigation-menu--body,
.ibo-navigation-menu--drawer{
height: $ibo-navigation-menu-height;
}
/* Body */
.ibo-navigation-menu--body{
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
height: $ibo-navigation-menu-height;
width: $ibo-navigation-menu--body--width-collapsed;
background-color: $ibo-navigation-menu--body--background-color;
transition: width 0.1s ease-in-out;
}
/* - Top part */
.ibo-navigation-menu--top-part{
padding: $ibo-navigation-menu--top-part--padding-y $ibo-navigation-menu--top-part--padding-x;
}
/* - Middle part */
.ibo-navigation-menu--middle-part{
/* Occupy as much space as possible */
flex-grow: 1;
/* Only the middle part should have a variable size */
overflow-y: auto;
padding: $ibo-navigation-menu--middle-part--padding-top $ibo-navigation-menu--middle-part--padding-x $ibo-navigation-menu--middle-part--padding-bottom;
/* Scrollbar for Firefox and future W3C specs. */
scrollbar-width: thin;
scrollbar-color: $ibo-navigation-menu--middle-part--scrollbar-thumb-background-color $ibo-navigation-menu--middle-part--scrollbar-track-background-color;
}
/* Scrollbar for Chrome/Edge/Safari */
.ibo-navigation-menu--middle-part::-webkit-scrollbar {
width: $ibo-navigation-menu--middle-part--scrollbar-width;
}
.ibo-navigation-menu--middle-part::-webkit-scrollbar-track {
background-color: $ibo-navigation-menu--middle-part--scrollbar-track-background-color;
}
.ibo-navigation-menu--middle-part::-webkit-scrollbar-thumb {
background-color: $ibo-navigation-menu--middle-part--scrollbar-thumb-background-color;
border: $ibo-navigation-menu--middle-part--scrollbar-thumb-border;
border-radius: $ibo-navigation-menu--middle-part--scrollbar-thumb-border-radius;
}
/* - Bottom part */
.ibo-navigation-menu--bottom-part{
height: 150px;
background-color: $ibo-navigation-menu--bottom-part--background-color;
}
.ibo-navigation-menu--toggler,
.ibo-navigation-menu--menu-group{
margin: calc(-1 * #{$ibo-navigation-menu--action--padding-y}) calc(-1 * #{$ibo-navigation-menu--action--padding-x});
padding: $ibo-navigation-menu--action--padding-y $ibo-navigation-menu--action--padding-x;
border-radius: $ibo-navigation-menu--action--border-radius;
}
/* Top part */
/* - Company logo: square */
.ibo-navigation-menu--square-company-logo{
display: flex;
width: $ibo-navigation-menu--square-company-logo--width;
margin: $ibo-navigation-menu--square-company-logo--margin-top $ibo-navigation-menu--square-company-logo--margin-x $ibo-navigation-menu--square-company-logo--margin-bottom;
}
/* - Toggler */
.ibo-navigation-menu--toggler{
display: flex;
/* Width is define here in addition of the icon so we can fix its width whether the menu is collapsed or expanded */
width: $ibo-navigation-menu--toggler--width;
&:hover,
&:active{
background-color: $ibo-navigation-menu--action--background-color--on-hover;
.ibo-navigation-menu--toggler-bar{
background-color: $ibo-navigation-menu--action--text-color--on-hover;
}
}
}
.ibo-navigation-menu--toggler-icon{
position: relative;
display: flex;
height: $ibo-navigation-menu--toggler-icon--height;
width: $ibo-navigation-menu--toggler-icon--width;
}
.ibo-navigation-menu--toggler-bar{
position: absolute;
display: block;
height: $ibo-navigation-menu--toggler-bar--height;
width: $ibo-navigation-menu--toggler-bar--width;
opacity: 1;
transition: all 0.2s linear;
background-color: $ibo-navigation-menu--body--text-color;
&:nth-child(1){
top: 0;
}
&:nth-child(2){
top: 8px;
}
&:nth-child(3){
top: 16px;
}
}
/* - Menu groups */
.ibo-navigation-menu--menu-group{
display: flex;
justify-content: left;
align-items: center;
/* To keep title on the line even when collapsed. Better visual feedback when expanding. */
white-space: nowrap;
overflow-x: hidden;
color: $ibo-navigation-menu--body--text-color;
@extend %ibo-font-ral-nor-200;
transition-property: background-color, color, padding, margin, border-radius;
transition-duration: 0.1s;
transition-timing-function: linear;
> .ibo-navigation-menu--menu-group-icon,
> .ibo-navigation-menu--menu-group-title{
display: flex; /* To avoid end space due to display:inline-block */
}
&:not(:last-child){
margin-bottom: $ibo-navigation-menu--middle-part--elements-spacing;
}
&:not(.ibo-navigation-menu--menu-group--is-active)
{
&:hover,
&:active{
color: $ibo-navigation-menu--action--text-color--on-hover;
background-color: $ibo-navigation-menu--action--background-color--on-hover;
}
&:active{
border-radius: $ibo-navigation-menu--action--border-radius--on-active;
}
}
&.ibo-navigation-menu--menu-group--is-active{
margin-right: calc(-2 * #{$ibo-navigation-menu--action--padding-x});
padding-right: calc(2 - #{$ibo-navigation-menu--action--padding-x});
color: $ibo-navigation-menu--menu-group-title--text-color--is-active;
background-color: $ibo-navigation-menu--menu-group--background-color--is-active;
border-radius: $ibo-navigation-menu--menu-group--border-radius--is-active;
.ibo-navigation-menu--menu-group-icon{
color: $ibo-navigation-menu--menu-group-icon--text-color--is-active;
}
}
}
.ibo-navigation-menu--menu-group-icon{
width: $ibo-navigation-menu--action-icon--width;
text-align: center;
font-size: $ibo-navigation-menu--menu-group-icon--font-size;
&::before{
width: $ibo-navigation-menu--action-icon--width;
}
}
.ibo-navigation-menu--menu-group-title{
margin-left: $ibo-navigation-menu--menu-group-title--margin-left;
text-align: left;
@extend %ibo-text-truncated-with-ellipsis;
}
/* Drawer */
.ibo-navigation-menu--drawer{
position: absolute;
z-index: -1;
top: 0;
bottom: 0;
right: 0;
width: $ibo-navigation-menu--drawer--width;
padding: $ibo-navigation-menu--drawer--padding-y $ibo-navigation-menu--drawer--padding-x;
overflow-y: auto;
background-color: $ibo-navigation-menu--drawer--background-color;
transition: right 0.2s ease-in-out;
}
/* - Menu filter */
.ibo-navigation-menu--menu-filter{
position: relative;
margin-bottom: $ibo-navigation-menu--menu-filter--margin-bottom;
}
.ibo-navigation-menu--menu-filter-input{
/* TODO: Refactor this into the standard field input */
width: $ibo-navigation-menu--menu-filter-input--width;
padding: $ibo-navigation-menu--menu-filter-input--padding-y $ibo-navigation-menu--menu-filter-input--padding-x;
color: $ibo-navigation-menu--menu-filter-input--text-color;
background-color: $ibo-navigation-menu--menu-filter-input--background-color;
border: $ibo-navigation-menu--menu-filter-input--border;
border-radius: $ibo-navigation-menu--menu-filter-input--border-radius;
&::placeholder{
color: $ibo-navigation-menu--menu-filter-input--placeholder-color;
}
/* This rule is duplicated otherwise Chrome won't be able to parse it. */
&:-ms-input-placeholder,
&::-ms-input-placeholder{
color: $ibo-navigation-menu--menu-filter-input--placeholder-color;
}
padding-right: $ibo-navigation-menu--menu-filter-input--padding-right; /* Must be at least #nm-filter-hotkey width + some padding */
}
.ibo-navigation-menu--menu-filter-clear{
display: none;
}
.ibo-navigation-menu--menu-filter-hotkey{
position: absolute;
top: $ibo-navigation-menu--menu-filter-input--padding-y;
right: $ibo-navigation-menu--menu-filter-input--padding-x;
border: $ibo-navigation-menu--menu-filter-hotkey--border;
border-radius: $ibo-navigation-menu--menu-filter-input--border-radius;
color: $ibo-navigation-menu--menu-filter-input--placeholder-color;
padding: 2px 4px;
@extend %ibo-font-ral-nor-50;
}
/* - Menu nodes */
.ibo-navigation-menu--menu-nodes{
display: none;
ul{
li{
> a,
> span{
display: block; /* Force to occupy parent's full width */
margin: $ibo-navigation-menu--menu-node--margin-y $ibo-navigation-menu--menu-node--margin-x;
padding: $ibo-navigation-menu--menu-node--padding-y $ibo-navigation-menu--menu-node--padding-x;
border-radius: 0;
color: $ibo-navigation-menu--menu-node--text-color;
@extend %ibo-font-ral-nor-150;
}
> a{
color: $ibo-navigation-menu--menu-node--hyperlink-color;
&:hover{
background-color: $ibo-navigation-menu--menu-node--background-color;
border-radius: $ibo-navigation-menu--menu-node--border-radius;
}
}
}
ul{
margin-top: $ibo-navigation-menu--menu-node--margin-top;
padding-left: $ibo-navigation-menu--drawer--padding-x;
}
}
&.ibo-navigation-menu--menu-nodes--is-active{
display: block;
}
}
.ibo-navigation-menu--menu-nodes-title{
margin-top: $ibo-navigation-menu--menu-nodes-title--margin-top;
margin-bottom: $ibo-navigation-menu--menu-nodes-title--margin-bottom;
@extend %ibo-font-ral-nor-350;
@extend %ibo-text-truncated-with-ellipsis;
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Copyright (C) 2013-2020 Combodo SARL
*
* 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
*/
// Navigation menu
Dict::Add('EN US', 'English', 'English', array(
'UI:Layout:NavigationMenu:CompanyLogo:AltText' => 'Company logo',
'UI:Layout:NavigationMenu:Toggler:Tooltip' => 'Expand / Collapse',
'UI:Layout:NavigationMenu:MenuFilter:Input:Placeholder' => 'Filter...',
'UI:Layout:NavigationMenu:MenuFilter:Input:Tooltip' => 'Type your keywords to filter menus',
'UI:Layout:NavigationMenu:MenuFilter:Input:Hotkey' => 'Alt + M',
));

BIN
images/itop-logo-square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2013-2020 Combodo SARL
*
* 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:
{
init_expanded: false,
active_menu_group: null,
},
css_classes:
{
menu_expanded: 'ibo-navigation-menu--is-expanded',
menu_active: 'ibo-navigation-menu--is-active',
menu_group_active: 'ibo-navigation-menu--menu-group--is-active',
menu_nodes_active: 'ibo-navigation-menu--menu-nodes--is-active'
},
// 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()
{
var me = this;
var oBodyElem = $('body');
// Click on collapse/expand toggler
this.element.find('[data-role="ibo-navigation-menu--toggler"]').on('click', function(oEvent){
me._onTogglerClick(oEvent);
});
// Click on menu group
this.element.find('[data-role="ibo-navigation-menu--menu-group"]').on('click', function(oEvent){
me._onMenuGroupClick(oEvent, $(this))
});
// Mostly for outside clicks that should close elements
oBodyElem.on('click', function(oEvent){
me._onBodyClick(oEvent);
});
},
// Events callbacks
_onTogglerClick: function(oEvent)
{
// Avoid anchor glitch
oEvent.preventDefault();
this.element.toggleClass(this.css_classes.menu_expanded);
// TODO: Save preference
},
_onMenuGroupClick: function(oEvent, oMenuGroupElem)
{
// Avoid anchor glitch
oEvent.preventDefault();
var sMenuGroupId = oMenuGroupElem.attr('data-menu-group-id');
this._openDrawer(sMenuGroupId);
},
_onBodyClick: function(oEvent)
{
if(this._checkIfClickShouldCloseDrawer(oEvent))
{
this._closeDrawer();
}
},
// Methods
_checkIfClickShouldCloseDrawer: function(oEvent)
{
if(
$(oEvent.target.closest('[data-role="ibo-navigation-menu--drawer"]')).length === 0
&& $(oEvent.target.closest('[data-role="ibo-navigation-menu--menu-group"]')).length === 0
&& $(oEvent.target.closest('[data-role="ibo-navigation-menu--toggler"]')).length === 0
)
{
this._closeDrawer();
}
},
/**
* 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._clearActiveMenuGroup();
// 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);
}
});
});

View File

@@ -0,0 +1,45 @@
<nav {% if sId is defined %}id="{{ sId }}"{% endif%} class="ibo-navigation-menu">
<div class="ibo-navigation-menu--body">
<div class="ibo-navigation-menu--top-part">
<a class="ibo-navigation-menu--square-company-logo" title="{{ sAppRevisionNumber }}" href="{{ sAppIconLink }}">
<img src="{{ sAppSquareIconUrl }}" alt="{{ 'UI:Layout:NavigationMenu:CompanyLogo:AltText'|dict_s }}" />
</a>
<a class="ibo-navigation-menu--toggler" data-role="ibo-navigation-menu--toggler" title="{{ 'UI:Layout:NavigationMenu:Toggler:Tooltip'|dict_s }}" href="#">
<span class="ibo-navigation-menu--toggler-icon">
<span class="ibo-navigation-menu--toggler-bar"></span>
<span class="ibo-navigation-menu--toggler-bar"></span>
<span class="ibo-navigation-menu--toggler-bar"></span>
</span>
</a>
</div>
<div class="ibo-navigation-menu--middle-part">
{% for aMenuGroup in aMenuGroups %}
{% include 'layouts/navigation-menu/menu-group.html.twig' with aMenuGroup %}
{% endfor %}
</div>
<div class="ibo-navigation-menu--bottom-part">
</div>
</div>
<div class="ibo-navigation-menu--drawer" data-role="ibo-navigation-menu--drawer">
<div class="ibo-navigation-menu--menu-filter" data-role="ibo-nav-menu--menu-filter">
{# TODO: Retrieve input style from a component? #}
<input class="ibo-navigation-menu--menu-filter-input" data-role="ibo-navigation-menu--menu-filter-input" type="text" placeholder="{{ 'UI:Layout:NavigationMenu:MenuFilter:Input:Placeholder'|dict_s }}" />
<a class="ibo-navigation-menu--menu-filter-clear" data-role="ibo-navigation-menu--menu-filter-clear" href="#"></a>
<span class="ibo-navigation-menu--menu-filter-hotkey">{{ 'UI:Layout:NavigationMenu:MenuFilter:Input:Hotkey'|dict_s }}</span>
</div>
<div class="ibo-navigation-menu--menu-groups">
{% for aMenuGroup in aMenuGroups %}
{% include 'layouts/navigation-menu/menu-nodes.html.twig' with aMenuGroup %}
{% endfor %}
</div>
</div>
</nav>
{# TODO: Move this to a dedicated script file #}
<script type="text/javascript" src="js/layouts/navigation-menu.js"></script>
<script type="text/javascript">
setTimeout(function(){
$('#{{ sId }}').navigation_menu();
}, 500);
</script>

View File

@@ -0,0 +1,4 @@
<a class="ibo-navigation-menu--menu-group" data-role="ibo-navigation-menu--menu-group" data-menu-group-id="{{ aMenuGroup.sId }}" href="#">
<span class="ibo-navigation-menu--menu-group-icon {{ aMenuGroup.sIconCssClasses }}" title="{{ aMenuGroup.sTitle }}"></span>
<span class="ibo-navigation-menu--menu-group-title">{{ aMenuGroup.sTitle }}</span>
</a>

View File

@@ -0,0 +1,15 @@
<li class="ibo-navigation-menu--menu-node" data-menu-node-id="{{ aMenuNode.sId }}">
{% if aMenuNode.sUrl is not empty %}
{% set sTarget = (aMenuNode.bOpenInNewWindow == true) ? 'target="_blank"' : '' %}
<a class="ibo-navigation-menu--menu-node-title" href="{{ aMenuNode.sUrl }}" {{ sTarget|raw }}>{{ aMenuNode.sTitle }}</a>
{% else %}
<span class="ibo-navigation-menu--menu-node-title">{{ aMenuNode.sTitle }}</span>
{% endif %}
{% if aMenuNode.aChildNodes is defined and aMenuNode.aChildNodes|length > 0 %}
<ul>
{% for aSubMenuNode in aMenuGroup.aSubMenuNodes %}
{% include 'layouts/navigation-menu/menu-node.html.twig' with aSubMenuNode %}
{% endfor %}
</ul>
{% endif %}
</li>

View File

@@ -0,0 +1,8 @@
<div class="ibo-navigation-menu--menu-nodes" data-role="ibo-navigation-menu--menu-nodes" data-menu-group-id="{{ aMenuGroup.sId }}">
<h2 class="ibo-navigation-menu--menu-nodes-title">{{ aMenuGroup.sTitle }}</h2>
<ul>
{% for aMenuNode in aMenuGroup.aSubMenuNodes %}
{% include 'layouts/navigation-menu/menu-node.html.twig' with aMenuNode %}
{% endfor %}
</ul>
</div>

View File

@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="{{ aPage.aMetadata.sLang }}">
<head>
<meta charset="{{ aPage.aMetadata.sCharset }}">
{# This block can be used to add your own meta tags by extending the default template #}
{% block iboPageExtraMetas %}
{% endblock %}
<base href="{{ aPage.sAbsoluteUrlAppRoot }}">
<title>{{ aPage.sTitle }}</title>
<link rel="shortcut icon" href="{{ aPage.sFaviconUrl|add_itop_version }}" />
{# Stylesheets MUST be loaded before any scripts otherwise we may face problems such as
- Visual glitches
- jQuery scripts spurious problems (like failing on a 'reload') #}
{% block iboPageCssFiles %}
{% for aCssFileData in aPage.aCssFiles %}
{% if aCssFileData['condition'] != '' %}<!--[if {{ aCssFileData['condition'] }}]>{% endif %}
<link type="text/css" href="{{ aCssFileData['link']|add_itop_version }}" rel="stylesheet" />
{% if aCssFileData['condition'] != '' %}<![endif]-->{% endif %}
{% endfor %}
{% endblock %}
{% block iboPageCssInline %}
{# We put each styles in a dedicated style tag to prevent massive failure if 1 style is broken (eg. missing semi-colon, bracket, ...) #}
{% for sCssInline in aPage.aCssInline %}
<style>
{{ sCssInline|raw }}
</style>
{% endfor %}
{% endblock %}
{% block iboPageJsFiles %}
{% for sJsFile in aPage.aJsFiles %}
<script type="text/javascript" src="{{ sJsFile|add_itop_version }}"></script>
{% endfor %}
{% endblock %}
</head>
<body data-gui-type="backoffice">
{% include 'layouts/navigation-menu/layout.html.twig' with aLayouts.aNavigationMenu %}
<div id="ibo-page-container">
<nav id="ibo-top-bar" class="ibo-top-bar">
<span>item</span><span>item</span><span>item</span><span>item</span><span>item</span><span>item</span><span>item</span><span>item</span>
</nav>
<main id="ibo-page-content">
<div>item</div>
<div>
itemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitem
</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
<div>item</div>
</main>
</div>
{% block iboPageJsInlineScripts %}
<script type="text/javascript">
{# TODO: How to do this in native JS? #}
$(document).ready(function(){
{% block iboPageJsInlineOnInit %}
{% for sJsInline in aPage.aJsInlineOnInit %}
{{ sJsInline|raw }}
{% endfor %}
{% endblock %}
{% block iboPageJsInlineOnDomReady %}
{% for sJsInline in aPage.aJsInlineOnDomReady %}
{{ sJsInline|raw }}
{% endfor %}
{% endblock %}
});
</script>
{% block iboPageJsInlineLive %}
{% for sJsInline in aPage.aJsInlineLive %}
{# We put each scripts in a dedicated script tag to prevent massive failure if 1 script is broken (eg. missing semi-colon or non closed comment) #}
<script type="text/javascript">
{{ sJsInline|raw }}
</script>
{% endfor %}
{% endblock %}
{% endblock %}
</body>
</html>