How to extend Customizer Panels and Sections to allow nesting – WordPress

The Customizer in WordPress is comprised of Panels, Sections, Settings and Controls. Panels are the top-most fields which contain the Sections and the Sections further include the Controls. The Settings are associated with the Controls. This is the basic architecture of the Customizer. By default, only Sections can be present in a Panel while Sections can only include Controls. This is sufficient for most of the cases but in some situations, the Customizer needs to be extended with multiple Panels and Sections especially if the number of Panels starts increasing. Everything starts appearing rather unorganised and haphazard. In this article, we are going to take a look at how to extend the customizer to allow nesting ( a Panel inside a Panel and a Section inside a Section ) so as to make it more organised and ordered.

First of all, create a new file in your theme directory, where all your JS files are placed, preferably in the js folder. For this example, we are naming it extend-customizer.js. Copy the following code in the extend-customizer.js file-

( function( $ ) {

  var api = wp.customize;

  api.bind( 'pane-contents-reflowed', function() {

    // Reflow sections
    var sections = [];

    api.section.each( function( section ) {

      if (
        'custom_section' !== section.params.type ||
        'undefined' === typeof section.params.section
      ) {

        return;

      }

      sections.push( section );

    });

    sections.sort( api.utils.prioritySort ).reverse();

    $.each( sections, function( i, section ) {

      var parentContainer = $( '#sub-accordion-section-' + section.params.section );

      parentContainer.children( '.section-meta' ).after( section.headContainer );

    });

    // Reflow panels
    var panels = [];

    api.panel.each( function( panel ) {

      if (
        'custom_panel' !== panel.params.type ||
        'undefined' === typeof panel.params.panel
      ) {

        return;

      }

      panels.push( panel );

    });

    panels.sort( api.utils.prioritySort ).reverse();

    $.each( panels, function( i, panel ) {

      var parentContainer = $( '#sub-accordion-panel-' + panel.params.panel );

      parentContainer.children( '.panel-meta' ).after( panel.headContainer );

    });

  });


  // Extend Panel
  var _panelEmbed = wp.customize.Panel.prototype.embed;
  var _panelIsContextuallyActive = wp.customize.Panel.prototype.isContextuallyActive;
  var _panelAttachEvents = wp.customize.Panel.prototype.attachEvents;

  wp.customize.Panel = wp.customize.Panel.extend({
    attachEvents: function() {

      if (
        'custom_panel' !== this.params.type ||
        'undefined' === typeof this.params.panel
      ) {

        _panelAttachEvents.call( this );

        return;

      }

      _panelAttachEvents.call( this );

      var panel = this;

      panel.expanded.bind( function( expanded ) {

        var parent = api.panel( panel.params.panel );

        if ( expanded ) {

          parent.contentContainer.addClass( 'current-panel-parent' );

        } else {

          parent.contentContainer.removeClass( 'current-panel-parent' );

        }

      });

      panel.container.find( '.customize-panel-back' )
        .off( 'click keydown' )
        .on( 'click keydown', function( event ) {

          if ( api.utils.isKeydownButNotEnterEvent( event ) ) {

            return;

          }

          event.preventDefault(); // Keep this AFTER the key filter above

          if ( panel.expanded() ) {

            api.panel( panel.params.panel ).expand();

          }

        });

    },
    embed: function() {

      if (
        'custom_panel' !== this.params.type ||
        'undefined' === typeof this.params.panel
      ) {

        _panelEmbed.call( this );

        return;

      }

      _panelEmbed.call( this );

      var panel = this;
      var parentContainer = $( '#sub-accordion-panel-' + this.params.panel );

      parentContainer.append( panel.headContainer );

    },
    isContextuallyActive: function() {

      if (
        'custom_panel' !== this.params.type
      ) {

        return _panelIsContextuallyActive.call( this );

      }

      var panel = this;
      var children = this._children( 'panel', 'section' );

      api.panel.each( function( child ) {

        if ( ! child.params.panel ) {

          return;

        }

        if ( child.params.panel !== panel.id ) {

          return;

        }

        children.push( child );

      });

      children.sort( api.utils.prioritySort );

      var activeCount = 0;

      _( children ).each( function ( child ) {

        if ( child.active() && child.isContextuallyActive() ) {

          activeCount += 1;

        }

      });

      return ( activeCount !== 0 );

    }

  });


  // Extend Section
  var _sectionEmbed = wp.customize.Section.prototype.embed;
  var _sectionIsContextuallyActive = wp.customize.Section.prototype.isContextuallyActive;
  var _sectionAttachEvents = wp.customize.Section.prototype.attachEvents;

  wp.customize.Section = wp.customize.Section.extend({
    attachEvents: function() {

      if (
        'custom_section' !== this.params.type ||
        'undefined' === typeof this.params.section
      ) {

        _sectionAttachEvents.call( this );

        return;

      }

      _sectionAttachEvents.call( this );

      var section = this;

      section.expanded.bind( function( expanded ) {

        var parent = api.section( section.params.section );

        if ( expanded ) {

          parent.contentContainer.addClass( 'current-section-parent' );

        } else {

          parent.contentContainer.removeClass( 'current-section-parent' );

        }

      });

      section.container.find( '.customize-section-back' )
        .off( 'click keydown' )
        .on( 'click keydown', function( event ) {

          if ( api.utils.isKeydownButNotEnterEvent( event ) ) {

            return;

          }

          event.preventDefault(); // Keep this AFTER the key filter above

          if ( section.expanded() ) {

            api.section( section.params.section ).expand();

          }

        });

    },
    embed: function() {

      if (
        'custom_section' !== this.params.type ||
        'undefined' === typeof this.params.section
      ) {

        _sectionEmbed.call( this );

        return;

      }

      _sectionEmbed.call( this );

      var section = this;
      var parentContainer = $( '#sub-accordion-section-' + this.params.section );

      parentContainer.append( section.headContainer );

    },
    isContextuallyActive: function() {

      if (
        'pe_section' !== this.params.type
      ) {

        return _sectionIsContextuallyActive.call( this );

      }

      var section = this;
      var children = this._children( 'section', 'control' );

      api.section.each( function( child ) {

        if ( ! child.params.section ) {

          return;

        }

        if ( child.params.section !== section.id ) {

          return;

        }

        children.push( child );

      });

      children.sort( api.utils.prioritySort );

      var activeCount = 0;

      _( children ).each( function ( child ) {

        if ( 'undefined' !== typeof child.isContextuallyActive ) {

          if ( child.active() && child.isContextuallyActive() ) {

            activeCount += 1;

          }

        } else {

          if ( child.active() ) {

            activeCount += 1;

          }

        }

      });

      return ( activeCount !== 0 );

    }

  });

})( jQuery );

Without going into much details, this code enables the nesting functionality for Customizer. This file needs to be enqueued to the customize_controls_enqueue_scripts hook. Add the following code to your functions.php –

function yourtheme_customize_controls_scripts() {
  wp_enqueue_script( 'yourtheme-customize-controls', get_theme_file_uri( '/assets/js/extend-customizer.js' ), array(), '1.0', true );
}
add_action( 'customize_controls_enqueue_scripts', 'pe_customize_controls_scripts' );

Keep in mind the path of the file in this code. It needs to point to the location of the file.

Now, we are done with the integration part. The framework has been set up. Now, adding the CSS styling so that everything seems coherent. Create a file named customize-style.css and copy the following code to it-

.in-sub-panel #customize-theme-controls .customize-pane-child.current-panel-parent,
#customize-theme-controls .customize-pane-child.current-section-parent {
  -webkit-transform: translateX(-100%);
  -ms-transform: translateX(-100%);
  transform: translateX(-100%);
}

This file has to be enqueued to the customize_controls_print_styles hook. Add the following code to functions.php –

function pe_customize_controls_styles() {
  wp_enqueue_style( 'yourtheme-customize-controls', get_theme_file_uri( '/assets/css/customize-style.css' ), array(), '1.0' );
}

Now, all we ned to do is define the custom controls and use them in the customizer. To define the Custom Panels and Sections, add the following code in customizer.php ( usually it is present in the inc/ folder in the theme directory ) –

<?php
if ( class_exists( 'WP_Customize_Panel' ) ) {
  class YourTheme_WP_Customize_Panel extends WP_Customize_Panel {
    public $panel;
    public $type = 'custom_panel';
    public function json() {
      $array = wp_array_slice_assoc( (array) $this, array( 'id', 'description', 'priority', 'type', 'panel', ) );
      $array['title'] = html_entity_decode( $this->title, ENT_QUOTES, get_bloginfo( 'charset' ) );
      $array['content'] = $this->get_content();
      $array['active'] = $this->active();
      $array['instanceNumber'] = $this->instance_number;
      return $array;
    }
  }
}
if ( class_exists( 'WP_Customize_Section' ) ) {
  class YourTheme_WP_Customize_Section extends WP_Customize_Section {
    public $section;
    public $type = 'custom_section';
    public function json() {
      $array = wp_array_slice_assoc( (array) $this, array( 'id', 'description', 'priority', 'panel', 'type', 'description_hidden', 'section', ) );
      $array['title'] = html_entity_decode( $this->title, ENT_QUOTES, get_bloginfo( 'charset' ) );
      $array['content'] = $this->get_content();
      $array['active'] = $this->active();
      $array['instanceNumber'] = $this->instance_number;
      if ( $this->panel ) {
        $array['customizeAction'] = sprintf( 'Customizing &#9656; %s', esc_html( $this->manager->get_panel( $this->panel )->title ) );
      } else {
        $array['customizeAction'] = 'Customizing';
      }
      return $array;
    }
  }
}
?>

In this code, the WP_Customize_Panel and WP_Customize_Section classes have been extended to create custom Panels and Sections. If you closely observe the codes of the custom classes, the number of elements to be passed to the arrays while defining the Panels and Sections have been changed. There is now a provision to define a panel in the Custom Panel class which is not there by default. Same goes for the Custom Sections. Now, Sections can be defined in Custom Sections.

This sets up the Custom Panels and Custom Sections. All that is left now is to initialize the Panels and Controls in the Customizer. Let’s set up a Control ( Control 1 ) in a Section ( Section 2 ) which is set up in another Section ( Section 1 ) which is nested in a Panel ( Panel 2 ) and this Panel is nested in another Panel ( Panel 1 ). Add the following code in customizer.php –

function custom_customize_register ( $wp_customize ) {

  $wp_customize->register_panel_type( 'PE_WP_Customize_Panel' );
  $wp_customize->register_section_type( 'PE_WP_Customize_Section' );

  add_panel( new YourTheme_WP_Customize_Panel (  $wp_customize, 'lvl_1_parent_panel', array(
    'title' => 'Panel 1',
    'priority' => 10,
  )));

  add_panel( new YourTheme_WP_Customize_Panel (  $wp_customize, 'lvl_2_parent_panel', array(
    'title' => 'Panel 2',
    'panel' => 'lvl_1_parent_panel'
  )));

  add_section( new YourTheme_WP_Customize_Section ( $wp_customize, 'lvl_1_parent_section', array(
    'title' => 'Section 1',
    'panel' => 'lvl_2_parent_panel',
  )));

  add_section( new YourTheme_WP_Customize_Section ( $wp_customize, 'lvl_2_parent_section', array(
    'title' => 'Section 2',
    'panel' => 'lvl_2_parent_panel',
    'section' => 'lvl_1_parent_section'
  )));

  add_setting ( 'custom_control', array(
    'default' => '',
    'sanitize_callback' => 'sanitize_text_field'
  ));

  add_control( 'custom_text', array(
    'label'  => __('Control 1', 'your-theme'),
    'type'   => 'text',
    'section' => 'lvl_2_parent_section'
  ));
}

add_action( 'customize_register', 'custom_customize_register');

Now, you should be able to see a new Panel in the Customizer, named ‘Panel 1’ with ‘Panel 2’ nested in it. In ‘Panel 2’, the ‘Section 1’ is present and ‘Section 2’ is nested in ‘Section 1’ and finally, the Text Control ‘Control 1’ is present in ‘Section 2’ resulting in nested Panels and Sections.

I would like to thank Ante Sepic for this awesome extension to Customizer. I have even used in some of my projects and the results are EPIC. Hope you like it as well.

Leave a Reply

Your email address will not be published. Required fields are marked *