Skip to content
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 · 38 comments
Open

Allow groups of fields to be broken into tabs #5

billerickson opened this issue Aug 20, 2014 · 38 comments

Comments

@billerickson
Copy link
Collaborator

@billerickson 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
Copy link
Member

@jtsternberg jtsternberg commented Aug 21, 2014

definitely 👍

@jtsternberg
Copy link
Member

@jtsternberg 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
Copy link
Member

@jtsternberg 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
Copy link
Collaborator Author

@billerickson billerickson commented Sep 3, 2014

Yep, looks good to me!

@robneu
Copy link

@robneu 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
Copy link

@rogerlos 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
Copy link
Member

@jtsternberg 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
Copy link

@teamcrisis teamcrisis commented Mar 24, 2015

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

@ashawkat
Copy link

@ashawkat ashawkat commented May 19, 2015

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

@jtsternberg
Copy link
Member

@jtsternberg 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
Copy link

@jashwant 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
Copy link

@ghost 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
Copy link

@StaggerLeee StaggerLeee commented Jun 9, 2015

I vote for this too. Very important.

@jashwant
Copy link

@jashwant jashwant commented Jun 27, 2015

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

@vaclavgreif
Copy link

@vaclavgreif vaclavgreif commented Jul 14, 2015

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

@themarcusbattle
Copy link

@themarcusbattle 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
Copy link

@jashwant 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 ?

@vaclavgreif
Copy link

@vaclavgreif vaclavgreif 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
Copy link

@willthemoor 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
Copy link

@hsleonis hsleonis commented Mar 1, 2016

Any update on this?
@marcusbattle

@rogerlos
Copy link

@rogerlos 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
Copy link

@hsleonis hsleonis commented Mar 1, 2016

Nice work @rogerlos

@DevinWalker
Copy link
Contributor

@DevinWalker DevinWalker commented May 10, 2016

Yeah nice work @rogerlos

@darkoromanov
Copy link

@darkoromanov 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
Copy link

@hsleonis 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
Copy link

@darkoromanov 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
Copy link

@hsleonis hsleonis commented Sep 7, 2016

Ha ha ha

@andreasupftw
Copy link

@andreasupftw andreasupftw commented Sep 16, 2016

@websumon
Copy link

@websumon websumon commented Oct 22, 2016

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

@kuzmenko1256
Copy link

@kuzmenko1256 kuzmenko1256 commented Nov 9, 2016

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

@WhereBeTheDan
Copy link

@WhereBeTheDan 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
Copy link

@stabilimenta 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
Copy link

@polupraneeth 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
Copy link
Contributor

@manzoorwanijk manzoorwanijk commented Nov 30, 2017

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

@tw2113
Copy link
Contributor

@tw2113 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
Copy link

@JiveDig JiveDig commented May 29, 2018

@sharmashivanand
Copy link

@sharmashivanand sharmashivanand commented Apr 20, 2019

The update in #5 (comment) seems tobe only available for standalone options page. Guess we are still looking for a solution for tabs inside metabox.

@dskurth
Copy link

@dskurth dskurth commented Jul 15, 2019

@jtsternberg,
Similar question, but instead of breaking up fields into tab groups, is there a way to break up repeatable records into separate groups? Such as posts do? For example: https://snag.gy/njsJpF.jpg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet