New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow groups of fields to be broken into tabs #5

Open
billerickson opened this Issue Aug 20, 2014 · 36 comments

Comments

Projects
None yet
@billerickson
Collaborator

billerickson commented Aug 20, 2014

If you have a lot of fields for a page, instead of having one long list we could display them in tabs like Advanced Custom Fields offers. Ex: http://cl.ly/image/1x1x3r0Y0l3U

@jtsternberg

This comment has been minimized.

Member

jtsternberg commented Aug 21, 2014

definitely 👍

@jtsternberg

This comment has been minimized.

Member

jtsternberg commented Aug 21, 2014

Right now grouped fields are strictly for repeatable groups, but we ought to make it more generic, making repeatable an option as well as tabs being an option: https://github.com/WebDevStudios/CMB2/blob/master/example-functions.php#L310-L314

I know @pmgarman already had it in the works to make field groups work w/o having to be repeatable.

@jtsternberg jtsternberg reopened this Aug 21, 2014

@jtsternberg

This comment has been minimized.

Member

jtsternberg commented Sep 3, 2014

@scottopolis used this approach for tabs. @billerickson is that what you had in mind?
http://jsfiddle.net/syahrasi/Us8uc/

@billerickson

This comment has been minimized.

Collaborator

billerickson commented Sep 3, 2014

Yep, looks good to me!

@robneu

This comment has been minimized.

robneu commented Sep 15, 2014

+1 for tab implementation. Also, IMO tabs should only be a UI construct. Grouping elements into tabs shouldn't necessarily change the way the data is saved. There's already jQuery UI tabs built into core so the JS part is pretty straight forward.

I was able to hack this into place, but it required making modifications to CMB, which I'd really like to avoid if possible.

@rogerlos

This comment has been minimized.

rogerlos commented Oct 17, 2014

On the old CMB, I was able to make tabbed admin option pages, though I had to hack my way around a bit. Essentially, I made every individual meta box a wordpress admin tab. Ran into some issues with repeatable fields....but as my only use of CMB on that project was a single options page with tabs, I hacked stuff around a bit to make it work.

My rather crude approach, don't know if this is helpful, again, this was using the old CMB:

/**
* Adds a wrapper div for the WP tabs to work with
 *
* @param $meta_box
*/
public function before_cmb_table( $meta_box ) {
    // check to see if there is an active tab, or set it to the default
    $active_tab = isset( $_GET[ 'tab' ] ) ? $_GET[ 'tab' ] : self::$default_tab;
    // add a class to show the tab if it's active
    $active_class = ($active_tab == 'cmb-tab-' . $meta_box['id'] ) ? 'cmb-tab-active' : '';
    // div wraps the table
    echo '<div id="cmb-tab-' . $meta_box['id'] . '" class="cmb-tab ' . $active_class . '">';
}
/**
* Closes the wrapper div
*/
public function after_cmb_table() {
    echo '</div>';
}
/**
* Tabbed options page using CMB boxes
*
* @param      $meta_boxes
* @param      $object_id
* @param bool $echo
*
* @return string
*/
public function cmb_tabbed_admin_form( $meta_boxes, $object_id, $echo = true ) {
    // discover if there is an active tab...
    $active_tab = isset( $_GET[ 'tab' ] ) ? $_GET[ 'tab' ] :  self::$default_tab;
    $save_flag = false;
    foreach( $meta_boxes as $key => $mb ) {
        // set the defaults
        $mb = cmb_Meta_Box::set_mb_defaults( $mb );
        // Make sure this meta box should be shown
        if ( ! apply_filters( 'cmb_show_on', true, $mb ) ) {
            unset( $meta_boxes[$key] );
            continue;
        }
        // if the default tab has not been set, make it this box (ie, the first box)
        if ( self::$default_tab === null ) {
            self::$default_tab = 'cmb-tab-' . $mb['id'];
            if ( $active_tab === null ) $active_tab = self::$default_tab;
        }
        // Make sure that our object type is explicitly set by the metabox config
        cmb_Meta_Box::set_object_type( cmb_Meta_Box::set_mb_type( $mb ) );
        // this is a little inelegant for multiple boxes all in the same form, but there you have it...
        if (
            // check nonce
            isset( $_POST['submit-cmb'], $_POST['object_id'], $_POST['wp_meta_box_nonce'] )
            && wp_verify_nonce( $_POST['wp_meta_box_nonce'], cmb_Meta_Box::nonce() )
            && $_POST['object_id'] == $object_id
        ) {
            cmb_save_metabox_fields( $mb, $object_id );
            $save_flag = true;
        }
    }
    if ( $save_flag === true ) {
        self::$myfunction->set_options( stripslashes_deep( get_option( self::$key ) ) );
        echo '<div class="updated"><p>Your settings were updated.</p></div>';
    }
    // make sure the above checking did not empty the metaboxes array
    if ( ! empty ( $meta_boxes ) ) {
        // start tab navigation from scratch
        $tabs = '';
        // output the metaboxes as a form
        ob_start();
        foreach ( (array) $meta_boxes as $meta_box ) {
            // the content tab's ID
            $this_tab = 'cmb-tab-' . $meta_box['id'];
            // check to see if the current tab is active
            $active_flag = ( $active_tab == $this_tab ) ? 'nav-tab-active' : '';
            // build tab navigation; we use the "data-tab" attribute as a shortcut for JS
            $tabs .= '<a href="?page=' . $_GET[ 'page' ] . '&amp;tab=' . $this_tab . 
                '" class="nav-tab ' . $active_flag . '" data-tab="' . $this_tab . '">' . $meta_box['title'] . '</a>';
            cmb_print_metabox( $meta_box, $object_id );
        }
        $form = ob_get_contents();
        ob_end_clean();
        // removed the filter, if you add it back, be sure to work-around its individual metabox id!
        $form_format = '<h2 class="nav-tab-wrapper">%s</h2>' .
            '<form class="cmb-form" method="post" id="%s" enctype="multipart/form-data" encoding="multipart/form-data">' .
            '<input type="hidden" name="object_id" value="%s">%s<hr>' .
            '<p><input type="submit" name="submit-cmb" value="%s" class="button-primary"></p></form>';
        $form = sprintf( $form_format, $tabs, $object_id, $object_id, $form, __( 'Save' ) );
        if ( $echo )
            echo $form;
        return $form;
    }
    // if the meta_box array was empty
    return '';
}

As I recall, I had to change some of the sanitization functions which were not expecting fields as strings or as arrays, or some such. Been awhile!

@jtsternberg

This comment has been minimized.

Member

jtsternberg commented Nov 24, 2014

@marcusbattle is working on #93 which will eventually directly benefit this (future) feature. Hopefully we'll get this in in the next few weeks.

@teamcrisis

This comment has been minimized.

teamcrisis commented Mar 24, 2015

Has any progress been made with this? Preferably something like this:
demo-metabox

@ashawkat

This comment has been minimized.

ashawkat commented May 19, 2015

@jtsternberg If you guys have implemented the tabbed version of metaboxes ?

@jtsternberg

This comment has been minimized.

Member

jtsternberg commented May 19, 2015

We have not, though I don't think it would be too difficult. As @robneu said, it would just mean outputting multiple CMB2 instances in a single metabox and using jquery tabs.

@jashwant

This comment has been minimized.

jashwant commented May 24, 2015

@ashawkat , I am including bootstrap tabs.js separately and that's how I am creating my markup.

function my_page_metabox() {
  $prefix = 'myprefix_';

  $cmb_demo = new_cmb2_box( array(
    'id'            => $prefix . 'metabox',
    'title'         => __( 'Page Options', 'cmb2' ),
    'object_types'  => array( 'page', ), // Post type
    'context'       => 'normal',
    'priority'      => 'high',
    'show_names'    => true,
  ));

  $cmb_demo->add_field( array(
    'name'       => __( '', 'cmb2' ),
    'id'         => $prefix . 'opts',
    'type'       => 'page'
  ));
}

add_action('cmb2_init', 'my_page_metabox');


function jt_cmb2_render_page_field_callback( $field, $value, $object_id, $object_type, $field_type_object ) {

  $value = wp_parse_args($value, array(
    'header_layout' => '',
    'footer_layout' => ''
  ));
  ?>
  <div class="my-tabs nav">
    <ul class="nav-tabs">
      <li class="active">
        <a href="#cmb-tab-header" data-toggle="tab"><?php _e('Header', 'myprefix'); ?></a>
      </li>
      <li>
        <a href="#cmb-tab-footer" data-toggle="tab"><?php _e('Footer', 'myprefix'); ?></a>
      </li>
    </ul>
    <div class="tab-content">
      <div id="cmb-tab-header" class="tab-pane fade in active">
        <div class="label">
          <label for="<?php echo $field_type_object->_id( '-header_layout' ); ?>">
            <?php _e('Header Layout', 'myprefix'); ?>
          </label>
          <div class="desc">Enter Header Layout</div>
        </div>
        <div class="field">
          <?php 
            echo $field_type_object->input( array(
              'name'  => $field_type_object->_name( '[header_layout]' ),
              'id'    => $field_type_object->_id( '-header_layout' ),
              'value' => $value['header_layout'],
              'desc' => ''
            )); 
          ?>
        </div>
      </div>
      <div id="cmb-tab-footer" class="tab-pane fade">
        <div class="label">
          <label for="<?php echo $field_type_object->_id( '-footer_layout' ); ?>">
            <?php _e('Footer Layout', 'myprefix'); ?>
          </label>
          <div class="desc">Enter Header Layout</div>
        </div>
        <div class="field">
          <?php 
            echo $field_type_object->input( array(
              'name'  => $field_type_object->_name( '[footer_layout]' ),
              'id'    => $field_type_object->_id( '-footer_layout' ),
              'value' => $value['footer_layout'],
            )); 
          ?>
        </div>
      </div>
    </div>
  </div>

  <?php

}
add_filter( 'cmb2_render_page', 'jt_cmb2_render_page_field_callback', 10, 5 );

@jtsternberg , Is there any side effect with this approach. Although, I've to write a lot of my own, but it's very flexible. Do I need to escape / sanitize the basic fields ( like textbox, select etc ), with this approach ?

@ghost

This comment has been minimized.

ghost commented May 26, 2015

This feature would be VERY helpful for us, as we currently have to make several metaboxes that would make so much more sense if they were grouped together. Having the option to group all these settings together using tabs, will greatly enhance the user experience IMHO.
I'm definitely keeping my eye on this thread 👍

@StaggerLeee

This comment has been minimized.

StaggerLeee commented Jun 9, 2015

I vote for this too. Very important.

@jashwant

This comment has been minimized.

jashwant commented Jun 27, 2015

@jtsternberg , may you please look into my solution ( given above ) and answer the question regarding escaping / sanitization ?

@vasikgreif

This comment has been minimized.

vasikgreif commented Jul 14, 2015

Another +1 here! That would make settings pages WAY more user friendly...

@themarcusbattle

This comment has been minimized.

themarcusbattle commented Jul 20, 2015

@billerickson and everyone else here. Wanted to let you know that this is still on our radar and in progress. We made great progress today at WDS to get this going. So listen out for updates for this functionality in the very near future.
screen shot 2015-07-20 at 4 49 53 pm

@themarcusbattle themarcusbattle self-assigned this Jul 20, 2015

@jashwant

This comment has been minimized.

jashwant commented Jul 21, 2015

@marcusbattle , can you look at my solution above and confirm that I can use it, till your work is live ?

@vasikgreif

This comment has been minimized.

vasikgreif commented Oct 2, 2015

Hi everybody, any news on this? What I basically need is to replace the Options Framework that I currently use with CMB2 options page. So, just being able to put fields in a metabox under separate tabs would work great for me!!!

@willthemoor

This comment has been minimized.

willthemoor commented Dec 8, 2015

It requires some shenanigans but you can make tabs out of a single metabox. I did it using title fields.

  1. Make sure jQuery UI is enqueued (or roll your own... tabs are pretty simple). This needs to be hooked to cmb2_after_form to work.
    add_action( 'cmb2_after_form', 'your_prefix_admin_scripts' , 10, 4 );
    function your_prefix_admin_scripts () {
         wp_enqueue_script('jquery-ui-tabs');
             // and the jQuery bit in 2) below either inline or called
    }
  1. Some JS to build the tabs from title fields
    jQuery(document).ready(function($){
        'use strict';

        function setUpOptionsTabs () {

            var $container = $('#your-outer-box-div');

            $container.prepend('<ul id="tab-nav"></ul>');

            // create the tabs from title fields
            $('.cmb2-metabox-title').each(function(i, item){
                var ret = '<li><a class="nav-tab" href="#tab-'+(i+1)+'">'+ $(this).text() +'</a></li>';
                $('#tab-nav').append(ret);
            });

            $container.tabs();
        }

        setUpOptionsTabs();
    });
  1. Within your CMB fields, make use of before_row to insert the markup you'll need for the tab containers. The first title field only get's an opening div:
        $cmb->add_field( array(
            'name' => 'my name',
            'id'   => 'my-id',
            'type' => 'title',
            'before_row' => '<div id="tab-1">'
        ) );

Subsequent title fields need to close that one and start a new one.

        $cmb->add_field( array(
            'name' => 'my name',
            'id'   => 'my-id',
            'type' => 'title',
            'before_row' => '</div><div id="tab-2">'
        ) );

... and so on.

You're last field on the page needs the after_row to close it all up.

        $cmb->add_field( array(
            'name'    => 'The Last Field',
            'id'      => 'last-id',
            'type'    => 'text',
            'after_row' => '</div>', //close final tab
        ) );

Sidebar: Would be great if new_cmb2_box supported the full set of before and after field methods so that we could skip the first/last thing here. Indeed, with a known set of tabs anyway, we could skip the JS as well by stuffing the entire <ul class="tab-nav"> into the before_row on the meta-box itself.

  1. The tabs themselves will be stacked text links unless you also load the jQuery UI CSS. And then it will still be ugly because... Jquery UI. :) WordPress does have default tab styles but they don't come with the JS for free. Here's some CSS based on the wysiwyg tabs (Visual/Text) that should at least get you started.
        .nav-tab {
            background: #ebebeb none repeat scroll 0 0;
            border: 1px solid #e5e5e5;
            box-sizing: content-box;
            color: #777777;
            cursor: pointer;
            float: left;
            font: 13px/19px "Open Sans",sans-serif;
            height: 20px;
            margin: 5px 0 0 5px;
            padding: 3px 8px 4px;
            position: relative;
            top: 1px;
        }
        .ui-tabs-active .nav-tab {
            background-color: #333;
            color: #fff;
        }

Hope this helps until tabs land in CMB2 core.
@hsleonis

This comment has been minimized.

hsleonis commented Mar 1, 2016

Any update on this?
@marcusbattle

@rogerlos

This comment has been minimized.

rogerlos commented Mar 1, 2016

https://github.com/rogerlos/cmb2-metatabs-options

I wrote the above class, which allows you to have:

  • "Settings" pages with multiple meta boxes
  • Optionally have tabs on that page; individual tabs can have multiple meta boxes, too

Fairly configurable and not too heavyweight. It's setup as a wordpress plugin, but wp.org rejected it "we don't want any more developer only plugins". (Shrug)

You can use it as a class within your project. Has a wiki...

@hsleonis

This comment has been minimized.

hsleonis commented Mar 1, 2016

Nice work @rogerlos

@DevinWalker

This comment has been minimized.

Contributor

DevinWalker commented May 10, 2016

Yeah nice work @rogerlos

@darkoromanov

This comment has been minimized.

darkoromanov commented Sep 6, 2016

Hey @rogerlos thank you very much, your code is what in Italian is called "fat that runs" :)

Just out of curiosity, do you have any idea about what wp.org meant by saying "we don't want any more developer only plugins"?? I have 3 plugins there and I'm writing the forth, so I'm quite interested :-\

@hsleonis

This comment has been minimized.

hsleonis commented Sep 7, 2016

Hi @darkoromanov, they meant plugins which can only be used by the developers, like frameworks, framework extensions etc. I've uploaded one recently and got the same message :)

@darkoromanov

This comment has been minimized.

darkoromanov commented Sep 7, 2016

Oh I see! I was reading it like it was: "we don't want any more developers, we only want plugins" :)

@hsleonis

This comment has been minimized.

hsleonis commented Sep 7, 2016

Ha ha ha

@andreasupftw

This comment has been minimized.

andreasupftw commented Sep 16, 2016

@websumon

This comment has been minimized.

websumon commented Oct 22, 2016

@rogerlos How it's possible to display pages or posts ??

@kuzmenko1256

This comment has been minimized.

kuzmenko1256 commented Nov 9, 2016

Quite recently, our team has created an extension that solves the problem https://github.com/LeadSoftInc/cmb2-tabs

@WhereBeTheDan

This comment has been minimized.

WhereBeTheDan commented Jan 26, 2017

I altered @willthemoor method to be a pure javascript solution. This assumes you want to create tabs within a single metabox:

  1. Enqueue jQueryUI:
    add_action( 'cmb2_after_form', 'your_prefix_admin_scripts' , 10, 4 );
    function your_prefix_admin_scripts () {
         wp_enqueue_script('jquery-ui-tabs');
    }
  1. Add this to your admin scripts:
$('.cmb2-metabox').each( function(index, el) {
    var tabs = $(this).find('.cmb-type-title');

    if (tabs.length > 1) {
        var metabox = $(this),
        nav = $('<ul class="tab-nav" />');
                
        tabs.each( function(index, el) {
            nav.append('<li><a class="nav-tab" href="#' + metabox.attr('id') + '-tab-' + index + '">' + $(this).find('.cmb2-metabox-title').text() + '</a></li>');

            $(this).nextUntil('.cmb-type-title').addBack().wrapAll('<div id="' + metabox.attr('id') + '-tab-' + index + '" class="tab" />');
       });

       $(this).prepend(nav);

       $(this).tabs();
    }
});
  1. Add any number of the default cmb2 title field type to your metabox. All other fields until the next title field will be grouped as a tab.
@stabilimenta

This comment has been minimized.

stabilimenta commented Mar 27, 2017

Here is some code that will create tabs for dynamically created repeating sections, in case it might be useful to someone. It doesn't rely on ID like jquery-ui tabs, and instead works with class.

This is my CMB2 code. My code is using the before_row, after_row attributes to get most of the html in place:

$content_sections->add_group_field( $group_field_id, array(
    'id'      => 'section-content',
    'type'    => 'wysiwyg',
    'options' => array( 'textarea_rows' => 5, ),
    'before_row'   => '
        <div class="cmb2-tabs">
            <ul class="tabs-nav">
                <li class="current"><a href="#tab-content-1">Content</a></li>
                <li><a href="#tab-content-2">Content Styling</a></li>
            </ul>
            <div class="tab-content tab-content-1 current">
    ',
    'after_row'    => '</div>',
) );

$content_sections->add_group_field( $group_field_id, array(
    'name' => esc_html__( 'Inner CSS classes', 'cmb2' ),
    'id'   => 'section-css-classes',
    'type' => 'text_medium',
    'default' => 'page-width',
    'row_classes'    => 'half',
    'before_row'   => '<div class="tab-content tab-content-2"><div class="row">',
) );

$content_sections->add_group_field( $group_field_id, array(
    'name' => esc_html__( 'Section ID', 'cmb2' ),
    'id'   => 'section-ID',
    'type' => 'text_medium',
    'row_classes'    => 'half',
    'after_row'   => '</div>',
) );

$content_sections->add_group_field( $group_field_id, array(
    'name' => esc_html__( 'Outer CSS classes', 'cmb2' ),
    'id'   => 'section-css-classes-2',
    'type' => 'text_medium',
    'row_classes'    => 'half',
    'before_row'   => '<div class="row">',
) );

$content_sections->add_group_field( $group_field_id, array(
    'name'    => esc_html__( 'Background Color', 'cmb2' ),
    'id'   => 'section-background-color',
    'type' => 'colorpicker',
    'default'  => '',
    'desc'    => esc_html__( 'Background color for section', 'cmb2' ),
    'row_classes'    => 'half',
    'after_row'   => '</div>',
) );

$content_sections->add_group_field( $group_field_id, array(
    'name' => esc_html__( 'Background Image', 'cmb2' ),
    'id'   => 'section-image',
    'type' => 'file',
    'options' => array(
        'url' => false, // Hide the text input for the url
    ),
    'desc'    => esc_html__( '&#8776;1400px x 915px', 'cmb2' ),
) );

$content_sections->add_group_field( $group_field_id, array(
    'name'             => esc_html__( 'Parallax Background', 'cmb2' ),
    'desc'             => esc_html__( 'Does not work on mobiles.', 'cmb2' ),
    'id'               => 'section-parallax',
    'type'             => 'radio_inline',
    'default' => 'true',
    'options'          => array(
        'false' => esc_html__( 'Off', 'cmb2' ),
        'true'   => esc_html__( 'On', 'cmb2' ),
    ),
    'after_row'    => '
            </div><!-- /.tab-content -->
        </div><!-- /.cmb2-tabs -->
    ',
) );

Then I included this javascript file in the admin:

jQuery(document).ready(function($) {
    $('.tab-content:not(.current)').css('display', 'none');
    $('body').on('click', '.tabs-nav a', function(event) {
        event.preventDefault();
        $(this).parent().addClass("current");
        $(this).parent().siblings().removeClass("current");
        var tab = $(this).attr("href");
        tab = tab.replace("#", "."); // change anchor tag # (id ref) to . (class ref) so it works with repeating sections
        // alert(tab);
        $(this).closest('.cmb2-tabs').children('.tab-content').not(tab).css('display', 'none');
        $(this).closest('.cmb2-tabs').children(tab).fadeIn();
    });
});

And included a css file in the admin:

.no-js .cmb2-tabs { display: none; }

.cmb2-tabs .tabs-nav
{
    margin: 6px 0 0 10px;
    clear: both;
    overflow: hidden;
}

.cmb2-tabs .tabs-nav li {
    list-style: none;
    float: left;
    position: relative;
    top: 0;
    margin: 1px .2em 0 0;
    padding: 0;
    white-space: nowrap;
    border: 1px #CCC solid;
    background-color: #EEE;
}

.cmb2-tabs .tabs-nav li a {
    display: block;
    text-decoration: none;
    font-size: 14px;
    line-height: 18px;
    padding: 10px 20px;
}

.cmb2-tabs .tabs-nav li.current {
     background-color: #FFF;
     border-bottom: 1px #FFF solid;
 }

.cmb2-tabs ul li a:focus {
    -webkit-box-shadow: none;
    box-shadow: none
}

.cmb2-tabs .tab-content
{
    padding: 10px 20px;
    border: 1px #CCC solid;
    border-bottom: 1px #CCC solid;
    margin-top: -1px;
    background-color: #FFF;
}
@polupraneeth

This comment has been minimized.

polupraneeth commented Jul 11, 2017

I have made a simple CMB2 Tabs extension that solves the problem.
CMB2 Tabs
https://github.com/stackadroit/cmb2-extensions

@manzoorwanijk

This comment has been minimized.

Contributor

manzoorwanijk commented Nov 30, 2017

Do I still need my own tweaking/extension or there is something in the core to create tabs?

@tw2113

This comment has been minimized.

Contributor

tw2113 commented Nov 30, 2017

Still need to do your own thing last I knew. I haven't seen any changes made/released with CMB2 core that would indicate otherwise.

@JiveDig

This comment has been minimized.

JiveDig commented May 29, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment