1

I'm trying to make a panel in customizer whose sections are sortable and the order of the sections determines the order of the areas they control at the front-end. I referred to the following question - possible to make sections in theme customizer sortable and saveable at publish button clicked?

Everything seems to be working fine. But when I try to sort the elements, sorting fails for the first time and items don't change their order. It works normally second time onwards. It's the first time that causes issues.

Here is the PHP code I used to add sections -

function itpg_front_page() {
    $wp_customize->add_panel(
        'itpg_front_page', array(
            'title'         =>  __('Front Page', 'it-photographer'),
            'description'   =>  __('Modules for the Static Front Page.<br/>Will work on Static Front page with Front Page Template set.', 'it-photographer'),
            'priority'      =>  30
        )
    );
    
    //Sections for te Panel. This is currently in default order but will vary depending on value of 'itpg_front_order'
    
    $default_sections = array(
        
        'itpg_hero' => array(
            'title'         =>  __('Hero', 'it-photographer'),
            'description'   =>  '',
            'panel'         =>  'itpg_front_page'
        ),
    
        'itpg_home_portfolio' => array(
            'title'         =>  __('Projects', 'it-photographer'),
            'description'   =>  __('Jetpack needs to be installed and Portfolios need to be activated for this section to work', 'it-photographer'),
            'panel'         => 'itpg_front_page'
        ),
    
        'itpg_home_testimonial' => array(
            'title'         =>  __('Testimonials', 'it-photographer'),
            'description'   =>  __('Jetpack needs to be installed and Testimonials need to be activated for this section to work', 'it-photographer'),
            'panel'         => 'itpg_front_page'
        ),
    
        'itpg_front_blog' => array(
            'title'         =>  __('From the Blog', 'it-photographer'),
            'description'   =>  '',
            'panel'         =>  'itpg_front_page'
        ),
    
        'itpg_cat_tabs' => array(
            'title'         =>  __('Category Tabs', 'it-photographer'),
            'description'   =>  '',
            'panel'         =>  'itpg_front_page'
        ),
    
        'itpg_counters' => array(
            'title'         =>  __('Counters', 'it-photographer'),
            'description'   =>  '',
            'panel'         =>  'itpg_front_page'
        )
    );
    
    $sortable_sections = get_theme_mod('itpg_front_order');
    
    if( !isset( $sortable_sections ) || empty( $sortable_sections ) ) {
        
        set_theme_mod( 'itpg_hidden_front_order', implode(',', array_keys( $default_sections ) ) );
        $sortable_sections = get_theme_mod('itpg_front_order');
        
    }
    
    $sortable_sections = explode(',', $sortable_sections );
    
    foreach( $sortable_sections as $sortable_section ){
        $wp_customize->add_section( $sortable_section, array(
            'title'       => $default_sections[$sortable_section]['title'],
            'description' => $default_sections[$sortable_section]['description'],
            'panel'       => 'itpg_front_page'
        ) );
    }
    $wp_customize->add_section(
        'itpg_hidden_front_order', array(
        'title'     =>  __('Front Page Order', 'it-photographer'),
        'panel'     =>  'itpg_panel_front_page'
        )
    );
    $wp_customize->add_setting(
        'itpg_front_order', array(
            'default'   =>  '',
            'sanitize_callback' =>  'sanitize_text_field'
        )
    );

    $wp_customize->add_control(
        'itpg_front_order', array(
             'type'     =>  'hidden',
            'section'   =>  'itpg_hidden_front_order'
        )
    );

    ...Added Controls for the respective sections...

}
add_action('customize_register', 'itpg_front_page');

The CSS nad JS files are enqueued in the customizer using customize_controls_enqueue_scripts -

function itpg_customize_control_scripts() {
    wp_enqueue_script( 'jquery-ui-sortable' );
    wp_enqueue_script("itpg-customize-control-js", esc_url(get_template_directory_uri() . "/assets/js/customize_controls.js"), array(), ITPG_VERSION, true );
}
add_action("customize_controls_enqueue_scripts", "itpg_customize_control_scripts");

function itpg_custom_admin_styles() {
    wp_enqueue_style( 'itpg-admin-css', esc_url( get_template_directory_uri() . '/assets/theme-styles/css/admin.css' ), array(), ITPG_VERSION );
}
add_action( 'admin_enqueue_scripts', 'itpg_custom_admin_styles' );

Since sortable jQuery plugin is to be used, I enqueued it from WordPress.

There are 6 sections in the above code that need to be re-arranged. I created another section in which the control stores the order of the sections.

I used CSS to hide the section with hidden input. Added the following code in admin.css -

li#accordion-section-itpg_hidden_front_order {
    display: none !important;
}

Here is my JS code I wrote in customize_controls.js -

jQuery(document).ready(function() {

 var frontPanel, frontSections, hiddenSection, hiddenAcc, order
    
frontPanel      = jQuery('#sub-accordion-panel-itpg_front_page')
frontSections   = frontPanel.find('li[id^="accordion-section-"]').not('#accordion-section-itpg_hidden_front_order')
hiddenAcc       = jQuery('#accordion-section-itpg_hidden_front_order')
hiddenPanel     = jQuery('#' + hiddenAcc.attr('aria-owns'));

//Added 'itpg_draggable' class to all the visible sections
frontSections.each(function() {
    jQuery(this).addClass('itpg_draggable') 
})

//Initialised the sortable for all items containing 'itpg_draggable' class
frontPanel.sortable({
    item: 'li.itpg_draggable',
    axis: 'y',
    stop: function( event, ui ) {
        getOrder()
    }
})

//Get ids of all the sections of Front Page and save them in the 'itpg_front_order' setting
const getOrder = () => {
    
    order = frontPanel.find('.itpg_draggable').map( function() {
        
        var id = jQuery(this).attr('id'),
            id = id.replace('accordion-section-','');
        
        return id;
    }).get().join(',');
    
    //Save the value in the hidden input field
    hiddenPanel.find('input').prop('value', order)

    //Make sure to trigger the change event to enable the Publish Button
    hiddenPanel.find('input').trigger('change')
    
}
}

When I comment out the trigger event that is used to let Publish Button know that a change needs to be saved, it works fine but setting cannot be saved.

Any help would be appreciated! Thanks

9
  • What's the error message?
    – Tom J Nowell
    Aug 14, 2021 at 20:36
  • There is no error. Its just that sortable is not working the way it should. Aug 14, 2021 at 20:38
  • Are you sure the dev consoles error log contains no errors? How do you know that sorting has failed? I notice there is no PHP in your code, no HTML markup, and the JS looks incomplete
    – Tom J Nowell
    Aug 14, 2021 at 20:42
  • Made some changes to the code. Earlier pasted an erroneous version. Yes, I've checked the dev console for errors. There are none. Aug 14, 2021 at 20:48
  • Here's a video to make things more clear - dropbox.com/s/wehe5zzkyiw5yfd/customizer-err.mov?dl=0 Aug 14, 2021 at 20:54

1 Answer 1

1

I think what your issue may be is that you aren't setting the priority of the sections after you manually change their order in the DOM, so once the controls UI reflows the manual change may be getting lost.

Your code can be simplified as well by not having to register hidden section or control. You can register just one setting that contains the theme mod and update it directly via the Customizer API without having to trigger a change on an input.

Here is the simplified JS code:

wp.customize.bind( 'ready', () => {
    const orderSetting = wp.customize( 'itpg_front_order' );
    const panel = wp.customize.panel( 'itpg_front_page' );

    panel.contentContainer.sortable({
        item: '.control-section',
        axis: 'y',
        stop: () => {
            // Get the sections in the new order.
            const newlySortedSections = [ ...panel.sections() ].sort( ( a, b ) => {
                return a.container.index() - b.container.index();
            } );

            // Persist the new order as part of the changeset (and trigger preview update).
            orderSetting.set(
                newlySortedSections.map( ( section ) => section.id ).join( ',' )
            );
        }
    });

    // When the order setting is updated, make sure the sections get the appropriate priorities.
    // By setting the priorities in this way, the setting can be updated programmatically and the UI will
    // update accordingly. This two-way data binding.
    orderSetting.bind( ( newOrder ) => {
        const newlyOrderedSections = newOrder.split( /,/ ).map( ( sectionId ) => wp.customize.section( sectionId ) );
        newlyOrderedSections.forEach( ( section, i ) => {
            section.priority( i );
        } );
    } );
} );

That JS code can be enqueued as follows, assuming stored in a customize-controls.js file in a plugin:

add_action(
    'customize_controls_enqueue_scripts',
    static function() {
        wp_enqueue_script(
            'itpg-customize-control-js',
            plugin_dir_url( __FILE__ ) . 'customize-controls.js',
            array( 'jquery-ui-sortable', 'customize-controls' ),
            false,
            true
        );
    }
);

And then here is the updated logic for registering the Customizer panel, sections, and setting:

add_action(
    'customize_register',
    static function ( WP_Customize_Manager $wp_customize ) {
        $panel = $wp_customize->add_panel(
            'itpg_front_page',
            array(
                'title'       => __( 'Front Page', 'it-photographer' ),
                'description' => __( 'Modules for the Static Front Page.<br/>Will work on Static Front page with Front Page Template set.', 'it-photographer' ),
                'priority'    => 30,
            )
        );

        $sections = array(
            'itpg_hero' => array(
                'title'       => __( 'Hero', 'it-photographer' ),
                'description' => '',
                'panel'       => 'itpg_front_page'
            ),

            'itpg_home_portfolio' => array(
                'title'       => __( 'Projects', 'it-photographer' ),
                'description' => __( 'Jetpack needs to be installed and Portfolios need to be activated for this section to work', 'it-photographer' ),
                'panel'       => 'itpg_front_page'
            ),

            'itpg_home_testimonial' => array(
                'title'       => __( 'Testimonials', 'it-photographer' ),
                'description' => __( 'Jetpack needs to be installed and Testimonials need to be activated for this section to work', 'it-photographer' ),
                'panel'       => 'itpg_front_page'
            ),

            'itpg_front_blog' => array(
                'title'       => __( 'From the Blog', 'it-photographer' ),
                'description' => '',
                'panel'       => 'itpg_front_page'
            ),

            'itpg_cat_tabs' => array(
                'title'       => __( 'Category Tabs', 'it-photographer' ),
                'description' => '',
                'panel'       => 'itpg_front_page'
            ),

            'itpg_counters' => array(
                'title'       => __( 'Counters', 'it-photographer' ),
                'description' => '',
                'panel'       => 'itpg_front_page'
            )
        );

        $theme_mod_id  = 'itpg_front_order';
        $section_order = get_theme_mod( $theme_mod_id, array_keys( $sections ) );

        foreach ( $sections as $section_id => $section ) {
            $section = $wp_customize->add_section(
                $section_id,
                array(
                    'title'       => $section['title'],
                    'description' => $section['description'],
                    'panel'       => $panel->id,

                    // Note the order here will change based on what was saved in the theme mod.
                    'priority'    => (int) array_search( $section_id, $section_order, true ),
                )
            );

            // Add control to each section just so that it is not hidden.
            // Actual controls would be registered here instead.
            $wp_customize->add_control(
                "{$section->id}-placeholder",
                [
                    'label'       => 'Placeholder',
                    'description' => 'This control is registered purely to make the section not be hidden.',
                    'type'        => 'text',
                    'section'     => $section->id,
                    'settings'    => [],
                ]
            );
        }

        // Register a setting for the theme mod which contains the section order.
        $wp_customize->add_setting(
            $theme_mod_id,
            array(
                'type'              => 'theme_mod',
                'default'           => $section_order,
                'sanitize_callback' => function ( $value) use ( $sections ) {
                    return array_intersect(
                        explode( ',', $value ),
                        array_keys( $sections )
                    );
                },
            )
        );
    }
);
2
  • WOW! Thanks a lot and I really appreciate you helping me out with this. I feel upgraded learning from the best! Aug 19, 2021 at 6:53
  • I just improved the JS logic a bit more to implement two-way data binding, so that if the order setting is updated programmatically the sections will then automatically get reordered accordingly. Aug 19, 2021 at 18:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.