diff --git a/css/backoffice/layout/_content.scss b/css/backoffice/layout/_content.scss index dd35518d40..f1cedb76bf 100644 --- a/css/backoffice/layout/_content.scss +++ b/css/backoffice/layout/_content.scss @@ -22,7 +22,7 @@ #ibo-main-content{ flex-grow: 1; /* To occupy maximum width, side content will handle its width */ - overflow-x: auto; /* To avoid main content to be too wide when the blocks within it have no width constraints. This way it will occupy only the remaining space left by the side part */ + overflow-x: auto; /* To avoid main content to be too wide when the blocks within it have no width constraints. This way it will occupy only the remaining space left by the side part. */ } } diff --git a/css/backoffice/layout/tab-container/_tab-container.scss b/css/backoffice/layout/tab-container/_tab-container.scss index d642e78d83..256a008f19 100644 --- a/css/backoffice/layout/tab-container/_tab-container.scss +++ b/css/backoffice/layout/tab-container/_tab-container.scss @@ -19,8 +19,23 @@ $ibo-tab-container--tab-toggler--padding-x: 24px !default; $ibo-tab-container--tab-container--padding-x: 32px !default; $ibo-tab-container--tab-container--padding-y: 32px !default; +$ibo-tab-container--extra-tabs-container--background-color: $ibo-tab-container--tabs-list--background-color !default; + +$ibo-tab-container--extra-tabs-list-toggler--padding-x: 12px !default; + +$ibo-tab-container--extra-tabs-list--max-height: 300px !default; +$ibo-tab-container--extra-tabs-list--border-radius: $ibo-border-radius-300; +$ibo-tab-container--extra-tabs-list--background-color: $ibo-tab-container--tabs-list--background-color !default; + +$ibo-tab-container--extra-tab-toggler--padding: 8px 16px !default; +$ibo-tab-container--extra-tab-toggler--max-width: 220px !default; +$ibo-tab-container--extra-tab-toggler--text-color: $ibo-color-grey-700 !default; +$ibo-tab-container--extra-tab-toggler--text-color--on-hover: $ibo-color-blue-800 !default; +$ibo-tab-container--extra-tab-toggler--background-color--on-hover: $ibo-color-grey-200 !default; + /* Rules */ .ibo-tab-container--tabs-list { + position: relative; @extend %ibo-full-height-content; height: $ibo-tab-container--tabs-list--height; @@ -47,11 +62,47 @@ $ibo-tab-container--tab-container--padding-y: 32px !default; padding-right: $ibo-tab-container--tab-toggler--padding-x; @extend %ibo-text-truncated-with-ellipsis; - color: inherit; /* To get color from parent */ + @extend %ibo-hyperlink-inherited-colors +} +.ibo-tab-container--extra-tabs-container{ + @extend .ibo-tab-container--tab-header; + + position: absolute; + top: 0; + bottom: 0; + right: 0; + + background-color: $ibo-tab-container--extra-tabs-container--background-color; +} +.ibo-tab-container--extra-tabs-list-toggler{ + @extend .ibo-tab-container--tab-toggler; + + padding-left: $ibo-tab-container--extra-tabs-list-toggler--padding-x; + padding-right: $ibo-tab-container--extra-tabs-list-toggler--padding-x; +} +.ibo-tab-container--extra-tabs-list{ + position: absolute; + top: calc(100% + 6px); + right: 12px; + max-height: $ibo-tab-container--extra-tabs-list--max-height; + display: flex; + flex-direction: column; + + background-color: $ibo-tab-container--extra-tabs-list--background-color; + border-radius: $ibo-tab-container--extra-tabs-list--border-radius; + @extend %ibo-elevation-100; +} +.ibo-tab-container--extra-tab-toggler{ + padding: $ibo-tab-container--extra-tab-toggler--padding; + max-width: $ibo-tab-container--extra-tab-toggler--max-width; + + color: $ibo-tab-container--extra-tab-toggler--text-color; + @extend %ibo-text-truncated-with-ellipsis; &:hover, &:active{ - color: inherit; + color: $ibo-tab-container--extra-tab-toggler--text-color--on-hover; + background-color: $ibo-tab-container--extra-tab-toggler--background-color--on-hover; } } diff --git a/js/layouts/tab-container.js b/js/layouts/tab-container.js index 26c0a3c2e9..88c3cdb140 100644 --- a/js/layouts/tab-container.js +++ b/js/layouts/tab-container.js @@ -1,3 +1,20 @@ +/* + * 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() { @@ -9,12 +26,17 @@ $(function() }, css_classes: { + is_hidden: 'ibo-is-hidden', }, js_selectors: { tabs_list: '[data-role="ibo-tab-container--tabs-list"]', tab_header: '[data-role="ibo-tab-container--tab-header"]', tab_toggler: '[data-role="ibo-tab-container--tab-toggler"]', + extra_tabs_container: '[data-role="ibo-tab-container--extra-tabs-container"]', + extra_tabs_list_toggler: '[data-role="ibo-tab-container--extra-tabs-list-toggler"]', + extra_tabs_list: '[data-role="ibo-tab-container--extra-tabs-list"]', + extra_tab_toggler: '[data-role="ibo-tab-container--extra-tab-toggler"]', }, // the constructor @@ -69,13 +91,47 @@ $(function() $(window).on('hashchange', function(){ me._onHashChange(); }); - // Define our own click handler for the tabs, overriding the default. + // Click on tab togglers this.element.find(this.js_selectors.tab_toggler).on('click', function(){ - me._onTogglerClick($(this)); + me._onTabTogglerClick($(this)); + }); + // Resize of the tab container + if(window.ResizeObserver) + { + const oTabsListRO = new ResizeObserver(function(){ + // Note: For a reason I don't understand, when called instantly the sub function IsElementVisibleToTheUser() won't be able to retrieve an element using the document.elementFromPoint() function + // As it won't return anything, the function always thinks it's invisible... + setTimeout(function(){ + me._onTabContainerResize(); + }, 200); + + }); + oTabsListRO.observe($('.ibo-tab-container--tabs-list')[0]); + } + // Click on extra tabs list toggler + this.element.find(this.js_selectors.extra_tabs_list_toggler).on('click', function(oEvent){ + me._onExtraTabsListTogglerClick($(this), oEvent); + }); + // Click on "extra tab togglers" + this.element.find(this.js_selectors.extra_tab_toggler).on('click', function(oEvent){ + me._onExtraTabTogglerClick($(this), oEvent); + }); + // Mostly for outside clicks that should close elements + $('body').on('click', function(oEvent){ + me._onBodyClick(oEvent); }); }, // Events callbacks + // - Update tab headers display on container resize + _onTabContainerResize: function() + { + const me = this; + this.element.find(this.js_selectors.tab_header).each(function(){ + me._updateTabHeaderDisplay($(this)); + }); + this._updateExtraTabsList(); + }, // - Update URL hash when tab is activated _onTabActivated: function(oUI) { @@ -120,7 +176,8 @@ $(function() } }); }, - _onTogglerClick: function(oTabHeaderElem) + // - Define our own click handler for the tabs, overriding the default. + _onTabTogglerClick: function(oTabHeaderElem) { if ($.bbq) { let oState = {}; @@ -135,6 +192,70 @@ $(function() oState[sId] = iIdx; $.bbq.pushState(oState); } + }, + // - Forward click event to real tab toggler + _onExtraTabTogglerClick: function(oExtraTabTogglerElem, oEvent) + { + // Prevent anchor default behaviour + oEvent.preventDefault(); + + // Trigger click event on real tab toggler (the hidden one) + const sTargetTabId = oExtraTabTogglerElem.attr('href').replace(/#/, ''); + this.element.find(this.js_selectors.tab_header+'[data-tab-id="'+sTargetTabId+'"] '+this.js_selectors.tab_toggler).trigger('click'); + }, + // - Toggle extra tabs list + _onExtraTabsListTogglerClick: function(oElem, oEvent) + { + // Prevent anchor default behaviour + oEvent.preventDefault(); + + // TODO 2.8.0: Should/could we use a popover menu instead here? + this.element.find(this.js_selectors.extra_tabs_list).toggleClass(this.css_classes.is_hidden); + }, + _onBodyClick: function(oEvent) + { + // Close extra tabs list if opened + if($(oEvent.target.closest(this.js_selectors.extra_tabs_container)).length === 0) + { + this.element.find(this.js_selectors.extra_tabs_list).addClass(this.css_classes.is_hidden); + } + }, + + // Helpers + /** + * Update tab header display based on its visibility to the user + * + * @param oTabHeaderElem jQuery element + * @private + */ + _updateTabHeaderDisplay(oTabHeaderElem) + { + const sTabId = oTabHeaderElem.attr('data-tab-id'); + const oMatchingExtraTabElem = this.element.find(this.js_selectors.extra_tab_toggler+'[href="#'+sTabId+'"]'); + + if(!IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2)) + { + oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden); + } + else + { + oMatchingExtraTabElem.addClass(this.css_classes.is_hidden); + } + }, + // - Update extra tabs list + _updateExtraTabsList: function() + { + const iVisibleExtraTabsCount = this.element.find(this.js_selectors.extra_tab_toggler+':not(.'+this.css_classes.is_hidden+')').length; + const oExtraTabsContainerElem = this.element.find(this.js_selectors.extra_tabs_container); + + if(iVisibleExtraTabsCount > 0) + { + oExtraTabsContainerElem.removeClass(this.css_classes.is_hidden); + } + else + { + oExtraTabsContainerElem.addClass(this.css_classes.is_hidden); + } } }); }); diff --git a/templates/layouts/tab-container/layout.html.twig b/templates/layouts/tab-container/layout.html.twig index b25550094a..c2e78117dc 100644 --- a/templates/layouts/tab-container/layout.html.twig +++ b/templates/layouts/tab-container/layout.html.twig @@ -5,16 +5,26 @@