From ab681b0954de268dffe1b36d34a66d02d8c856ae Mon Sep 17 00:00:00 2001 From: Molkobain Date: Thu, 16 Jul 2020 14:48:52 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B02847=20-=20Rework=20of=20the=20global=20?= =?UTF-8?q?iTopWebPage=20layout=20(Part=20I)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rework of the iTopWebPage class - Use of TWIG templates - Preliminary work of the navigation menu --- application/applicationextension.inc.php | 4 +- application/dashboard.class.inc.php | 4 +- application/itopwebpage.class.inc.php | 536 ++++++++++-------- application/menunode.class.inc.php | 86 +++ css/backoffice/layout/_navigation-menu.scss | 422 ++++++++++++++ .../layouts/en.dictionary.navigation-menu.php | 27 + images/itop-logo-square.png | Bin 0 -> 2296 bytes js/layouts/navigation-menu.js | 143 +++++ .../layouts/navigation-menu/layout.html.twig | 45 ++ .../navigation-menu/menu-group.html.twig | 4 + .../navigation-menu/menu-node.html.twig | 15 + .../navigation-menu/menu-nodes.html.twig | 8 + templates/pages/backoffice/layout.html.twig | 100 ++++ 13 files changed, 1159 insertions(+), 235 deletions(-) create mode 100644 css/backoffice/layout/_navigation-menu.scss create mode 100644 dictionaries/ui/layouts/en.dictionary.navigation-menu.php create mode 100644 images/itop-logo-square.png create mode 100644 js/layouts/navigation-menu.js create mode 100644 templates/layouts/navigation-menu/layout.html.twig create mode 100644 templates/layouts/navigation-menu/menu-group.html.twig create mode 100644 templates/layouts/navigation-menu/menu-node.html.twig create mode 100644 templates/layouts/navigation-menu/menu-nodes.html.twig create mode 100644 templates/pages/backoffice/layout.html.twig diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index 7b1baa97d..182eb44e8 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -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. * diff --git a/application/dashboard.class.inc.php b/application/dashboard.class.inc.php index 322e68e38..44d9bd49b 100644 --- a/application/dashboard.class.inc.php +++ b/application/dashboard.class.inc.php @@ -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'); } } diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index 330f2aef5..b81496524 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -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( <<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 .= '
'.ExecutionKPI::GetDescription().'
'; + } + + // 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 .= '
'.ExecutionKPI::GetDescription().'
'; - } - - //$sSouthPane = '

Peak memory Usage: '.sprintf('%.3f MB', memory_get_peak_usage(true) / (1024*1024)).'

'; - $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 = "\n"; - $sHtml .= "\n"; - $sHtml .= "\n"; - // Make sure that Internet Explorer renders the page using its latest/highest/greatest standards ! - $sHtml .= "\n"; - $sPageCharset = self::PAGES_CHARSET; - $sHtml .= "\n"; - $sHtml .= "".htmlentities($this->s_title, ENT_QUOTES, $sPageCharset)."\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 .= "\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 .= "\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 .= "\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 .= "\n"; - } + } - if (count($this->a_styles) > 0) - { - $sHtml .= "\n"; - } + + // TODO: Does this still work? $sHtml .= "\n"; - $sHtml .= "\n"; + $sHtml .= "\n"; $sBodyClass = ""; @@ -1178,18 +1261,6 @@ EOF; $sHtml .= "
"; } - // 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 = '
'.$sSouthPane.'
'; } - $sIconUrl = Utils::GetConfig()->Get('app_icon_url'); + // TODO: What do we do with this? $sOnlineHelpUrl = MetaModel::GetConfig()->Get('online_help'); //$sLogOffMenu = ""; @@ -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(); } /** diff --git a/application/menunode.class.inc.php b/application/menunode.class.inc.php index 070204b54..2fb863e64 100644 --- a/application/menunode.class.inc.php +++ b/application/menunode.class.inc.php @@ -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) { diff --git a/css/backoffice/layout/_navigation-menu.scss b/css/backoffice/layout/_navigation-menu.scss new file mode 100644 index 000000000..90ce4972f --- /dev/null +++ b/css/backoffice/layout/_navigation-menu.scss @@ -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; +} \ No newline at end of file diff --git a/dictionaries/ui/layouts/en.dictionary.navigation-menu.php b/dictionaries/ui/layouts/en.dictionary.navigation-menu.php new file mode 100644 index 000000000..7087cff64 --- /dev/null +++ b/dictionaries/ui/layouts/en.dictionary.navigation-menu.php @@ -0,0 +1,27 @@ + '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', +)); \ No newline at end of file diff --git a/images/itop-logo-square.png b/images/itop-logo-square.png new file mode 100644 index 0000000000000000000000000000000000000000..fdea44f0b2aac0075f2eca8945da788ae2755981 GIT binary patch literal 2296 zcmVEX>4Tx04R}tkv&MmKpe$i(`rR34rUN>$WWauh+haUQpF-zC~bvS9ZW9$5Slb3 zDK3tJYr(;f#j1mgv#t)Vf*|+-;^gS0=prTlFDU-$6w^)AA*ywCkPdeof70G~uW%M8;dUMHT~ zv<=St#3C!oD)Bk-h)EYDe&o8+;5W_%mj#|Fn(5>mu}CZx+E{60Rx~x@3F5G->69;I z8mw~O;;fY_ta(rV!cb0MPH~;)AYxcV90^EJP{9UDuo0nEC&faN_M;yD0oR`(mqM-$ zFmlYJ3=OjD2mgcL-CFtau|`rT3Us_U&c_fC+y&}Y$N4^Xocamie+I7fmcLvBrawuq zwzTjO(7O#>T(>l354hX``kxHhlwHYBQ^@Cm_cQvYEYNogbgwpgYwqLp0Z3C@|;fceVHS@0n(QKkT4#*h$pMaR2}S24YJ`L;&3YYXBhk$^@PO000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2jm141~.YP%w000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}000KhNkloa<}*IxkL~Kw$~c0)xR; zHde@lrcT-Cf1c;@6+JuEFMvgW z;r~Ab@M8zK16Wd3DIM5%spbOM;}RIHrh*9qM5GQ`GY{hja{1Tt zxS04?e+5ELEKWeW`@7^Xy-wyYdvNA|3U6&k$=vbt-3*`F1rk)emH1co+Fe;v<9py5MJJ1Uc+kw zJxiu6uma6OPNlJ^NrHgX!7fzoLWI{y^65>W^9ZG^JQxky2kBI6pWfh{iC{u0<52LO6v!lukKIQ2Dc~f|3F~ zmSp6wyFrLT4{4m{<;3sWP2c-%=#gQZc}sAbmLc3)0HSMdBN%v_{0IL=I3eP9|B~Fh zFO$FgFHjQ;)ai?$#Podh>{MrgRa3ppXuzMODjH#dyf7m+VKZ2Asnk;CV=|(N0Id{jJ~!P zJ<^LQh>k!aA`N&qbzuA~>0^)Dy&`}BWUP*+t``Zler}@v> z4`6b`6I8o32)CwO&T<(-*L|G$#wS5*L^y`3Ybmcke>L7sTS-6vFsMmAC0xnKt9uE} zTZPy5t&&OE^DmG(*n#m!5JC`)juTmV$JKLf$YcDyf718Z8jLXr8K(ZupBF8uSpV!{ zhX1$=5t36IT>w6*BM$-;fsLO(PIBKpm>`c(!pei0MU*!N<7e$@K1=G^E?fVKSDrge za{p$G@exW@*nLu3FFW!eB5dPra{nfb&X>Bm2@uM?I#bHEB6r0V6bPY7J=cYBB1kVm za{p$S0;pWkr9z-dJ$FAy2jk~#^BH5w)EW%rF1|{vZ3j#X&XDUlZS5!steP9^6aqu) z&{nIS#s{UY!DghiMjGU-!)RU57;qXEkm!7h^wICo``9`npZGFvYa1%D)H+f6BKb?F z$-dEz9{G@_9mhz&@DP6Q+Xyu&bOye)FFA`mWi&X6>uK2Z%!K!8jjCUW-+vyRNfHeA zk?VOKAwVS-B4h}^|Gi?`k7(B!_x|6l_Fa3(Gl)vjMd?KZ2}Vz`&E6td#&dn`ZbAKf?R|yQuos(teA?XAF7rsv)n; z+ae{iOi*~yx-`295TU3Ic{63QISF}FywZs`;9h?-B3z4m-6H(t#WF9OyT4e|g}g#@ zntXut;v_n|2`yZUlUPoo>jbKy1(OdZYRpy06H=fvA2M{}hXr9ZMYj)<@7RfrOKAMY zK|(F-Ct1=8nzy~gz`i@lfAA(c)639{KPW72QjLMPW(;}LBu|MR?Puu3uJTK+4SdlX zH@SJ6aO1f6zJChsY5#uj)#cUliYu=^-5LU z?sKZ!yULpEq5DCof>~Gp;7kIw)jbfD1#{JN^&~Ik01pF=)d3SFe-Nr1XZ{QL#=CAX S`^_N$0000 literal 0 HcmV?d00001 diff --git a/js/layouts/navigation-menu.js b/js/layouts/navigation-menu.js new file mode 100644 index 000000000..1ff31d882 --- /dev/null +++ b/js/layouts/navigation-menu.js @@ -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); + } + }); +}); diff --git a/templates/layouts/navigation-menu/layout.html.twig b/templates/layouts/navigation-menu/layout.html.twig new file mode 100644 index 000000000..9c4f22cd4 --- /dev/null +++ b/templates/layouts/navigation-menu/layout.html.twig @@ -0,0 +1,45 @@ + + +{# TODO: Move this to a dedicated script file #} + + \ No newline at end of file diff --git a/templates/layouts/navigation-menu/menu-group.html.twig b/templates/layouts/navigation-menu/menu-group.html.twig new file mode 100644 index 000000000..bf04f298f --- /dev/null +++ b/templates/layouts/navigation-menu/menu-group.html.twig @@ -0,0 +1,4 @@ + + + {{ aMenuGroup.sTitle }} + \ No newline at end of file diff --git a/templates/layouts/navigation-menu/menu-node.html.twig b/templates/layouts/navigation-menu/menu-node.html.twig new file mode 100644 index 000000000..04d4d23e6 --- /dev/null +++ b/templates/layouts/navigation-menu/menu-node.html.twig @@ -0,0 +1,15 @@ +
  • + {% if aMenuNode.sUrl is not empty %} + {% set sTarget = (aMenuNode.bOpenInNewWindow == true) ? 'target="_blank"' : '' %} + {{ aMenuNode.sTitle }} + {% else %} + {{ aMenuNode.sTitle }} + {% endif %} + {% if aMenuNode.aChildNodes is defined and aMenuNode.aChildNodes|length > 0 %} +
      + {% for aSubMenuNode in aMenuGroup.aSubMenuNodes %} + {% include 'layouts/navigation-menu/menu-node.html.twig' with aSubMenuNode %} + {% endfor %} +
    + {% endif %} +
  • \ No newline at end of file diff --git a/templates/layouts/navigation-menu/menu-nodes.html.twig b/templates/layouts/navigation-menu/menu-nodes.html.twig new file mode 100644 index 000000000..8afd968a6 --- /dev/null +++ b/templates/layouts/navigation-menu/menu-nodes.html.twig @@ -0,0 +1,8 @@ +
    +

    {{ aMenuGroup.sTitle }}

    +
      + {% for aMenuNode in aMenuGroup.aSubMenuNodes %} + {% include 'layouts/navigation-menu/menu-node.html.twig' with aMenuNode %} + {% endfor %} +
    +
    \ No newline at end of file diff --git a/templates/pages/backoffice/layout.html.twig b/templates/pages/backoffice/layout.html.twig new file mode 100644 index 000000000..55744ad6d --- /dev/null +++ b/templates/pages/backoffice/layout.html.twig @@ -0,0 +1,100 @@ + + + + + {# This block can be used to add your own meta tags by extending the default template #} + {% block iboPageExtraMetas %} + {% endblock %} + + {{ aPage.sTitle }} + + + {# 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'] != '' %}{% 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 %} + + {% endfor %} + {% endblock %} + + {% block iboPageJsFiles %} + {% for sJsFile in aPage.aJsFiles %} + + {% endfor %} + {% endblock %} + + + {% include 'layouts/navigation-menu/layout.html.twig' with aLayouts.aNavigationMenu %} +
    + +
    +
    item
    +
    +itemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitemitem +
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    item
    +
    +
    + + {% block iboPageJsInlineScripts %} + + + {% 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) #} + + {% endfor %} + {% endblock %} + {% endblock %} + + \ No newline at end of file