mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
N°3900 - Breadcrumbs: Improve behavior when items are too many for the screen width
When the screen isn't large enough we now put the oldest entries in a dropdown menu like on a browser to access the previous pages.
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
$ibo-breadcrumbs--margin-right: 16px !default;
|
||||
|
||||
$ibo-breadcrumbs--item--text-color: $ibo-color-grey-800 !default;
|
||||
|
||||
$ibo-breadcrumbs--item-icon--margin-x: 8px !default;
|
||||
@@ -27,9 +29,27 @@ $ibo-breadcrumbs--item-label--max-width: 100px !default;
|
||||
$ibo-breadcrumbs--item-separator--margin-x: 12px !default;
|
||||
$ibo-breadcrumbs--item-separator--text-color: $ibo-color-grey-500 !default;
|
||||
|
||||
$ibo-breadcrumbs--previous-items-list-toggler--text-color: $ibo-color-grey-700 !default;
|
||||
$ibo-breadcrumbs--previous-items-list-toggler--margin-right: 2 * $ibo-breadcrumbs--item-separator--margin-x !default;
|
||||
|
||||
$ibo-breadcrumbs--previous-items-list--top: 37px !default;
|
||||
$ibo-breadcrumbs--previous-items-list--padding-y: 8px !default;
|
||||
$ibo-breadcrumbs--previous-items-list--background-color: $ibo-color-white-100 !default;
|
||||
|
||||
$ibo-breadcrumbs--previous-item--text-color: $ibo-breadcrumbs--item--text-color !default;
|
||||
$ibo-breadcrumbs--previous-item--padding-x: 12px !default;
|
||||
$ibo-breadcrumbs--previous-item--padding-y: 12px !default;
|
||||
$ibo-breadcrumbs--previous-item-label--max-width: 200px !default;
|
||||
|
||||
.ibo-breadcrumbs{
|
||||
position: relative;
|
||||
margin-right: $ibo-breadcrumbs--margin-right;
|
||||
@extend %ibo-full-height-content;
|
||||
|
||||
&.ibo-is-overflowing {
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
* {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -40,13 +60,6 @@ $ibo-breadcrumbs--item-separator--text-color: $ibo-color-grey-500 !default;
|
||||
@extend %ibo-font-ral-med-100;
|
||||
|
||||
&:not(:last-child){
|
||||
&::after{
|
||||
content: '\f054';
|
||||
margin: 0 $ibo-breadcrumbs--item-separator--margin-x;
|
||||
color: $ibo-breadcrumbs--item-separator--text-color;
|
||||
@extend %fa-solid-base;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
.ibo-breadcrumbs--item-icon{
|
||||
> *{
|
||||
@@ -78,3 +91,50 @@ $ibo-breadcrumbs--item-separator--text-color: $ibo-color-grey-500 !default;
|
||||
max-width: $ibo-breadcrumbs--item-label--max-width;
|
||||
@extend %ibo-text-truncated-with-ellipsis;
|
||||
}
|
||||
|
||||
.ibo-breadcrumbs--item,
|
||||
.ibo-breadcrumbs--previous-items-list-toggler {
|
||||
&:not(:last-child){
|
||||
&::after{
|
||||
content: '\f054';
|
||||
margin: 0 $ibo-breadcrumbs--item-separator--margin-x;
|
||||
color: $ibo-breadcrumbs--item-separator--text-color;
|
||||
@extend %fa-solid-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-breadcrumbs--previous-items-list-toggler {
|
||||
margin-right: $ibo-breadcrumbs--previous-items-list-toggler--margin-right;
|
||||
color: $ibo-breadcrumbs--previous-items-list-toggler--text-color !important;
|
||||
@extend %ibo-font-ral-med-100;
|
||||
|
||||
&:not(:last-child) {
|
||||
&::after {
|
||||
position: absolute; /* To be outside of the button */
|
||||
right: -1 * $ibo-breadcrumbs--previous-items-list-toggler--margin-right;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ibo-breadcrumbs--previous-items-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch; /* For the items to occupy the full width */
|
||||
|
||||
position: fixed;
|
||||
top: $ibo-breadcrumbs--previous-items-list--top;
|
||||
padding: $ibo-breadcrumbs--previous-items-list--padding-y 0;
|
||||
|
||||
background-color: $ibo-breadcrumbs--previous-items-list--background-color;
|
||||
@extend %ibo-elevation-300;
|
||||
}
|
||||
|
||||
.ibo-breadcrumbs--previous-item {
|
||||
color: $ibo-breadcrumbs--previous-item--text-color;
|
||||
@extend %ibo-font-ral-med-100;
|
||||
padding: $ibo-breadcrumbs--previous-item--padding-y $ibo-breadcrumbs--previous-item--padding-x;
|
||||
|
||||
.ibo-breadcrumbs--item-label {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2013-2021 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
|
||||
*/
|
||||
|
||||
// Global search
|
||||
Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Component:Breadcrumbs:PreviousItemsListToggler:Label' => 'Previous pages',
|
||||
));
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2013-2021 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
|
||||
*/
|
||||
|
||||
// Global search
|
||||
Dict::Add('FR FR', 'French', 'Français', array(
|
||||
'UI:Component:Breadcrumbs:PreviousItemsListToggler:Label' => 'Pages précédentes',
|
||||
));
|
||||
@@ -30,6 +30,26 @@ $(function()
|
||||
new_entry: null,
|
||||
max_count: 8
|
||||
},
|
||||
css_classes:
|
||||
{
|
||||
is_hidden: 'ibo-is-hidden',
|
||||
is_transparent: 'ibo-is-transparent',
|
||||
is_opaque: 'ibo-is-opaque',
|
||||
is_overflowing: 'ibo-is-overflowing',
|
||||
breadcrumbs_item: 'ibo-breadcrumbs--item',
|
||||
breadcrumbs_previous_item: 'ibo-breadcrumbs--previous-item',
|
||||
},
|
||||
js_selectors:
|
||||
{
|
||||
breadcrumbs: '[data-role="ibo-breadcrumbs"]',
|
||||
item: '[data-role="ibo-breadcrumbs--item"]',
|
||||
previous_items_container: '[data-role="ibo-breadcrumbs--previous-items-container"]',
|
||||
previous_items_list_toggler: '[data-role="ibo-breadcrumbs--previous-items-list-toggler"]',
|
||||
previous_items_list: '[data-role="ibo-breadcrumbs--previous-items-list"]',
|
||||
previous_item: '[data-role="ibo-breadcrumbs--previous-item"]',
|
||||
},
|
||||
|
||||
items_observer: null,
|
||||
|
||||
// the constructor
|
||||
_create: function()
|
||||
@@ -41,7 +61,7 @@ $(function()
|
||||
// Check that storage API is available
|
||||
if(typeof(Storage) !== 'undefined')
|
||||
{
|
||||
$(window).bind('hashchange', function(e)
|
||||
$(window).on('hashchange', function(e)
|
||||
{
|
||||
me.RefreshLatestEntry();
|
||||
});
|
||||
@@ -50,7 +70,7 @@ $(function()
|
||||
|
||||
if (this.options.new_entry !== null) {
|
||||
var sUrl = this.options.new_entry.url;
|
||||
if (sUrl.length == 0) {
|
||||
if (sUrl.length === 0) {
|
||||
sUrl = window.location.href;
|
||||
}
|
||||
// Eliminate items having the same id, before appending the new item
|
||||
@@ -71,6 +91,8 @@ $(function()
|
||||
}
|
||||
this._writeDataToStorage(aBreadCrumb);
|
||||
|
||||
// Build markup
|
||||
// - Add entries to the markup
|
||||
for (iEntry in aBreadCrumb)
|
||||
{
|
||||
var sBreadcrumbsItemHtml = '';
|
||||
@@ -90,30 +112,131 @@ $(function()
|
||||
|
||||
var sTitle = oEntry['description'],
|
||||
sLabel = oEntry['label'];
|
||||
if (sTitle.length == 0) {
|
||||
if (sTitle.length === 0) {
|
||||
sTitle = sLabel;
|
||||
}
|
||||
sTitle = EncodeHtml(sTitle, false);
|
||||
sLabel = EncodeHtml(sLabel, false);
|
||||
|
||||
if ((this.options.new_entry !== null) && (iEntry == aBreadCrumb.length - 1)) {
|
||||
if ((this.options.new_entry !== null) && (iEntry === aBreadCrumb.length - 1)) {
|
||||
// Last entry is the current page
|
||||
sBreadcrumbsItemHtml += '<span class="ibo-breadcrumbs--item--is-current" data-breadcrumb-entry-number="'+iEntry+'" title="'+sTitle+'">'+sIconSpec+'<span class="ibo-breadcrumbs--item-label">'+sLabel+'</span></span>';
|
||||
sBreadcrumbsItemHtml += '<span class="ibo-is-current" data-role="" data-breadcrumb-entry-number="'+iEntry+'" title="'+sTitle+'">'+sIconSpec+'<span class="ibo-breadcrumbs--item-label">'+sLabel+'</span></span>';
|
||||
} else {
|
||||
var sSanitizedUrl = StripArchiveArgument(oEntry['url']);
|
||||
sBreadcrumbsItemHtml += '<a class="ibo-breadcrumbs--item" data-breadcrumb-entry-number="'+iEntry+'" href="'+sSanitizedUrl+'" title="'+sTitle+'">'+sIconSpec+'<span class="ibo-breadcrumbs--item-label">'+sLabel+'</span></a>';
|
||||
sBreadcrumbsItemHtml += '<a class="" data-role="" data-breadcrumb-entry-number="'+iEntry+'" href="'+sSanitizedUrl+'" title="'+sTitle+'">'+sIconSpec+'<span class="ibo-breadcrumbs--item-label">'+sLabel+'</span></a>';
|
||||
}
|
||||
}
|
||||
this.element.append(sBreadcrumbsItemHtml);
|
||||
|
||||
const oNormalItemElem = $(sBreadcrumbsItemHtml)
|
||||
.addClass(this.css_classes.breadcrumbs_item)
|
||||
.attr('data-role', 'ibo-breadcrumbs--item');
|
||||
this.element.append(oNormalItemElem);
|
||||
|
||||
const oPreviousItemElem = $(sBreadcrumbsItemHtml)
|
||||
.addClass(this.css_classes.breadcrumbs_previous_item)
|
||||
.attr('data-role', 'ibo-breadcrumbs--previous-item')
|
||||
// Note: We prepend items as we want the oldest to be at the bottom of the list, like in a browser
|
||||
this.element.find(this.js_selectors.previous_items_list).prepend(oPreviousItemElem);
|
||||
}
|
||||
}
|
||||
|
||||
this._updateOverflowingState();
|
||||
this._bindEvents();
|
||||
},
|
||||
// events bound via _bind are removed automatically
|
||||
// revert other modifications here
|
||||
_destroy: function()
|
||||
{
|
||||
// Remove listeners
|
||||
this.element.find(this.js_selectors.previous_items_list_toggler).off('click');
|
||||
|
||||
// Remove observers
|
||||
this.items_observer.disconnect();
|
||||
|
||||
// Clear any existing entries in the markup
|
||||
this.element.find(this.js_selectors.item).remove();
|
||||
this.element.find(this.js_selectors.previous_item).remove();
|
||||
|
||||
this.element.removeClass('ibo-breadcrumbs');
|
||||
},
|
||||
_bindEvents: function ()
|
||||
{
|
||||
const me = this;
|
||||
|
||||
// Enable responsiveness if more than 1 item
|
||||
if(window.IntersectionObserver && (this.element.find(this.js_selectors.item).length > 1)) {
|
||||
// Set an observer on the items
|
||||
this.items_observer = new IntersectionObserver(function(aItems, oIntersectObs){
|
||||
aItems.forEach(oItem => {
|
||||
let oItemElem = $(oItem.target);
|
||||
let bIsVisible = oItem.isIntersecting;
|
||||
|
||||
// Important: We toggle "visibility" instead of "display" otherwise once they are hidden, they never trigger back the intersection.
|
||||
if(bIsVisible) {
|
||||
oItemElem.css('visibility', '');
|
||||
}
|
||||
else {
|
||||
// Here we also check if the item has an invisible left sibbling before hiding it.
|
||||
// There reason is that on initialization, the last item might be overflowing on the right BEFORE the breadcrumbs is flagged as overflowing, making it disappear
|
||||
let oLeftSiblingElem = oItemElem.prev(me.js_selectors.item);
|
||||
if (oLeftSiblingElem.length > 0 && oLeftSiblingElem.css('visibility') !== 'hidden') {
|
||||
bIsVisible = true;
|
||||
} else {
|
||||
oItemElem.css('visibility', 'hidden');
|
||||
}
|
||||
}
|
||||
me._updateItemDisplay(oItemElem, bIsVisible);
|
||||
});
|
||||
|
||||
let bShouldShowPreviousItemsList = false;
|
||||
me.element.find(me.js_selectors.item).each(function() {
|
||||
if ($(this).css('visibility') === 'hidden') {
|
||||
bShouldShowPreviousItemsList = true;
|
||||
|
||||
// Note: Can break a .each function loop, must return false
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Move previous items toggler before first visible item for a better UX
|
||||
if (bShouldShowPreviousItemsList) {
|
||||
let oFirstVisibleItem = me.element.find(me.js_selectors.item).first();
|
||||
me.element.find(me.js_selectors.item).each(function() {
|
||||
if ($(this).css('visibility') !== 'hidden') {
|
||||
oFirstVisibleItem = $(this);
|
||||
|
||||
// Note: Can break a .each function loop, must return false
|
||||
return false;
|
||||
}
|
||||
});
|
||||
me.element.find(me.js_selectors.previous_items_container).insertBefore(oFirstVisibleItem);
|
||||
}
|
||||
|
||||
me._updateOverflowingState();
|
||||
me._updatePreviousItemsList();
|
||||
}, {
|
||||
root: $(this.js_selectors.breadcrumbs)[0],
|
||||
threshold: [1] // Must be completely visible
|
||||
});
|
||||
this.element.find(this.js_selectors.item).each(function(){
|
||||
me.items_observer.observe(this);
|
||||
});
|
||||
|
||||
this.element.find(this.js_selectors.previous_items_list_toggler).on('click', function (oEvent) {
|
||||
oEvent.preventDefault();
|
||||
me.element.find(me.js_selectors.previous_items_list).toggleClass(me.css_classes.is_hidden);
|
||||
});
|
||||
$('body').on('click', function (oEvent) {
|
||||
if (true === me.element.find(me.js_selectors.previous_items_list).hasClass(me.css_classes.is_hidden)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($(oEvent.target.closest(me.js_selectors.previous_items_container)).length === 0) {
|
||||
me.element.find(me.js_selectors.previous_items_list).addClass(me.css_classes.is_hidden);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
_readDataFromStorage: function()
|
||||
{
|
||||
var sBreadCrumbStorageKey = this.options.itop_instance_id + 'breadcrumb-v1';
|
||||
@@ -131,6 +254,7 @@ $(function()
|
||||
sBreadCrumbData = JSON.stringify(aBreadCrumb);
|
||||
sessionStorage.setItem(sBreadCrumbStorageKey, sBreadCrumbData);
|
||||
},
|
||||
|
||||
// Refresh the latest entry (navigating to a tab)
|
||||
RefreshLatestEntry: function(sRefreshHrefTo)
|
||||
{
|
||||
@@ -149,5 +273,68 @@ $(function()
|
||||
}
|
||||
this._writeDataToStorage(aBreadCrumb);
|
||||
},
|
||||
|
||||
// Helpers
|
||||
/**
|
||||
* Update item display based on its visibility to the user
|
||||
*
|
||||
* @param oItemElem {Object} jQuery element
|
||||
* @param bIsVisible {boolean|null} If null, visibility will be computed automatically. Not that performance might not be great so it's preferable to pass the value when known
|
||||
* @return {void}
|
||||
* @private
|
||||
*/
|
||||
_updateItemDisplay(oItemElem, bIsVisible = null)
|
||||
{
|
||||
const iEntryNumber = parseInt(oItemElem.attr('data-breadcrumb-entry-number'));
|
||||
const oMatchingExtraItemElem = this.element.find(this.js_selectors.previous_items_list+' [data-breadcrumb-entry-number="'+iEntryNumber+'"]');
|
||||
|
||||
// Manually check if the item is visible if the info isn't passed
|
||||
if (bIsVisible === null) {
|
||||
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oItemElem[0], true, 2);
|
||||
}
|
||||
|
||||
// Hide/show the corresponding extra item element
|
||||
if (bIsVisible) {
|
||||
oMatchingExtraItemElem.addClass(this.css_classes.is_hidden);
|
||||
} else {
|
||||
oMatchingExtraItemElem.removeClass(this.css_classes.is_hidden);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Update previous items list
|
||||
*
|
||||
* @return {void}
|
||||
* @private
|
||||
*/
|
||||
_updatePreviousItemsList: function () {
|
||||
const iVisiblePreviousItemsCount = this.element.find(this.js_selectors.previous_item+':not(.'+this.css_classes.is_hidden+')').length;
|
||||
const oPreviousItemsContainerElem = this.element.find(this.js_selectors.previous_items_container);
|
||||
|
||||
if (iVisiblePreviousItemsCount > 0) {
|
||||
oPreviousItemsContainerElem.removeClass(this.css_classes.is_hidden);
|
||||
} else {
|
||||
oPreviousItemsContainerElem.addClass(this.css_classes.is_hidden);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Update the overflowing state of the breadcrumbs by checking if the items cumulated width is greater than the breadcrumbs visible space
|
||||
*
|
||||
* @return {void}
|
||||
* @private
|
||||
*/
|
||||
_updateOverflowingState: function () {
|
||||
const fBreadcrumbsWidth = this.element.outerWidth();
|
||||
let fItemsTotalWidth = 0;
|
||||
|
||||
this.element.find(this.js_selectors.item).each(function () {
|
||||
fItemsTotalWidth += $(this).outerWidth();
|
||||
});
|
||||
|
||||
if (fItemsTotalWidth > fBreadcrumbsWidth) {
|
||||
this.element.addClass(this.css_classes.is_overflowing);
|
||||
} else {
|
||||
this.element.removeClass(this.css_classes.is_overflowing);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,7 +224,6 @@ $(function()
|
||||
|
||||
$('#ibo-breadcrumbs')
|
||||
.breadcrumbs('destroy')
|
||||
.html('')
|
||||
.breadcrumbs({
|
||||
itop_instance_id: oData['breadcrumb_instance_id'],
|
||||
max_count: oData['breadcrumb_max_count'],
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
<div id="{{ oUIBlock.GetId() }}" class="ibo-breadcrumbs"></div>
|
||||
<div id="{{ oUIBlock.GetId() }}" class="ibo-breadcrumbs">
|
||||
<span class="ibo-breadcrumbs--previous-items-container ibo-is-hidden" data-role="ibo-breadcrumbs--previous-items-container">
|
||||
<button class="ibo-breadcrumbs--previous-items-list-toggler ibo-button ibo-is-alternative ibo-is-neutral" data-role="ibo-breadcrumbs--previous-items-list-toggler"
|
||||
aria-label="{{ 'UI:Component:Breadcrumbs:PreviousItemsListToggler:Label'|dict_s }}"
|
||||
data-tooltip-content="{{ 'UI:Component:Breadcrumbs:PreviousItemsListToggler:Label'|dict_s }}"
|
||||
>
|
||||
<span class="fas fa-ellipsis-h"></span>
|
||||
</button>
|
||||
<div class="ibo-breadcrumbs--previous-items-list ibo-is-hidden" data-role="ibo-breadcrumbs--previous-items-list"></div>
|
||||
</span>
|
||||
</div>
|
||||
Reference in New Issue
Block a user