N°2847 - Rework of TabContainer / Tab

- Add JS widget to handle front-end logic and for better encapsulation
- Move SCSS files to match convention
- Update SCSS files
- Remove unused SCSS file
- Move HTML templates to match convention
- Remove unused HTML template
- Renamed codes and folders to match convention
- Update PHPDoc
- Reformat code
- Remove usage of return type hinting when using "self"
This commit is contained in:
Molkobain
2020-09-30 17:46:11 +02:00
parent 261131d6d7
commit 50bf0c9a27
20 changed files with 398 additions and 287 deletions

View File

@@ -1,4 +0,0 @@
/*!
* copyright Copyright (C) 2010-2020 Combodo SARL
* license http://opensource.org/licenses/AGPL-3.0
*/

View File

@@ -13,9 +13,8 @@
@import "popover-menu/popover-menu-item";
@import "newsroom-menu";
@import "tabcontainer";
@import "tab";
@import "ajaxtab";
@import "tab-container/tab-container";
@import "tab-container/tab";
@import "title";
@import "form";
@import "input";

View File

@@ -1,4 +0,0 @@
/*!
* copyright Copyright (C) 2010-2020 Combodo SARL
* license http://opensource.org/licenses/AGPL-3.0
*/

View File

@@ -1,37 +0,0 @@
/*!
* copyright Copyright (C) 2010-2020 Combodo SARL
* license http://opensource.org/licenses/AGPL-3.0
*/
.ibo-tab-container {
}
.ibo-tab-container-header {
background: $ibo-color-grey-100;
}
.ibo-tab-header {
a {
@extend %ibo-font-ral-med-200;
color: $ibo-color-grey-700;
:hover {
color: $ibo-color-grey-900;
}
}
&.ui-state-active a {
color: $ibo-color-blue-800;
@extend %ibo-font-ral-bol-200;
:hover {
color: $ibo-color-blue-800;
}
}
}
.ibo-tab-content {
background: $ibo-color-white-100;
}

View File

@@ -0,0 +1,55 @@
/*!
* @copyright Copyright (C) 2010-2020 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
/* SCSS variables */
$ibo-tab-container--tabs-list--height: 36px !default;
$ibo-tab-container--tabs-list--padding-x: 24px !default;
$ibo-tab-container--tabs-list--background-color: $ibo-color-grey-100 !default;
$ibo-tab-container--tab-header--max-width: 110px !default;
$ibo-tab-container--tab-header--text-color: $ibo-color-grey-700 !default;
$ibo-tab-container--tab-header--text-color--is-active: $ibo-color-blue-800 !default;
$ibo-tab-container--tab-header--text-color--on-hover: $ibo-color-blue-800 !default;
$ibo-tab-container--tab-header--background-color--on-hover: $ibo-color-grey-200 !default;
$ibo-tab-container--tab-toggler--padding-x: 24px !default;
/* Rules */
.ibo-tab-container--tabs-list {
@extend %ibo-full-height-content;
justify-content: center;
height: $ibo-tab-container--tabs-list--height;
background-color: $ibo-tab-container--tabs-list--background-color;
@extend %ibo-font-ral-nor-150;
}
.ibo-tab-container--tab-header{
@extend %ibo-full-height-content;
color: $ibo-tab-container--tab-header--text-color;
&:hover{
color: $ibo-tab-container--tab-header--text-color--on-hover;
background-color: $ibo-tab-container--tab-header--background-color--on-hover;
}
&.ui-tabs-active{
@extend %ibo-font-ral-bol-150;
color: $ibo-tab-container--tab-header--text-color--is-active;
}
}
.ibo-tab-container--tab-toggler{
@extend %ibo-fully-centered-content;
padding-left: $ibo-tab-container--tab-toggler--padding-x;
padding-right: $ibo-tab-container--tab-toggler--padding-x;
@extend %ibo-text-truncated-with-ellipsis;
color: inherit; /* To get color from parent */
&:hover,
&:active{
color: inherit;
}
}

View File

@@ -0,0 +1,28 @@
/*!
* @copyright Copyright (C) 2010-2020 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
.ibo-tab--header {
a {
@extend %ibo-font-ral-med-200;
color: $ibo-color-grey-700;
:hover {
color: $ibo-color-grey-900;
}
}
&.ui-state-active a {
color: $ibo-color-blue-800;
@extend %ibo-font-ral-bol-200;
:hover {
color: $ibo-color-blue-800;
}
}
}
.ibo-tab--content {
background: $ibo-color-white-100;
}

View File

@@ -929,52 +929,52 @@ button.ui-button {
.ui-spinner-down {
bottom: 0;
}
.ui-tabs {
position: relative;
padding: .2em;
.ui-tabs-nav {
margin: 0;
padding: .2em .2em 0;
li {
list-style: none;
float: left;
position: relative;
top: 0;
margin: 1px .2em 0 0;
border-bottom-width: 0;
padding: 0;
white-space: nowrap;
}
.ui-tabs-anchor {
float: left;
padding: .5em 1em;
text-decoration: none;
}
li.ui-tabs-active {
margin-bottom: -1px;
padding-bottom: 1px;
.ui-tabs-anchor {
cursor: text;
}
}
li.ui-state-disabled {
.ui-tabs-anchor {
cursor: text;
}
}
li.ui-tabs-loading {
.ui-tabs-anchor {
cursor: text;
}
}
}
.ui-tabs-panel {
display: block;
border-width: 0;
padding: 1em 1.4em;
// background: none;
}
}
//.ui-tabs {
// position: relative;
// padding: .2em;
// .ui-tabs-nav {
// margin: 0;
// padding: .2em .2em 0;
// li {
// list-style: none;
// float: left;
// position: relative;
// top: 0;
// margin: 1px .2em 0 0;
// border-bottom-width: 0;
// padding: 0;
// white-space: nowrap;
// }
// .ui-tabs-anchor {
// float: left;
// padding: .5em 1em;
// text-decoration: none;
// }
// li.ui-tabs-active {
// margin-bottom: -1px;
// padding-bottom: 1px;
// .ui-tabs-anchor {
// cursor: text;
// }
// }
// li.ui-state-disabled {
// .ui-tabs-anchor {
// cursor: text;
// }
// }
// li.ui-tabs-loading {
// .ui-tabs-anchor {
// cursor: text;
// }
// }
// }
// .ui-tabs-panel {
// display: block;
// border-width: 0;
// padding: 1em 1.4em;
// // background: none;
// }
//}
.ui-tabs-collapsible {
.ui-tabs-nav {
li.ui-tabs-active {

View File

@@ -1,44 +1,140 @@
// The "tab widgets" to handle.
var tabs = $('div[id^=tabbedContent]');
// 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)
// when their href was beginning by #. Starting with 1.9, a <base> tag in the page
// is taken into account and causes "local" tabs to be considered as Ajax
// unless their URL is equal to the URL of the page...
if ($('base').length > 0) {
$('div[id^=tabbedContent] > ul > li > a').each(function () {
var sHash = location.hash;
var sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, '');
$(this).attr("href", sCleanLocation + $(this).attr("href"));
});
}
if ($.bbq) {
// This selector will be reused when selecting actual tab widget A elements.
var tab_a_selector = 'ul.ui-tabs-nav a';
$(function()
{
$.widget( 'itop.tab_container',
{
// default options
options:
{
},
css_classes:
{
},
js_selectors:
{
tabs_list: '[data-role="ibo-tab-container--tabs-list"]',
tab_header: '[data-role="ibo-tab-container--tab-header"]',
tab_toggler: '[data-role="ibo-tab-container--tab-toggler"]',
},
// Enable tabs on all tab widgets. The `event` property must be overridden so
// 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.
tabs.tabs({event: 'change'});
// the constructor
_create: function()
{
this.element.addClass('ibo-tab-container');
// Define our own click handler for the tabs, overriding the default.
tabs.find(tab_a_selector).click(function () {
var state = {};
// 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)
// when their href was beginning by #. Starting with 1.9, a <base> tag in the page
// is taken into account and causes "local" tabs to be considered as Ajax
// unless their URL is equal to the URL of the page...
if ($('base').length > 0) {
this.element.find(this.js_selectors.tab_toggler).each(function () {
const sHash = location.hash;
const sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, '');
$(this).attr('href', sCleanLocation + $(this).attr('href'));
});
}
// Get the id of this tab widget.
var id = $(this).closest('div[id^=tabbedContent]').attr('id');
if ($.bbq) {
// This selector will be reused when selecting actual tab widget A elements.
const sTabAnchorSelector = 'ul.ui-tabs-nav a';
// Get the index of this tab.
var idx = $(this).parent().prevAll().length;
// Enable tabs on all tab widgets. The `event` property must be overridden so
// 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'});
} else {
this.element.tabs();
}
// Set the state!
state[id] = idx;
$.bbq.pushState(state);
});
} else {
tabs.tabs();
}
$('.ibo-tab-container-spinner').hide();
$('.ibo-tab-container').show();
this._bindEvents();
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('ibo-tab-container');
},
_bindEvents: function()
{
const me = this;
// Bind an event on tab activation
this.element.on('tabsactivate', 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(){
me._onHashChange();
});
// Define our own click handler for the tabs, overriding the default.
this.element.find(this.js_selectors.tab_toggler).on('click', function(){
me._onTogglerClick($(this));
});
},
// Events callbacks
// - Update URL hash when tab is activated
_onTabActivated: function(oUI)
{
let oState = {};
// Get the id of this tab widget.
const sId = this.element.attr( 'id' );
// Get the index of this tab.
const iIdx = $(oUI.newTab).prevAll().length;
// Set the state!
oState[ sId ] = iIdx;
$.bbq.pushState( oState );
},
// - Change current tab as necessary when URL hash changes
_onHashChange: function()
{
// Get the index for this tab widget from the hash, based on the
// appropriate id property. In jQuery 1.4, you should use e.getState()
// instead of $.bbq.getState(). The second, 'true' argument coerces the
// string value to a number.
const iIdx = $.bbq.getState( this.element.attr('id'), true ) || 0;
// Select the appropriate tab for this tab widget by triggering the custom
// event specified in the .tabs() init above (you could keep track of what
// tab each widget is on using .data, and only select a tab if it has
// changed).
this.element.find(this.js_selectors.tab_toggler).eq(iIdx).triggerHandler('change');
// Iterate over all truncated lists to find whether they are expanded or not
$('a.truncated').each(function()
{
const sState = $.bbq.getState( this.id, true ) || 'close';
if (sState === 'open')
{
$(this).trigger('open');
}
else
{
$(this).trigger('close');
}
});
},
_onTogglerClick: function(oTabHeaderElem)
{
if ($.bbq) {
let oState = {};
// Get the id of this tab widget.
const sId = this.element.attr('id');
// Get the index of this tab.
const iIdx = oTabHeaderElem.parent().prevAll().length;
// Set the state!
oState[sId] = iIdx;
$.bbq.pushState(oState);
}
}
});
});

View File

@@ -21,7 +21,6 @@ namespace Combodo\iTop\Application\UI\Layout\TabContainer\Tab;
use Combodo\iTop\Application\UI\iUIBlock;
use Combodo\iTop\Application\UI\Layout\iUIContentBlock;
use Combodo\iTop\Application\UI\UIException;
use Dict;
use TabManager;
@@ -30,95 +29,99 @@ use TabManager;
* Class AjaxTab
*
* @package Combodo\iTop\Application\UI\Layout\TabContainer\Tab
* @internal
* @since 2.8.0
*/
class AjaxTab extends Tab
{
class AjaxTab extends Tab {
// Overloaded constants
public const BLOCK_CODE = 'ibo-ajaxtab';
public const HTML_TEMPLATE_REL_PATH = 'layouts/tabcontainer/tab/ajaxtab';
public const BLOCK_CODE = 'ibo-ajax-tab';
public const TAB_TYPE = TabManager::ENUM_TAB_TYPE_AJAX;
protected const TAB_TYPE = TabManager::ENUM_TAB_TYPE_AJAX;
/** @var string */
private $sURL;
/** @var bool */
/** @var string The target URL to be loaded asynchronously */
private $sUrl;
/** @var bool Whether the tab should should be cached by the browser or always refreshed */
private $bCache;
/**
* @param string $sHtml
* @param string $sUrl
*
* @return \Combodo\iTop\Application\UI\iUIBlock
* @throws \Combodo\iTop\Application\UI\UIException
* @return $this
*/
public function AddHtml(string $sHtml): iUIBlock
{
throw new UIException($this, Dict::Format('UIBlock:Error:AddBlockForbidden', $this->GetId()));
}
public function SetUrl(string $sUrl) {
$this->sUrl = $sUrl;
/**
* @param \Combodo\iTop\Application\UI\iUIBlock $oSubBlock
*
* @return iUIContentBlock
* @throws \Combodo\iTop\Application\UI\UIException
*/
public function AddSubBlock(iUIBlock $oSubBlock): iUIContentBlock
{
throw new UIException($this, Dict::Format('UIBlock:Error:AddBlockForbidden', $this->GetId()));
}
/**
* @return array|\Combodo\iTop\Application\UI\iUIBlock[]
*/
public function GetSubBlocks(): array
{
return [];
}
/**
* @param mixed $sURL
*
* @return AjaxTab
*/
public function SetURL(string $sURL): self
{
$this->sURL = $sURL;
return $this;
}
/**
* @return string
*/
public function GetUrl(): string {
return $this->sUrl;
}
/**
* Set whether the tab should should be cached by the browser or always refreshed
*
* @param bool $bCache
*
* @return AjaxTab
* @return $this
*/
public function SetCache(bool $bCache): self
{
public function SetCache(bool $bCache) {
$this->bCache = $bCache;
return $this;
}
/**
* Return whether the tab should should be cached by the browser or always refreshed
*
* @return string
*/
public function GetURL(): string
{
return $this->sURL;
}
/**
* @return string
*/
public function GetCache(): string
{
public function GetCache(): string {
return $this->bCache ? 'true' : 'false';
}
public function GetParameters(): array
{
//-------------------------------
// iUIBlock implementation
//-------------------------------
/**
* @inheritDoc
*/
public function GetParameters(): array {
$aParams = parent::GetParameters();
$aParams['sURL'] = $this->GetURL();
$aParams['sURL'] = $this->GetUrl();
$aParams['sCache'] = $this->GetCache() ? 'true' : 'false';
return $aParams;
}
//-------------------------------
// iUIContentBlock implementation
//-------------------------------
/**
* @inheritDoc
* @throws \Combodo\iTop\Application\UI\UIException
*/
public function AddHtml(string $sHtml) {
throw new UIException($this, Dict::Format('UIBlock:Error:AddBlockForbidden', $this->GetId()));
}
/**
* @inheritDoc
* @throws \Combodo\iTop\Application\UI\UIException
*/
public function AddSubBlock(iUIBlock $oSubBlock) {
throw new UIException($this, Dict::Format('UIBlock:Error:AddBlockForbidden', $this->GetId()));
}
/**
* @inheritDoc
*/
public function GetSubBlocks(): array {
return [];
}
}

View File

@@ -27,33 +27,56 @@ use TabManager;
* Class Tab
*
* @package Combodo\iTop\Application\UI\Layout\TabContainer\Tab
* @internal
* @since 2.8.0
*/
class Tab extends UIContentBlock
{
// Overloaded constants
public const BLOCK_CODE = 'ibo-tab';
public const HTML_TEMPLATE_REL_PATH = 'layouts/tabcontainer/tab/tab';
public const HTML_TEMPLATE_REL_PATH = 'layouts/tab-container/tab/layout';
protected const TAB_TYPE = TabManager::ENUM_TAB_TYPE_HTML;
/** @var string */
public const TAB_TYPE = TabManager::ENUM_TAB_TYPE_HTML;
/** @var string */
protected $sTitle;
/**
* Tab constructor.
*
* @param string $sTabCode
* @param string $sTitle
*/
public function __construct(string $sTabCode, string $sTitle)
{
parent::__construct($sTabCode);
$this->sTitle = $sTitle;
}
/**
* @return string
*/
public function GetType(): string
{
return static::TAB_TYPE;
}
/**
* @return string
*/
public function GetTitle(): string
{
return $this->sTitle;
}
//-------------------------------
// iUIBlock implementation
//-------------------------------
/**
* @inheritDoc
*/
public function GetParameters(): array
{
return [

View File

@@ -36,8 +36,9 @@ use Dict;
class TabContainer extends UIContentBlock
{
// Overloaded constants
public const BLOCK_CODE = 'ibo-tabcontainer';
public const HTML_TEMPLATE_REL_PATH = 'layouts/tabcontainer/layout';
public const BLOCK_CODE = 'ibo-tab-container';
public const HTML_TEMPLATE_REL_PATH = 'layouts/tab-container/layout';
public const JS_TEMPLATE_REL_PATH = 'layouts/tab-container/layout';
public const JS_FILES_REL_PATH = [
'js/layouts/tab-container.js'
];

View File

@@ -209,7 +209,7 @@ class TabManager
// Set the content of the tab
/** @var \Combodo\iTop\Application\UI\Layout\TabContainer\Tab\AjaxTab $oTab */
$oTab = $this->InitTab($this->m_sCurrentTabContainer, $sTabCode, static::ENUM_TAB_TYPE_AJAX, $sTabTitle);
$oTab->SetURL($sUrl)
$oTab->SetUrl($sUrl)
->SetCache($bCache);
return ''; // Nothing to add to the page for now

View File

@@ -366,57 +366,6 @@ JS
SetUserPreference(parent_id+'_'+this.id+'_height', $(this).height(), true); // true => persistent
}
} );
// refresh the hash when the tab is changed (from a JS script)
$('body').on( 'tabsactivate', '.ui-tabs', function(event, ui) {
var state = {};
// Get the id of this tab widget.
var id = $(ui.newTab).closest( 'div[id^=tabbedContent]' ).attr( 'id' );
// Get the index of this tab.
var idx = $(ui.newTab).prevAll().length;
// Set the state!
state[ id ] = idx;
$.bbq.pushState( state );
});
// Bind an event to window.onhashchange that, when the history state changes,
// iterates over all tab widgets, changing the current tab as necessary.
$(window).bind( 'hashchange', function(e)
{
// Iterate over all tab widgets.
tabs.each(function()
{
// Get the index for this tab widget from the hash, based on the
// appropriate id property. In jQuery 1.4, you should use e.getState()
// instead of $.bbq.getState(). The second, 'true' argument coerces the
// string value to a number.
var idx = $.bbq.getState( this.id, true ) || 0;
// Select the appropriate tab for this tab widget by triggering the custom
// event specified in the .tabs() init above (you could keep track of what
// tab each widget is on using .data, and only select a tab if it has
// changed).
$(this).find( tab_a_selector ).eq( idx ).triggerHandler( 'change' );
});
// Iterate over all truncated lists to find whether they are expanded or not
$('a.truncated').each(function()
{
var state = $.bbq.getState( this.id, true ) || 'close';
if (state == 'open')
{
$(this).trigger('open');
}
else
{
$(this).trigger('close');
}
});
});
// Shortcut menu actions
$('.actions_button a').click( function() {

View File

@@ -0,0 +1,31 @@
{# @copyright Copyright (C) 2010-2020 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
<div id="{{ oUIBlock.GetId() }}" class="ibo-tab-container" data-role="ibo-tab-container">
{% block iboTabContainer %}
<ul class="ibo-tab-container--tabs-list" data-role="ibo-tab-container--tabs-list">
{% block iboTabContainerTabsList %}
{% for oTab in oUIBlock.GetSubBlocks() %}
{% if oTab.GetType() == constant('TabManager::ENUM_TAB_TYPE_AJAX') %}
<li class="ibo-tab-container--tab-header" data-role="ibo-tab-container--tab-header" data-tab-id="tab_{{ loop.index }}" data-tab-type="{{ oTab.GetType() }}" data-cache="{{ oTab.GetCache() }}">
<a href="{{ oTab.GetUrl() }}" class="ibo-tab-container--tab-toggler" data-role="ibo-tab-container--tab-toggler"><span>{{ oTab.GetTitle() }}</span></a>
</li>
{% elseif oTab.GetType() == constant('TabManager::ENUM_TAB_TYPE_HTML') %}
<li class="ibo-tab-container--tab-header" data-role="ibo-tab-container--tab-header" data-tab-id="tab_{{ loop.index }}" data-tab-type="{{ oTab.GetType() }}">
<a href="#tab_{{ loop.index }}" class="ibo-tab-container--tab-toggler" data-role="ibo-tab-container--tab-toggler"><span>{{ oTab.GetTitle() }}</span></a>
</li>
{% endif %}
{% endfor %}
{% endblock %}
</ul>
{% block iboTabContainerTabsContainers %}
{% for oTab in oUIBlock.GetSubBlocks() %}
{% if oTab.GetType() == 'html' %}
<div id="tab_{{ loop.index }}" class="ibo-tab-container--tab-container">
{{ render_block(oTab, {aPage: aPage}) }}
</div>
{% endif %}
{% endfor %}
{% endblock %}
{% endblock %}
</div>

View File

@@ -0,0 +1 @@
$('#{{ oUIBlock.GetId() }}').tab_container();

View File

@@ -0,0 +1,9 @@
{# @copyright Copyright (C) 2010-2020 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
<div id="{{ oUIBlock.GetId() }}" class="ibo-tab">
{% block iboContentBlockContainer %}
{% for oSubBlock in oUIBlock.GetSubBlocks() %}
{{ render_block(oSubBlock, {aPage: aPage}) }}
{% endfor %}
{% endblock %}
</div>

View File

@@ -1,26 +0,0 @@
{# @copyright Copyright (C) 2010-2020 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
{% block iboTabContainer %}
<div class="ibo-tab-container-spinner"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
<!-- tabs -->
<div id="tabbedContent_{$sPrefix}{$container_index}" class="ibo-tab-container" style="display: none;">
<ul class="ibo-tab-container-header">
{% for oTab in oUIBlock.GetSubBlocks() %}
{% if oTab.GetType() == 'ajax' %}
<li class="ibo-tab-header ibo-tab-header-ajax" data-cache="{{ oTab.GetCache() }}"><a href="{{ oTab.GetURL() }}" class="ibo-tab-link" data-tab-id="$sTabCodeForHtml"><span>{{ oTab.GetTitle() }}</span></a></li>
{% elseif oTab.GetType() == 'html' %}
<li class="ibo-tab-header ibo-tab-header-html"><a href="#tab_{{ oTab.GetId() }}" class="ibo-tab-link" data-tab-id="$sTabCodeForHtml"><span>{{ oTab.GetTitle() }}</span></a></li>
{% endif %}
{% endfor %}
</ul>
{% for oTab in oUIBlock.GetSubBlocks() %}
{% if oTab.GetType() == 'html' %}
<div id="tab_{{ oTab.GetId() }}" class="ibo-tab-content">
{{ render_block(oTab, {aPage: aPage}) }}
</div>
{% endif %}
{% endfor %}
</div>
<!-- end of tabs-->
{% endblock %}

View File

@@ -1,2 +0,0 @@
{# @copyright Copyright (C) 2010-2020 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}

View File

@@ -1,2 +0,0 @@
{# @copyright Copyright (C) 2010-2020 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}

View File

@@ -1,9 +0,0 @@
{# @copyright Copyright (C) 2010-2020 Combodo SARL #}
{# @license http://opensource.org/licenses/AGPL-3.0 #}
<div id="{{ oUIBlock.GetId() }}" class="ibo-tab-content-block">
{% block iboContentBlockContainer %}
{% for oSubBlock in oUIBlock.GetSubBlocks() %}
{{ render_block(oSubBlock, {aPage: aPage}) }}
{% endfor %}
{% endblock %}
</div>