Home › Forums › General Issues › Solution share : nested tabs
I was trying to organize my configuration fields in option page in multiple depths of tabs but found that trying to close a nested tabgroup to add nother tab in parent tabgroup was impossible, like exposed by @tdmalone in this thread.
Also, using group field is not an option : my code is already written and I don’t want to have to handle useless additional array dimensions or non unique field names, like suggested by @jackfowler.
So I tried to fix the issues caused by poor DOM tree the code of ACF generates.
1. Add the tab field a custom tabgroupend
setting:
add_action( 'acf/render_field_settings/type=tab', function( array $field ) {
acf_render_field_setting( $field, [
'label' => __( 'End of tab group', 'mydom' ),
'instructions' => '', // Tooltip
'hint' => '',
'type' => 'true_false',
'name' => 'tabgroupend',
'ui' => 1,
'class' => 'acf-field-object-true-false-ui',
], global: false );
And set a custom class on the tabs where this setting is enabled:
add_filter( 'acf/prepare_field', function( array $field ) {
if( ! empty( $field['tabgroupend'] ) ) {
$field['class'] .= ' acf-tabgroupend';
}
return $field;
} );
This results to add acf-tabgroupend
on tab links.
2. Set up a nested tabs structure with such logic :
3. Now script a bit through in a JS loaded on backend only :
(written in ES6)
window.acf?.addAction('ready', function() { // This allows to delay a bit after ACF other actions execution
if( ! acfTabgroupendEls.length ) {
return
}
/**
* Move acf-tabgroupend subsequent tabs to upper tabgroup
*/
acfTabgroupendEls.forEach( tabpanel => {
const ownTab = document.querySelector( .acf-tab-button[data-key="${tabpanel.dataset.key}"]
).parentNode // li
const ownTabWrap = ownTab.closest( '.acf-tab-wrap' )
// Tabs to move
let tabsToMove = []
let p = false // used to flag the tab
ownTabWrap.querySelectorAll( 'li' ).forEach( li => {
if( ! p ) {
if( li.isSameNode( ownTab ) ) {
p = true
}
return
}
tabsToMove.push( li )
} )
// Find tab group to move to
let elem = ownTabWrap.previousElementSibling
let secu = 0
let parentTabWrap
// Loop over the previous siblings to find possible parent in previous tabwraps
while( elem && secu < 1000 ) {
secu++
if( elem.matches( '.acf-tab-wrap' ) ) {
// tabwrap has an explicit end: continue to search up
if( ! elem.querySelector( '.acf-tabgroupend' ) ) {
parentTabWrap = elem
break
}
}
elem = elem.previousElementSibling
}
// No parent found: abort
if( ! parentTabWrap ) {
return
}
const newSiblingsTabs = parentTabWrap.querySelectorAll( 'li' )
// Move tabs to parent
tabsToMove.forEach( tab => {
parentTabWrap.childNodes[0].appendChild( tab )
} )
} )
/**
* Rework tabs showing/hiding
*/
document.querySelectorAll( '.acf-fields' ).forEach( acfFieldsEl => {
const childrenEls = Array.from( acfFieldsEl.childNodes ).filter( x => x.nodeType == Node.ELEMENT_NODE )
// No children elements: skip
if( ! childrenEls.length ) {
return
}
let tabTree = {}
let tabs = {}
let depth = 0
let currentKey
childrenEls.forEach( childEl => {
if( childEl.matches( '.acf-tab-wrap' ) ) {
childEl.querySelectorAll( '.acf-tab-button[data-key]' ).forEach( tabBtn => {
if( tabBtn.dataset.endpoint == '1' ) {
depth++
}
tabs[tabBtn.dataset.key] = {
depth: depth,
element: tabBtn.parentNode,
children: [],
}
if( tabBtn.matches( '.acf-tabgroupend' ) ) {
depth--
}
} )
}
} )
// No tabgroup in there: skip
if( _.isEmpty( tabs ) ) { // use of Lodash isEmpty helper, build your own if you prefer
return
}
// Store children with each parent tab
childrenEls.forEach( childEl => {
if( childEl.matches( '.acf-field-tab[data-key]' ) ) {
currentKey = childEl.dataset.key
}
else if( currentKey ) {
tabs[currentKey].children.push( childEl )
}
} )
Object.values( tabs ).forEach( tab => {
tab.element.addEventListener( 'click', ( e ) => {
Object.values( tabs ).forEach( t => {
// Different depth: skip
if( t.depth != tab.depth ) {
return
}
const activate = t.element.isSameNode( tab.element )
t.element.classList.toggle( 'active', activate )
t.children.forEach( child => {
child.classList.toggle( 'acf-hidden', ! activate )
if( ! activate ) {
child.setAttribute( 'hidden', '' )
}
else {
child.removeAttribute( 'hidden' )
}
} )
} )
} )
} )
} )
/**
* Fixes wrong visible tabs at load
*/
document.querySelectorAll( '.acf-tab-wrap .active' ).forEach( tabBtn => tabBtn.dispatchEvent( new MouseEvent( 'click' ) ) )
}
Worth to say that this solution will let unchanged the field groups where you did not enable tabgroupend
custom setting on any tab.
You must be logged in to reply to this topic.
Welcome to the Advanced Custom Fields community forum.
Browse through ideas, snippets of code, questions and answers between fellow ACF users
Helping others is a great way to earn karma, gain badges and help ACF development!
We use cookies to offer you a better browsing experience, analyze site traffic and personalize content. Read about how we use cookies and how you can control them in our Privacy Policy. If you continue to use this site, you consent to our use of cookies.