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:
Molkobain
2021-09-07 10:21:47 +02:00
parent d1a05f41e5
commit f6fbd5a7a5
6 changed files with 318 additions and 16 deletions

View File

@@ -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;
}
}

View File

@@ -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',
));

View File

@@ -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',
));

View File

@@ -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);
}
}
});
});

View File

@@ -224,7 +224,6 @@ $(function()
$('#ibo-breadcrumbs')
.breadcrumbs('destroy')
.html('')
.breadcrumbs({
itop_instance_id: oData['breadcrumb_instance_id'],
max_count: oData['breadcrumb_max_count'],

View File

@@ -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>