N°3560 Allow to scroll vertically through tabs content. User can activate this feature in preference page

This commit is contained in:
Stephen Abello
2021-01-15 11:02:39 +01:00
parent 442e9598f8
commit 137b4e55c4
9 changed files with 277 additions and 13 deletions

View File

@@ -12,11 +12,16 @@ $ibo-panel-with-tab-container--tab-container--margin-x: -1 * $ibo-panel--body--p
> .ibo-panel--body {
> .ibo-tab-container {
margin-top: $ibo-panel-with-tab-container--body--padding-top;
height: 80vh;
> .ibo-tab-container--tabs-list{
margin-left: $ibo-panel-with-tab-container--tabs-list--margin-x;
margin-right: $ibo-panel-with-tab-container--tabs-list--margin-x;
}
> .ibo-tab-container--tab-container-list{
height: 100%;
overflow-y: auto;
}
> .ibo-tab-container--tab-container{
margin-left: $ibo-panel-with-tab-container--tab-container--margin-x;
margin-right: $ibo-panel-with-tab-container--tab-container--margin-x;

View File

@@ -26,4 +26,8 @@ Dict::Add('EN US', 'English', 'English', array(
'UI:RichText:ToolbarState' => 'Toolbar default state',
'UI:RichText:ToolbarState:Expanded' => 'Expanded',
'UI:RichText:ToolbarState:Collapsed' => 'Collapsed',
));
'UI:Tabs:Preferences' => 'Tabs',
'UI:Tabs:Scrollable:Label' => 'Tabs navigation',
'UI:Tabs:Scrollable:Classic' => 'Classic',
'UI:Tabs:Scrollable:Scrollable' => 'Scrollable',
));

2
js/ScrollMagic.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,178 @@
$.widget( "itop.scrollabletabs", $.ui.tabs, {
js_selectors:
{
tab_toggler: '[data-role="ibo-tab-container--tab-toggler"]',
tab_container_list: '[data-role="ibo-tab-container--tab-container-list"]'
},
controller: null,
_create: function() {
var me = this;
// Initialize a single controller for this tab container
this.controller = new ScrollMagic.Controller({'container': '#' + this.element.find(this.js_selectors.tab_container_list).attr('id'), 'refreshInterval' : 200});
// Add remote tabs to controller after they are loaded
var afterloadajax = function (a, b)
{
me._newScene(b.tab, b.panel).addTo(me.controller);
};
this.element.on('scrollabletabsload', afterloadajax);
this._super();
// Load remote tabs as soon as possible
$(this.js_selectors.tab_toggler).each(function() {
var that = this;
if($(that).attr('href').charAt(0) !== '#') {
var index = $(this).parent('li').prevAll().length
me.load(index);
}
});
// Add every other tab to the controller
$(this.js_selectors.tab_toggler).each(function(){
var that = this;
if($(that).attr('href').charAt(0) === '#') {
me._newScene($(that).parent('li'), $($(that).attr('href'))).addTo(me.controller);
}
});
},
// Create a new scene to be added to the controller
_newScene: function(tab, panel)
{
var me = this;
var iPanelId = panel.attr('id');
return new ScrollMagic.Scene({
triggerElement: '#' + iPanelId,
triggerHook: 0.03, // show, when scrolled 10% into view
duration: function () {
return $('#' + iPanelId).outerHeight();
}
})
.on("enter", function (event) {
$(tab).addClass("ui-tabs-active ui-state-active");
$(tab).siblings('li').removeClass("ui-tabs-active ui-state-active");
me.setTab($(tab));
me.element.trigger('tabscrolled', [{'newTab': $(tab)}]);
})
.on("leave", function (event){
$(tab).removeClass("ui-tabs-active ui-state-active");
})
},
// jQuery UI overload
_refresh: function() {
this._setOptionDisabled( this.options.disabled );
this._setupEvents( this.options.event );
this._setupHeightStyle( this.options.heightStyle );
this.tabs.not( this.active ).attr( {
"aria-selected": "false",
"aria-expanded": "false",
tabIndex: -1
} );
this.panels.not( this._getPanelForTab( this.active ) )
// jQuery UI overload : Do NOT hide panels
//.hide()
.attr( {
"aria-hidden": "true"
} );
// Make sure one tab is in the tab order
if ( !this.active.length ) {
this.tabs.eq( 0 ).attr( "tabIndex", 0 );
} else {
this.active
.attr( {
"aria-selected": "true",
"aria-expanded": "true",
tabIndex: 0
} );
this._addClass( this.active, "ui-tabs-active", "ui-state-active" );
this._getPanelForTab( this.active )
.show()
.attr( {
"aria-hidden": "false"
} );
}
},
// jQuery UI overload
// Handles show/hide for selecting tabs
_toggle: function( event, eventData ) {
var that = this,
toShow = eventData.newPanel,
toHide = eventData.oldPanel;
this.running = true;
function complete() {
that.running = false;
// We don't want to trigger activate event in this mode
//that._trigger( "activate", event, eventData );
}
function show() {
// jQuery UI overload
//Showing a tab here equals to scrolling to its content.
//Enter/leave events on scenes handle the active/inactive classes on tabs
//that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" );
if ( toShow.length && that.options.show ) {
//that._show( toShow, that.options.show, complete );
that.controller.scrollTo('#' + $(toShow).attr('id'));
} else {
that.controller.scrollTo('#' + $(toShow).attr('id'));
// toShow.show();
complete();
}
}
// jQuery UI overload
// We just want to scroll to the new tab with our "show" function, nothing more
// Start out by hiding, then showing, then completing
// if ( toHide.length && this.options.hide ) {
// this._hide( toHide, this.options.hide, function() {
// that._removeClass( eventData.oldTab.closest( "li" ),
// "ui-tabs-active", "ui-state-active" );
// show();
// } );
// } else {
// this._removeClass( eventData.oldTab.closest( "li" ),
// "ui-tabs-active", "ui-state-active" );
// toHide.hide();
// show();
// }
//this._removeClass( eventData.oldTab.closest( "li" ), "ui-tabs-active", "ui-state-active" );
show();
toHide.attr( "aria-hidden", "true" );
eventData.oldTab.attr( {
"aria-selected": "false",
"aria-expanded": "false"
} );
// If we're switching tabs, remove the old tab from the tab order.
// If we're opening from collapsed state, remove the previous tab from the tab order.
// If we're collapsing, then keep the collapsing tab in the tab order.
if ( toShow.length && toHide.length ) {
eventData.oldTab.attr( "tabIndex", -1 );
} else if ( toShow.length ) {
this.tabs.filter( function() {
return $( this ).attr( "tabIndex" ) === 0;
} )
.attr( "tabIndex", -1 );
}
toShow.attr( "aria-hidden", "false" );
eventData.newTab.attr( {
"aria-selected": "true",
"aria-expanded": "true",
tabIndex: 0
} );
},
// Set the current tab information
setTab : function(tab){
this.active = tab;
},
});

View File

@@ -27,6 +27,7 @@ $(function()
css_classes:
{
is_hidden: 'ibo-is-hidden',
is_scrollable: 'ibo-is-scrollable'
},
js_selectors:
{
@@ -42,7 +43,8 @@ $(function()
// the constructor
_create: function()
{
this.element.addClass('ibo-tab-container');
var me = this;
this.element.addClass('ibo-tab-container');
// Ugly patch for a change in the behavior of jQuery UI:
// Before jQuery UI 1.9, tabs were always considered as "local" (opposed to Ajax)
@@ -65,13 +67,22 @@ $(function()
// that the tabs aren't changed on click, and any custom event name can be
// specified. Note that if you define a callback for the 'select' event, it
// will be executed for the selected tab whenever the hash changes.
this.element.tabs({event: 'change'});
this._addTabsWidget({event: 'change'});
} else {
this.element.tabs();
this._addTabsWidget();
}
this._bindEvents();
},
_addTabsWidget: function(aParams)
{
if(this.element.hasClass('ibo-is-scrollable')){
this.element.scrollabletabs(aParams);
} else {
this.element.tabs(aParams);
}
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
@@ -86,6 +97,9 @@ $(function()
this.element.on('tabsactivate', function(oEvent, oUI){
me._onTabActivated(oUI);
});
this.element.on('tabscrolled', function(oEvent, oUI){
me._onTabActivated(oUI);
});
// Bind an event to window.onhashchange that, when the history state changes,
// iterates over all tab widgets, changing the current tab as necessary.
$(window).on('hashchange', function(){

View File

@@ -321,6 +321,34 @@ JS
$oRichTextBlock->AddSubBlock($oRichTextForm);
$oContentLayout->AddMainBlock($oRichTextBlock);
//////////////////////////////////////////////////////////////////////////
//
// Tabs preferences
//
//////////////////////////////////////////////////////////////////////////
$oTabsBlock = new Panel(Dict::S('UI:Tabs:Preferences'), array(), 'grey', 'ibo-tabs');
$oTabsForm = new Form();
$oTabsForm->AddSubBlock(InputFactory::MakeForHidden('operation', 'apply_tab_config'));
$sTabsScrollableValue = appUserPreferences::GetPref('tab_scrollable', false);;
$oTabsScrollable = InputFactory::MakeForSelectWithLabel('tab_scrollable', Dict::S('UI:Tabs:Scrollable:Label'));
$oTabsScrollable->GetInput()->AddOption(InputFactory::MakeForSelectOption('true', Dict::S('UI:Tabs:Scrollable:Scrollable'), true === $sTabsScrollableValue));
$oTabsScrollable->GetInput()->AddOption(InputFactory::MakeForSelectOption('false', Dict::S('UI:Tabs:Scrollable:Classic'), false === $sTabsScrollableValue));
$oTabsForm->AddSubBlock($oTabsScrollable);
// - Cancel button
$oTabsCancelButton = ButtonFactory::MakeForSecondaryAction(Dict::S('UI:Button:Cancel'));
$oTabsCancelButton->SetOnClickJsCode("window.location.href = '$sURL'");
$oTabsForm->AddSubBlock($oTabsCancelButton);
// - Submit button
$oTabsSubmitButton = ButtonFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), null, null, true);
$oTabsForm->AddSubBlock($oTabsSubmitButton);
$oTabsBlock->AddSubBlock($oTabsForm);
$oContentLayout->AddMainBlock($oTabsBlock);
//////////////////////////////////////////////////////////////////////////
//
// User picture placeholder
@@ -472,6 +500,10 @@ try {
appUserPreferences::SetPref('richtext_config', json_encode($aRichTextConfig));
DisplayPreferences($oPage);
break;
case 'apply_tab_config':
$bScrollable= utils::ReadParam('tab_scrollable', 'false') === 'true';
appUserPreferences::SetPref('tab_scrollable', $bScrollable);
break;
case 'apply_language':
$sLangCode = utils::ReadParam('language', 'EN US');
$oUser = UserRights::GetUserObject();

View File

@@ -41,7 +41,8 @@ class TabContainer extends UIContentBlock
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/layouts/tab-container/layout';
public const DEFAULT_JS_TEMPLATE_REL_PATH = 'base/layouts/tab-container/layout';
public const DEFAULT_JS_FILES_REL_PATH = [
'js/layouts/tab-container.js'
'js/layouts/tab-container/tab-container.js',
'js/layouts/tab-container/scrollable-tabs.js'
];
// Specific constants
@@ -51,6 +52,9 @@ class TabContainer extends UIContentBlock
public const ENUM_LAYOUT_VERTICAL = 'vertical';
/** @var string */
public const DEFAULT_LAYOUT = self::ENUM_LAYOUT_HORIZONTAL;
/** @var bool */
public const DEFAULT_SCROLLABLE = false;
/** @var string $sName */
private $sName;
@@ -58,6 +62,8 @@ class TabContainer extends UIContentBlock
private $sPrefix;
/** @var string $sLayout Layout of the tabs (horizontal, vertical, ...), see static::ENUM_LAYOUT_XXX */
private $sLayout;
/** @var bool $bIsScrollable Define if we can scroll through tabs */
private $bIsScrollable;
/**
* TabContainer constructor.
@@ -80,6 +86,8 @@ class TabContainer extends UIContentBlock
$this->sName = $sName;
$this->sPrefix = $sPrefix;
$this->sLayout = appUserPreferences::GetPref('tab_layout', static::DEFAULT_LAYOUT);
$this->bIsScrollable = appUserPreferences::GetPref('tab_scrollable', static::DEFAULT_SCROLLABLE);
}
/**
@@ -183,4 +191,21 @@ class TabContainer extends UIContentBlock
public function GetLayout(): string {
return $this->sLayout;
}
/**
* @param bool $bIsScrollable
* @return $this
*/
public function SetIsScrollable($bIsScrollable) {
$this->bIsScrollable = $bIsScrollable;
return $this;
}
/**
* @return bool
*/
public function GetIsScrollable(): bool {
return $this->bIsScrollable;
}
}

View File

@@ -140,6 +140,7 @@ EOF
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/search/search_form_criteria_date_time.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/clipboard.min.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/clipboardwidget.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/ScrollMagic.min.js');
}
/**

View File

@@ -1,6 +1,7 @@
{# @copyright Copyright (C) 2010-2020 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
<div id="{{ oUIBlock.GetId() }}" class="ibo-tab-container ibo-is-{{ oUIBlock.GetLayout() }}" data-role="ibo-tab-container">
<div id="{{ oUIBlock.GetId() }}" class="ibo-tab-container ibo-is-{{ oUIBlock.GetLayout() }}{% if oUIBlock.GetIsScrollable() %} ibo-is-scrollable{% endif %}"
data-role="ibo-tab-container">
{% block iboTabContainer %}
<ul class="ibo-tab-container--tabs-list" data-role="ibo-tab-container--tabs-list">
{% if not aPage.isPrintable %}
@@ -42,13 +43,15 @@
{% block iboTabContainerTabsContainers %}
{% if not aPage.isPrintable %}
{% for oTab in oUIBlock.GetSubBlocks() %}
{% if oTab.GetType() == constant('TabManager::ENUM_TAB_TYPE_HTML') %}
<div id="tab_{{ oTab.GetId()|sanitize_identifier }}" class="ibo-tab-container--tab-container">
{{ render_block(oTab, {aPage: aPage}) }}
</div>
{% endif %}
{% endfor %}
<div id="{{ oUIBlock.GetId() }}--tab-container-list" class="ibo-tab-container--tab-container-list" data-role="ibo-tab-container--tab-container-list">
{% for oTab in oUIBlock.GetSubBlocks() %}
{% if oTab.GetType() == constant('TabManager::ENUM_TAB_TYPE_HTML') %}
<div id="tab_{{ oTab.GetId()|sanitize_identifier }}" class="ibo-tab-container--tab-container">
{{ render_block(oTab, {aPage: aPage}) }}
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
{% for oTab in oUIBlock.GetSubBlocks() %}