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

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