Support

Account

Home Forums General Issues Solution share : nested tabs

Unread

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 :

    • Tab 1
    • Tab 2
      • Tab 2.1 <– enable native endpoint setting on this one
      • Tab 2.2
      • Tab 2.3 <– enable custom tabgroupend setting on this one
    • Tab 3

    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.

Viewing 1 post (of 1 total)

You must be logged in to reply to this topic.