$this->previewed_menus = array();
$this->manager = $manager;
+ // Skip useless hooks when the user can't manage nav menus anyway.
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ return;
+ }
+
+ add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) );
add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
+
+ // Selective Refresh partials.
+ add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
+ }
+
+ /**
+ * Adds a nonce for customizing menus.
+ *
+ * @since 4.5.0
+ * @access public
+ *
+ * @param array $nonces Array of nonces.
+ * @return array $nonces Modified array of nonces.
+ */
+ public function filter_nonces( $nonces ) {
+ $nonces['customize-menus'] = wp_create_nonce( 'customize-menus' );
+ return $nonces;
}
/**
$items = array();
if ( 'post_type' === $type ) {
- if ( ! get_post_type_object( $object ) ) {
+ $post_type = get_post_type_object( $object );
+ if ( ! $post_type ) {
return new WP_Error( 'nav_menus_invalid_post_type' );
}
'object' => '',
'url' => home_url(),
);
+ } elseif ( 'post' !== $object && 0 === $page && $post_type->has_archive ) {
+ // Add a post type archive link.
+ $items[] = array(
+ 'id' => $object . '-archive',
+ 'title' => $post_type->labels->archives,
+ 'type' => 'post_type_archive',
+ 'type_label' => __( 'Post Type Archive' ),
+ 'object' => $object,
+ 'url' => get_post_type_archive_link( $object ),
+ );
}
$posts = get_posts( array(
}
}
+ /**
+ * Filter the available menu items during a search request.
+ *
+ * @since 4.5.0
+ *
+ * @param array $items The array of menu items.
+ * @param array $args Includes 'pagenum' and 's' (search) arguments.
+ */
+ $items = apply_filters( 'customize_nav_menu_searched_items', $items, $args );
+
return $items;
}
// Pass data to JS.
$settings = array(
- 'nonce' => wp_create_nonce( 'customize-menus' ),
'allMenus' => wp_get_nav_menus(),
'itemTypes' => $this->available_item_types(),
'l10n' => array(
'untitled' => _x( '(no label)', 'missing menu item navigation label' ),
'unnamed' => _x( '(unnamed)', 'Missing menu name.' ),
'custom_label' => __( 'Custom Link' ),
- /* translators: %s: Current menu location */
- 'menuLocation' => __( '(Currently set to: %s)' ),
+ /* translators: %s: menu location */
+ 'menuLocation' => _x( '(Currently set to: %s)', 'menu' ),
'menuNameLabel' => __( 'Menu Name' ),
'itemAdded' => __( 'Menu item added' ),
'itemDeleted' => __( 'Menu item deleted' ),
'invalidTitleTpl' => __( '%s (Invalid)' ),
/* translators: %s: title of menu item in draft status */
'pendingTitleTpl' => __( '%s (Pending)' ),
- 'taxonomyTermLabel' => __( 'Taxonomy' ),
- 'postTypeLabel' => __( 'Post Type' ),
'itemsFound' => __( 'Number of items found: %d' ),
'itemsFoundMore' => __( 'Additional items found: %d' ),
'itemsLoadingMore' => __( 'Loading more results... please wait.' ),
'reorderLabelOn' => esc_attr__( 'Reorder menu items' ),
'reorderLabelOff' => esc_attr__( 'Close reorder mode' ),
),
- 'menuItemTransport' => 'postMessage',
+ 'settingTransport' => 'postMessage',
'phpIntMax' => PHP_INT_MAX,
'defaultSettingValues' => array(
'nav_menu' => $temp_nav_menu_setting->default,
'nav_menu_item' => $temp_nav_menu_item_setting->default,
),
+ 'locationSlugMappedToName' => get_registered_nav_menus(),
);
$data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings ) );
public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
$setting_args = array(
- 'type' => WP_Customize_Nav_Menu_Setting::TYPE,
+ 'type' => WP_Customize_Nav_Menu_Setting::TYPE,
+ 'transport' => 'postMessage',
);
} elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
$setting_args = array(
- 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
+ 'type' => WP_Customize_Nav_Menu_Item_Setting::TYPE,
+ 'transport' => 'postMessage',
);
}
return $setting_args;
) ) );
$menus = wp_get_nav_menus();
- // Menu loactions.
+ // Menu locations.
$locations = get_registered_nav_menus();
$num_locations = count( array_keys( $locations ) );
- $description = '<p>' . sprintf( _n( 'Your theme contains %s menu location. Select which menu you would like to use.', 'Your theme contains %s menu locations. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) );
+ if ( 1 == $num_locations ) {
+ $description = '<p>' . __( 'Your theme supports one menu. Select which menu you would like to use.' );
+ } else {
+ $description = '<p>' . sprintf( _n( 'Your theme supports %s menu. Select which menu appears in each location.', 'Your theme supports %s menus. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) );
+ }
$description .= '</p><p>' . __( 'You can also place menus in widget areas with the Custom Menu widget.' ) . '</p>';
$this->manager->add_section( 'menu_locations', array(
) ) );
$nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
- $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id ) );
+ $this->manager->add_setting( new WP_Customize_Nav_Menu_Setting( $this->manager, $nav_menu_setting_id, array(
+ 'transport' => 'postMessage',
+ ) ) );
// Add the menu contents.
$menu_items = (array) wp_get_nav_menu_items( $menu_id );
$value = (array) $item;
$value['nav_menu_term_id'] = $menu_id;
$this->manager->add_setting( new WP_Customize_Nav_Menu_Item_Setting( $this->manager, $menu_item_setting_id, array(
- 'value' => $value,
+ 'value' => $value,
+ 'transport' => 'postMessage',
) ) );
// Create a control for each menu item.
'priority' => 999,
) ) );
- $this->manager->add_setting( 'new_menu_name', array(
- 'type' => 'new_menu',
- 'default' => '',
- 'transport' => 'postMessage',
- ) );
-
$this->manager->add_control( 'new_menu_name', array(
'label' => '',
'section' => 'add_menu',
'type' => 'text',
+ 'settings' => array(),
'input_attrs' => array(
'class' => 'menu-name-field',
'placeholder' => __( 'New menu name' ),
),
) );
- $this->manager->add_setting( 'create_new_menu', array(
- 'type' => 'new_menu',
- ) );
-
$this->manager->add_control( new WP_Customize_New_Menu_Control( $this->manager, 'create_new_menu', array(
- 'section' => 'add_menu',
+ 'section' => 'add_menu',
+ 'settings' => array(),
) ) );
}
if ( $post_types ) {
foreach ( $post_types as $slug => $post_type ) {
$item_types[] = array(
- 'title' => $post_type->labels->singular_name,
+ 'title' => $post_type->labels->name,
'type' => 'post_type',
'object' => $post_type->name,
);
continue;
}
$item_types[] = array(
- 'title' => $taxonomy->labels->singular_name,
+ 'title' => $taxonomy->labels->name,
'type' => 'taxonomy',
'object' => $taxonomy->name,
);
<span class="item-title" aria-hidden="true">
<span class="menu-item-title<# if ( ! data.title ) { #> no-title<# } #>">{{ data.title || wp.customize.Menus.data.l10n.untitled }}</span>
</span>
- <button type="button" class="not-a-button item-add">
+ <button type="button" class="button-link item-add">
<span class="screen-reader-text"><?php
/* translators: 1: Title of a menu item, 2: Type of a menu item */
printf( __( 'Add to menu: %1$s (%2$s)' ), '{{ data.title || wp.customize.Menus.data.l10n.untitled }}', '{{ data.type_label }}' );
<div id="new-custom-menu-item" class="accordion-section">
<h4 class="accordion-section-title" role="presentation">
<?php _e( 'Custom Links' ); ?>
- <button type="button" class="not-a-button" aria-expanded="false">
+ <button type="button" class="button-link" aria-expanded="false">
<span class="screen-reader-text"><?php _e( 'Toggle section: Custom Links' ); ?></span>
<span class="toggle-indicator" aria-hidden="true"></span>
</button>
</h4>
- <div class="accordion-section-content">
+ <div class="accordion-section-content customlinkdiv">
<input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
- <p id="menu-item-url-wrap">
- <label class="howto" for="custom-menu-item-url">
- <span><?php _e( 'URL' ); ?></span>
- <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
- </label>
+ <p id="menu-item-url-wrap" class="wp-clearfix">
+ <label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
+ <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
</p>
- <p id="menu-item-name-wrap">
- <label class="howto" for="custom-menu-item-name">
- <span><?php _e( 'Link Text' ); ?></span>
- <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
- </label>
+ <p id="menu-item-name-wrap" class="wp-clearfix">
+ <label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
+ <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
</p>
<p class="button-controls">
<span class="add-to-menu">
<?php echo esc_html( $available_item_type['title'] ); ?>
<span class="spinner"></span>
<span class="no-items"><?php _e( 'No items' ); ?></span>
- <button type="button" class="not-a-button" aria-expanded="false">
+ <button type="button" class="button-link" aria-expanded="false">
<span class="screen-reader-text"><?php
/* translators: %s: Title of a section with menu items */
printf( __( 'Toggle section: %s' ), esc_html( $available_item_type['title'] ) ); ?></span>
<?php
}
+ //
// Start functionality specific to partial-refresh of menu changes in Customizer preview.
- const RENDER_AJAX_ACTION = 'customize_render_menu_partial';
- const RENDER_NONCE_POST_KEY = 'render-menu-nonce';
- const RENDER_QUERY_VAR = 'wp_customize_menu_render';
+ //
/**
- * The number of wp_nav_menu() calls which have happened in the preview.
+ * Nav menu args used for each instance, keyed by the args HMAC.
*
* @since 4.3.0
* @access public
- * @var int
+ * @var array
*/
- public $preview_nav_menu_instance_number = 0;
+ public $preview_nav_menu_instance_args = array();
/**
- * Nav menu args used for each instance.
+ * Filter arguments for dynamic nav_menu selective refresh partials.
*
- * @since 4.3.0
+ * @since 4.5.0
* @access public
- * @var array
+ *
+ * @param array|false $partial_args Partial args.
+ * @param string $partial_id Partial ID.
+ * @return array Partial args.
*/
- public $preview_nav_menu_instance_args = array();
+ public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
+
+ if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) {
+ if ( false === $partial_args ) {
+ $partial_args = array();
+ }
+ $partial_args = array_merge(
+ $partial_args,
+ array(
+ 'type' => 'nav_menu_instance',
+ 'render_callback' => array( $this, 'render_nav_menu_partial' ),
+ 'container_inclusive' => true,
+ 'settings' => array(), // Empty because the nav menu instance may relate to a menu or a location.
+ 'capability' => 'edit_theme_options',
+ )
+ );
+ }
+
+ return $partial_args;
+ }
/**
* Add hooks for the Customizer preview.
* @access public
*/
public function customize_preview_init() {
- add_action( 'template_redirect', array( $this, 'render_menu' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
-
- if ( ! isset( $_REQUEST[ self::RENDER_QUERY_VAR ] ) ) {
- add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
- add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
- }
+ add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
+ add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
+ add_filter( 'wp_footer', array( $this, 'export_preview_data' ), 1 );
+ add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) );
}
/**
*
* @since 4.3.0
* @access public
- *
* @see wp_nav_menu()
+ * @see WP_Customize_Widgets_Partial_Refresh::filter_dynamic_sidebar_params()
*
* @param array $args An array containing wp_nav_menu() arguments.
* @return array Arguments.
*/
public function filter_wp_nav_menu_args( $args ) {
- $this->preview_nav_menu_instance_number += 1;
- $args['instance_number'] = $this->preview_nav_menu_instance_number;
-
+ /*
+ * The following conditions determine whether or not this instance of
+ * wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be
+ * selective refreshed if...
+ */
$can_partial_refresh = (
+ // ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
! empty( $args['echo'] )
&&
+ // ...and if the fallback_cb can be serialized to JSON, since it will be included in the placement context data,
( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) )
&&
+ // ...and if the walker can also be serialized to JSON, since it will be included in the placement context data as well,
( empty( $args['walker'] ) || is_string( $args['walker'] ) )
- &&
- (
+ // ...and if it has a theme location assigned or an assigned menu to display,
+ && (
! empty( $args['theme_location'] )
||
( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) )
)
+ &&
+ // ...and if the nav menu would be rendered with a wrapper container element (upon which to attach data-* attributes).
+ (
+ ! empty( $args['container'] )
+ ||
+ ( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
+ )
);
$args['can_partial_refresh'] = $can_partial_refresh;
- $hashed_args = $args;
+ $exported_args = $args;
+ // Empty out args which may not be JSON-serializable.
if ( ! $can_partial_refresh ) {
- $hashed_args['fallback_cb'] = '';
- $hashed_args['walker'] = '';
+ $exported_args['fallback_cb'] = '';
+ $exported_args['walker'] = '';
}
- // Replace object menu arg with a term_id menu arg, as this exports better to JS and is easier to compare hashes.
- if ( ! empty( $hashed_args['menu'] ) && is_object( $hashed_args['menu'] ) ) {
- $hashed_args['menu'] = $hashed_args['menu']->term_id;
+ /*
+ * Replace object menu arg with a term_id menu arg, as this exports better
+ * to JS and is easier to compare hashes.
+ */
+ if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) {
+ $exported_args['menu'] = $exported_args['menu']->term_id;
}
- ksort( $hashed_args );
- $hashed_args['args_hash'] = $this->hash_nav_menu_args( $hashed_args );
+ ksort( $exported_args );
+ $exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
- $this->preview_nav_menu_instance_args[ $this->preview_nav_menu_instance_number ] = $hashed_args;
+ $args['customize_preview_nav_menus_args'] = $exported_args;
+ $this->preview_nav_menu_instance_args[ $exported_args['args_hmac'] ] = $exported_args;
return $args;
}
/**
- * Prepare wp_nav_menu() calls for partial refresh. Wraps output in container for refreshing.
+ * Prepares wp_nav_menu() calls for partial refresh.
+ *
+ * Injects attributes into container element.
*
* @since 4.3.0
* @access public
* @return null
*/
public function filter_wp_nav_menu( $nav_menu_content, $args ) {
- if ( ! empty( $args->can_partial_refresh ) && ! empty( $args->instance_number ) ) {
- $nav_menu_content = preg_replace(
- '/(?<=class=")/',
- sprintf( 'partial-refreshable-nav-menu partial-refreshable-nav-menu-%1$d ', $args->instance_number ),
- $nav_menu_content,
- 1 // Only update the class on the first element found, the menu container.
- );
+ if ( isset( $args->customize_preview_nav_menus_args['can_partial_refresh'] ) && $args->customize_preview_nav_menus_args['can_partial_refresh'] ) {
+ $attributes = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
+ $attributes .= ' data-customize-partial-type="nav_menu_instance"';
+ $attributes .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
+ $nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . $attributes, $nav_menu_content, 1 );
}
return $nav_menu_content;
}
/**
- * Hash (hmac) the arguments with the nonce and secret auth key to ensure they
- * are not tampered with when submitted in the Ajax request.
+ * Hashes (hmac) the nav menu arguments to ensure they are not tampered with when
+ * submitted in the Ajax request.
+ *
+ * Note that the array is expected to be pre-sorted.
*
* @since 4.3.0
* @access public
*
* @param array $args The arguments to hash.
- * @return string
+ * @return string Hashed nav menu arguments.
*/
public function hash_nav_menu_args( $args ) {
- return wp_hash( wp_create_nonce( self::RENDER_AJAX_ACTION ) . serialize( $args ) );
+ return wp_hash( serialize( $args ) );
}
/**
* @access public
*/
public function customize_preview_enqueue_deps() {
- wp_enqueue_script( 'customize-preview-nav-menus' );
+ wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this.
wp_enqueue_style( 'customize-preview' );
-
- add_action( 'wp_print_footer_scripts', array( $this, 'export_preview_data' ) );
}
/**
- * Export data from PHP to JS.
+ * Exports data from PHP to JS.
*
* @since 4.3.0
* @access public
// Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
$exports = array(
- 'renderQueryVar' => self::RENDER_QUERY_VAR,
- 'renderNonceValue' => wp_create_nonce( self::RENDER_AJAX_ACTION ),
- 'renderNoncePostKey' => self::RENDER_NONCE_POST_KEY,
- 'requestUri' => '/',
- 'theme' => array(
- 'stylesheet' => $this->manager->get_stylesheet(),
- 'active' => $this->manager->is_theme_active(),
- ),
- 'previewCustomizeNonce' => wp_create_nonce( 'preview-customize_' . $this->manager->get_stylesheet() ),
- 'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
+ 'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
);
-
- if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
- $exports['requestUri'] = esc_url_raw( home_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
- }
-
printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
}
+ /**
+ * Export any wp_nav_menu() calls during the rendering of any partials.
+ *
+ * @since 4.5.0
+ * @access public
+ *
+ * @param array $response Response.
+ * @return array Response.
+ */
+ public function export_partial_rendered_nav_menu_instances( $response ) {
+ $response['nav_menu_instance_args'] = $this->preview_nav_menu_instance_args;
+ return $response;
+ }
+
/**
* Render a specific menu via wp_nav_menu() using the supplied arguments.
*
* @access public
*
* @see wp_nav_menu()
+ *
+ * @param WP_Customize_Partial $partial Partial.
+ * @param array $nav_menu_args Nav menu args supplied as container context.
+ * @return string|false
*/
- public function render_menu() {
- if ( empty( $_POST[ self::RENDER_QUERY_VAR ] ) ) {
- return;
- }
-
- $this->manager->remove_preview_signature();
-
- if ( empty( $_POST[ self::RENDER_NONCE_POST_KEY ] ) ) {
- wp_send_json_error( 'missing_nonce_param' );
- }
-
- if ( ! is_customize_preview() ) {
- wp_send_json_error( 'expected_customize_preview' );
- }
-
- if ( ! check_ajax_referer( self::RENDER_AJAX_ACTION, self::RENDER_NONCE_POST_KEY, false ) ) {
- wp_send_json_error( 'nonce_check_fail' );
- }
+ public function render_nav_menu_partial( $partial, $nav_menu_args ) {
+ unset( $partial );
- if ( ! current_user_can( 'edit_theme_options' ) ) {
- wp_send_json_error( 'unauthorized' );
+ if ( ! isset( $nav_menu_args['args_hmac'] ) ) {
+ // Error: missing_args_hmac.
+ return false;
}
- if ( ! isset( $_POST['wp_nav_menu_args'] ) ) {
- wp_send_json_error( 'missing_param' );
- }
+ $nav_menu_args_hmac = $nav_menu_args['args_hmac'];
+ unset( $nav_menu_args['args_hmac'] );
- if ( ! isset( $_POST['wp_nav_menu_args_hash'] ) ) {
- wp_send_json_error( 'missing_param' );
+ ksort( $nav_menu_args );
+ if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) {
+ // Error: args_hmac_mismatch.
+ return false;
}
- $wp_nav_menu_args = json_decode( wp_unslash( $_POST['wp_nav_menu_args'] ), true );
- if ( ! is_array( $wp_nav_menu_args ) ) {
- wp_send_json_error( 'wp_nav_menu_args_not_array' );
- }
-
- $wp_nav_menu_args_hash = sanitize_text_field( wp_unslash( $_POST['wp_nav_menu_args_hash'] ) );
- if ( ! hash_equals( $this->hash_nav_menu_args( $wp_nav_menu_args ), $wp_nav_menu_args_hash ) ) {
- wp_send_json_error( 'wp_nav_menu_args_hash_mismatch' );
- }
+ ob_start();
+ wp_nav_menu( $nav_menu_args );
+ $content = ob_get_clean();
- $wp_nav_menu_args['echo'] = false;
- wp_send_json_success( wp_nav_menu( $wp_nav_menu_args ) );
+ return $content;
}
}