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:
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user