Skip to content

Instantly share code, notes, and snippets.

@westonruter
Last active July 16, 2023 13:11
Show Gist options
  • Save westonruter/7f2b9c18113f0576a72e0aca3ce3dbcb to your computer and use it in GitHub Desktop.
Save westonruter/7f2b9c18113f0576a72e0aca3ce3dbcb to your computer and use it in GitHub Desktop.
Code answering this WPSE question: https://wordpress.stackexchange.com/a/373092/8521
(function () {
// Augment each menu item control once it is added and embedded.
wp.customize.control.bind( 'add', ( control ) => {
if ( control.extended( wp.customize.Menus.MenuItemControl ) ) {
control.deferred.embedded.done( () => {
extendControl( control );
} );
}
} );
/**
* Extend the control with roles information.
*
* @param {wp.customize.Menus.MenuItemControl} control
*/
function extendControl( control ) {
control.authFieldset = control.container.find( '.nav_menu_role_authentication' );
control.rolesFieldset = control.container.find( '.nav_menu_roles' );
// Set the initial UI state.
updateControlFields( control );
// Update the UI state when the setting changes programmatically.
control.setting.bind( () => {
updateControlFields( control );
} );
// Update the setting when the inputs are modified.
control.authFieldset.find( 'input' ).on( 'click', function () {
setSettingRoles( control.setting, this.value );
} );
control.rolesFieldset.find( 'input' ).on( 'click', function () {
const checkedRoles = [];
control.rolesFieldset.find( ':checked' ).each( function () {
checkedRoles.push( this.value );
} );
setSettingRoles( control.setting, checkedRoles.length === 0 ? 'in' : checkedRoles );
} );
}
/**
* Extend the setting with roles information.
*
* @param {wp.customize.Setting} setting
* @param {string|Array} roles
*/
function setSettingRoles( setting, roles ) {
setting.set(
Object.assign(
{},
_.clone( setting() ),
{ roles }
)
);
}
/**
* Apply the control's setting value to the control's fields.
*
* @param {wp.customize.Menus.MenuItemControl} control
*/
function updateControlFields( control ) {
const roles = control.setting().roles || '';
const radioValue = _.isArray( roles ) ? 'in' : roles;
const checkedRoles = _.isArray( roles ) ? roles : [];
control.rolesFieldset.toggle( 'in' === radioValue );
const authRadio = control.authFieldset.find( `input[type=radio][value="${ radioValue }"]` );
authRadio.prop( 'checked', true );
control.rolesFieldset.find( 'input[type=checkbox]' ).each( function () {
this.checked = checkedRoles.includes( this.value );
} );
}
})();
<?php
/**
* Plugin Name: Customize Nav Menu Roles
* Plugin URI: https://gist.github.com/westonruter/7f2b9c18113f0576a72e0aca3ce3dbcb
* Description: Extend nav menu item controls with UI for manipulating the user roles that are allowed to view them. Extension of the <a href="https://wordpress.org/plugins/nav-menu-roles/">Nav Menu Roles</a> plugin by Kathy Darling.
* Version: 0.1
* Author: Weston Ruter
* Author URI: https://weston.ruter.net/
* License: GNU General Public License v2 (or later)
* License URI: http://www.gnu.org/licenses/gpl-2.0.html
* Gist Plugin URI: https://gist.github.com/westonruter/7f2b9c18113f0576a72e0aca3ce3dbcb
*/
namespace Customize_Nav_Menu_Roles;
use WP_Customize_Manager;
use WP_Customize_Nav_Menu_Item_Setting;
/**
* Display the fields in the Customizer.
*
* This function is copied from a Stack Overflow question.
*
* @link https://wordpress.stackexchange.com/questions/372493/add-settings-to-menu-items-in-the-customizer
*/
function kia_customizer_custom_fields() {
global $wp_roles;
/**
* Pass the menu item to the filter function.
* This change is suggested as it allows the use of information from the menu item (and
* by extension the target object) to further customize what filters appear during menu
* construction.
*/
$display_roles = apply_filters( 'nav_menu_roles', $wp_roles->role_names );
if ( ! $display_roles ) {
return;
}
?>
<fieldset class="nav_menu_role_authentication">
<legend class="customize-control-title"><?php _e( 'Display Mode', 'nav-menu-roles' ); ?></legend>
<label for="edit-menu-item-role_logged_in-{{ data.menu_item_id }}">
<input type="radio" id="edit-menu-item-role_logged_in-{{ data.menu_item_id }}" value="in" name="menu-item-role-{{ data.menu_item_id }}" />
<?php _e( 'Logged In Users', 'nav-menu-roles' ); ?><br/>
</label>
<label for="edit-menu-item-role_logged_out-{{ data.menu_item_id }}">
<input type="radio" id="edit-menu-item-role_logged_out-{{ data.menu_item_id }}" value="out" name="menu-item-role-{{ data.menu_item_id }}" />
<?php _e( 'Logged Out Users', 'nav-menu-roles' ); ?><br/>
</label>
<label for="edit-menu-item-role_everyone-{{ data.menu_item_id }}">
<input type="radio" id="edit-menu-item-role_everyone-{{ data.menu_item_id }}" value="" name="menu-item-role-{{ data.menu_item_id }}" />
<?php _e( 'Everyone', 'nav-menu-roles' ); ?><br/>
</label>
</fieldset>
<fieldset class="nav_menu_roles">
<legend class="customize-control-title"><?php _e( 'Restrict menu item to minimum role', 'nav-menu-roles' ); ?></legend>
<?php foreach ( $display_roles as $role => $name ) : ?>
<label for="edit-menu-item-role_<?php echo $role; ?>-{{ data.menu_item_id }}">
<input type="checkbox" id="edit-menu-item-role_<?php echo esc_attr( $role ); ?>-{{ data.menu_item_id }}" class="edit-menu-item-role" value="<?php echo esc_attr( $role ); ?>" />
<?php echo esc_html( $name ); ?><br/>
</label>
<?php endforeach; ?>
</fieldset>
<?php
}
add_action( 'wp_nav_menu_item_custom_fields_customize_template', __NAMESPACE__ . '\kia_customizer_custom_fields' );
// Enqueue script which extends nav menu item controls.
add_action(
'customize_controls_enqueue_scripts',
static function () {
wp_enqueue_script(
'customize-nav-menu-roles',
plugin_dir_url( __FILE__ ) . '/customize-nav-menu-roles.js',
[ 'customize-nav-menus' ],
filemtime( __DIR__ . '/customize-nav-menu-roles.js' ),
true
);
}
);
/**
* Sanitize roles value.
*
* @param string|array $value Roles.
* @return array|string Sanitized roles.
*/
function sanitize_roles_value( $value ) {
global $wp_roles;
if ( is_array( $value ) ) {
return array_intersect( $value, array_keys( $wp_roles->role_names ) );
} elseif ( in_array( $value, [ '', 'in', 'out' ], true ) ) {
return $value;
}
return '';
}
/**
* Get sanitized posted value for a setting's roles.
*
* @param WP_Customize_Nav_Menu_Item_Setting $setting Setting.
*
* @return array|string|null Roles value or null if no posted value present.
*/
function get_sanitized_roles_post_data( WP_Customize_Nav_Menu_Item_Setting $setting ) {
global $wp_roles;
if ( ! $setting->post_value() ) {
return null;
}
$unsanitized_post_value = $setting->manager->unsanitized_post_values()[ $setting->id ];
if ( isset( $unsanitized_post_value['roles'] ) ) {
$value = $unsanitized_post_value['roles'];
if ( is_array( $value ) ) {
return array_intersect( $value, array_keys( $wp_roles->role_names ) );
} elseif ( in_array( $value, [ '', 'in', 'out' ], true ) ) {
return $value;
}
}
return '';
}
/**
* Preview changes to the nav menu item roles.
*
* Note the unimplemented to-do in the doc block for the setting's preview method.
*
* @see WP_Customize_Nav_Menu_Item_Setting::preview()
*
* @param WP_Customize_Nav_Menu_Item_Setting $setting Setting.
*/
function preview_nav_menu_setting_postmeta( WP_Customize_Nav_Menu_Item_Setting $setting ) {
$roles = get_sanitized_roles_post_data( $setting );
if ( null === $roles ) {
return;
}
add_filter(
'get_post_metadata',
static function ( $value, $object_id, $meta_key ) use ( $setting, $roles ) {
if ( $object_id === $setting->post_id && '_nav_menu_role' === $meta_key ) {
return [ $roles ];
}
return $value;
},
10,
3
);
}
/**
* Save changes to the nav menu item roles.
*
* Note the unimplemented to-do in the doc block for the setting's preview method.
*
* @see WP_Customize_Nav_Menu_Item_Setting::update()
*
* @param WP_Customize_Nav_Menu_Item_Setting $setting Setting.
*/
function save_nav_menu_setting_postmeta( WP_Customize_Nav_Menu_Item_Setting $setting ) {
$roles = get_sanitized_roles_post_data( $setting );
if ( null !== $roles ) {
update_post_meta( $setting->post_id, '_nav_menu_role', $roles );
}
}
// Set up previewing.
add_action(
'customize_register',
static function( WP_Customize_Manager $wp_customize ) {
if ( $wp_customize->settings_previewed() ) {
foreach ( $wp_customize->settings() as $setting ) {
if ( $setting instanceof WP_Customize_Nav_Menu_Item_Setting ) {
preview_nav_menu_setting_postmeta( $setting );
}
}
}
},
1000
);
// Set up saving.
add_action(
'customize_save_after',
function ( WP_Customize_Manager $wp_customize ) {
foreach ( $wp_customize->settings() as $setting ) {
if ( $setting instanceof WP_Customize_Nav_Menu_Item_Setting && $setting->check_capabilities() ) {
save_nav_menu_setting_postmeta( $setting );
}
}
}
);
@DianaLaa
Copy link

DianaLaa commented Jul 15, 2023

Thanks for this code snippet. Very userful. However, I'm running into a problem. The preview functionality doesn't entirely work.

Scenario:

  • Go to Appearances > Menus, add a menu item and save the menu
  • Go to the Customizer and update the custom field for the menu item
  • Press Publish
  • The change is not visible in the preview pane
  • Refresh the Customizer page
  • The change is now visible in the preview pane

I was under the impression that the method hooked to customize_register would take care of this. It seems that upon pressing Publish, this is being called, and $wp_customize->settings_previewed() does return true, but the next statement, $wp_customize->settings() returns no settings to iterate.

Edit: Please note, this is a menu item that already exists and has been saved to the database. I'd expect that if I update the custom field and press Publish, the preview pane would reflect the updated field value.

Could you advise me please? What could I be missing?

@DianaLaa
Copy link

For others who might have the same problem, I found that reading $wp_customize->unsanitized_post_values() returns the submitted data.

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