[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

title

Body

[close]

/wp-includes/ -> class-wp-customize-nav-menus.php (source)

   1  <?php
   2  /**
   3   * WordPress Customize Nav Menus classes
   4   *
   5   * @package WordPress
   6   * @subpackage Customize
   7   * @since 4.3.0
   8   */
   9  
  10  /**
  11   * Customize Nav Menus class.
  12   *
  13   * Implements menu management in the Customizer.
  14   *
  15   * @since 4.3.0
  16   *
  17   * @see WP_Customize_Manager
  18   */
  19  final class WP_Customize_Nav_Menus {
  20  
  21      /**
  22       * WP_Customize_Manager instance.
  23       *
  24       * @since 4.3.0
  25       * @var WP_Customize_Manager
  26       */
  27      public $manager;
  28  
  29      /**
  30       * Original nav menu locations before the theme was switched.
  31       *
  32       * @since 4.9.0
  33       * @var array
  34       */
  35      protected $original_nav_menu_locations;
  36  
  37      /**
  38       * Constructor.
  39       *
  40       * @since 4.3.0
  41       *
  42       * @param object $manager An instance of the WP_Customize_Manager class.
  43       */
  44  	public function __construct( $manager ) {
  45          $this->manager                     = $manager;
  46          $this->original_nav_menu_locations = get_nav_menu_locations();
  47  
  48          // See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L469-L499
  49          add_action( 'customize_register', array( $this, 'customize_register' ), 11 );
  50          add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );
  51          add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );
  52          add_action( 'customize_save_nav_menus_created_posts', array( $this, 'save_nav_menus_created_posts' ) );
  53  
  54          // Skip remaining hooks when the user can't manage nav menus anyway.
  55          if ( ! current_user_can( 'edit_theme_options' ) ) {
  56              return;
  57          }
  58  
  59          add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) );
  60          add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
  61          add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
  62          add_action( 'wp_ajax_customize-nav-menus-insert-auto-draft', array( $this, 'ajax_insert_auto_draft_post' ) );
  63          add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
  64          add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
  65          add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
  66          add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
  67          add_action( 'customize_preview_init', array( $this, 'make_auto_draft_status_previewable' ) );
  68  
  69          // Selective Refresh partials.
  70          add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
  71      }
  72  
  73      /**
  74       * Adds a nonce for customizing menus.
  75       *
  76       * @since 4.5.0
  77       *
  78       * @param string[] $nonces Array of nonces.
  79       * @return string[] $nonces Modified array of nonces.
  80       */
  81  	public function filter_nonces( $nonces ) {
  82          $nonces['customize-menus'] = wp_create_nonce( 'customize-menus' );
  83          return $nonces;
  84      }
  85  
  86      /**
  87       * Ajax handler for loading available menu items.
  88       *
  89       * @since 4.3.0
  90       */
  91  	public function ajax_load_available_items() {
  92          check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
  93  
  94          if ( ! current_user_can( 'edit_theme_options' ) ) {
  95              wp_die( -1 );
  96          }
  97  
  98          $all_items  = array();
  99          $item_types = array();
 100          if ( isset( $_POST['item_types'] ) && is_array( $_POST['item_types'] ) ) {
 101              $item_types = wp_unslash( $_POST['item_types'] );
 102          } elseif ( isset( $_POST['type'] ) && isset( $_POST['object'] ) ) { // Back compat.
 103              $item_types[] = array(
 104                  'type'   => wp_unslash( $_POST['type'] ),
 105                  'object' => wp_unslash( $_POST['object'] ),
 106                  'page'   => empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ),
 107              );
 108          } else {
 109              wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
 110          }
 111  
 112          foreach ( $item_types as $item_type ) {
 113              if ( empty( $item_type['type'] ) || empty( $item_type['object'] ) ) {
 114                  wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
 115              }
 116              $type   = sanitize_key( $item_type['type'] );
 117              $object = sanitize_key( $item_type['object'] );
 118              $page   = empty( $item_type['page'] ) ? 0 : absint( $item_type['page'] );
 119              $items  = $this->load_available_items_query( $type, $object, $page );
 120              if ( is_wp_error( $items ) ) {
 121                  wp_send_json_error( $items->get_error_code() );
 122              }
 123              $all_items[ $item_type['type'] . ':' . $item_type['object'] ] = $items;
 124          }
 125  
 126          wp_send_json_success( array( 'items' => $all_items ) );
 127      }
 128  
 129      /**
 130       * Performs the post_type and taxonomy queries for loading available menu items.
 131       *
 132       * @since 4.3.0
 133       *
 134       * @param string $type   Optional. Accepts any custom object type and has built-in support for
 135       *                         'post_type' and 'taxonomy'. Default is 'post_type'.
 136       * @param string $object Optional. Accepts any registered taxonomy or post type name. Default is 'page'.
 137       * @param int    $page   Optional. The page number used to generate the query offset. Default is '0'.
 138       * @return WP_Error|array Returns either a WP_Error object or an array of menu items.
 139       */
 140  	public function load_available_items_query( $type = 'post_type', $object = 'page', $page = 0 ) {
 141          $items = array();
 142  
 143          if ( 'post_type' === $type ) {
 144              $post_type = get_post_type_object( $object );
 145              if ( ! $post_type ) {
 146                  return new WP_Error( 'nav_menus_invalid_post_type' );
 147              }
 148  
 149              if ( 0 === $page && 'page' === $object ) {
 150                  // Add "Home" link. Treat as a page, but switch to custom on add.
 151                  $items[] = array(
 152                      'id'         => 'home',
 153                      'title'      => _x( 'Home', 'nav menu home label' ),
 154                      'type'       => 'custom',
 155                      'type_label' => __( 'Custom Link' ),
 156                      'object'     => '',
 157                      'url'        => home_url(),
 158                  );
 159              } elseif ( 'post' !== $object && 0 === $page && $post_type->has_archive ) {
 160                  // Add a post type archive link.
 161                  $items[] = array(
 162                      'id'         => $object . '-archive',
 163                      'title'      => $post_type->labels->archives,
 164                      'type'       => 'post_type_archive',
 165                      'type_label' => __( 'Post Type Archive' ),
 166                      'object'     => $object,
 167                      'url'        => get_post_type_archive_link( $object ),
 168                  );
 169              }
 170  
 171              // Prepend posts with nav_menus_created_posts on first page.
 172              $posts = array();
 173              if ( 0 === $page && $this->manager->get_setting( 'nav_menus_created_posts' ) ) {
 174                  foreach ( $this->manager->get_setting( 'nav_menus_created_posts' )->value() as $post_id ) {
 175                      $auto_draft_post = get_post( $post_id );
 176                      if ( $post_type->name === $auto_draft_post->post_type ) {
 177                          $posts[] = $auto_draft_post;
 178                      }
 179                  }
 180              }
 181  
 182              $posts = array_merge(
 183                  $posts,
 184                  get_posts(
 185                      array(
 186                          'numberposts' => 10,
 187                          'offset'      => 10 * $page,
 188                          'orderby'     => 'date',
 189                          'order'       => 'DESC',
 190                          'post_type'   => $object,
 191                      )
 192                  )
 193              );
 194  
 195              foreach ( $posts as $post ) {
 196                  $post_title = $post->post_title;
 197                  if ( '' === $post_title ) {
 198                      /* translators: %d: ID of a post */
 199                      $post_title = sprintf( __( '#%d (no title)' ), $post->ID );
 200                  }
 201                  $items[] = array(
 202                      'id'         => "post-{$post->ID}",
 203                      'title'      => html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ),
 204                      'type'       => 'post_type',
 205                      'type_label' => get_post_type_object( $post->post_type )->labels->singular_name,
 206                      'object'     => $post->post_type,
 207                      'object_id'  => intval( $post->ID ),
 208                      'url'        => get_permalink( intval( $post->ID ) ),
 209                  );
 210              }
 211          } elseif ( 'taxonomy' === $type ) {
 212              $terms = get_terms(
 213                  $object,
 214                  array(
 215                      'child_of'     => 0,
 216                      'exclude'      => '',
 217                      'hide_empty'   => false,
 218                      'hierarchical' => 1,
 219                      'include'      => '',
 220                      'number'       => 10,
 221                      'offset'       => 10 * $page,
 222                      'order'        => 'DESC',
 223                      'orderby'      => 'count',
 224                      'pad_counts'   => false,
 225                  )
 226              );
 227              if ( is_wp_error( $terms ) ) {
 228                  return $terms;
 229              }
 230  
 231              foreach ( $terms as $term ) {
 232                  $items[] = array(
 233                      'id'         => "term-{$term->term_id}",
 234                      'title'      => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
 235                      'type'       => 'taxonomy',
 236                      'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
 237                      'object'     => $term->taxonomy,
 238                      'object_id'  => intval( $term->term_id ),
 239                      'url'        => get_term_link( intval( $term->term_id ), $term->taxonomy ),
 240                  );
 241              }
 242          }
 243  
 244          /**
 245           * Filters the available menu items.
 246           *
 247           * @since 4.3.0
 248           *
 249           * @param array  $items  The array of menu items.
 250           * @param string $type   The object type.
 251           * @param string $object The object name.
 252           * @param int    $page   The current page number.
 253           */
 254          $items = apply_filters( 'customize_nav_menu_available_items', $items, $type, $object, $page );
 255  
 256          return $items;
 257      }
 258  
 259      /**
 260       * Ajax handler for searching available menu items.
 261       *
 262       * @since 4.3.0
 263       */
 264  	public function ajax_search_available_items() {
 265          check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
 266  
 267          if ( ! current_user_can( 'edit_theme_options' ) ) {
 268              wp_die( -1 );
 269          }
 270  
 271          if ( empty( $_POST['search'] ) ) {
 272              wp_send_json_error( 'nav_menus_missing_search_parameter' );
 273          }
 274  
 275          $p = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
 276          if ( $p < 1 ) {
 277              $p = 1;
 278          }
 279  
 280          $s     = sanitize_text_field( wp_unslash( $_POST['search'] ) );
 281          $items = $this->search_available_items_query(
 282              array(
 283                  'pagenum' => $p,
 284                  's'       => $s,
 285              )
 286          );
 287  
 288          if ( empty( $items ) ) {
 289              wp_send_json_error( array( 'message' => __( 'No results found.' ) ) );
 290          } else {
 291              wp_send_json_success( array( 'items' => $items ) );
 292          }
 293      }
 294  
 295      /**
 296       * Performs post queries for available-item searching.
 297       *
 298       * Based on WP_Editor::wp_link_query().
 299       *
 300       * @since 4.3.0
 301       *
 302       * @param array $args Optional. Accepts 'pagenum' and 's' (search) arguments.
 303       * @return array Menu items.
 304       */
 305  	public function search_available_items_query( $args = array() ) {
 306          $items = array();
 307  
 308          $post_type_objects = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
 309          $query             = array(
 310              'post_type'              => array_keys( $post_type_objects ),
 311              'suppress_filters'       => true,
 312              'update_post_term_cache' => false,
 313              'update_post_meta_cache' => false,
 314              'post_status'            => 'publish',
 315              'posts_per_page'         => 20,
 316          );
 317  
 318          $args['pagenum'] = isset( $args['pagenum'] ) ? absint( $args['pagenum'] ) : 1;
 319          $query['offset'] = $args['pagenum'] > 1 ? $query['posts_per_page'] * ( $args['pagenum'] - 1 ) : 0;
 320  
 321          if ( isset( $args['s'] ) ) {
 322              $query['s'] = $args['s'];
 323          }
 324  
 325          $posts = array();
 326  
 327          // Prepend list of posts with nav_menus_created_posts search results on first page.
 328          $nav_menus_created_posts_setting = $this->manager->get_setting( 'nav_menus_created_posts' );
 329          if ( 1 === $args['pagenum'] && $nav_menus_created_posts_setting && count( $nav_menus_created_posts_setting->value() ) > 0 ) {
 330              $stub_post_query = new WP_Query(
 331                  array_merge(
 332                      $query,
 333                      array(
 334                          'post_status'    => 'auto-draft',
 335                          'post__in'       => $nav_menus_created_posts_setting->value(),
 336                          'posts_per_page' => -1,
 337                      )
 338                  )
 339              );
 340              $posts           = array_merge( $posts, $stub_post_query->posts );
 341          }
 342  
 343          // Query posts.
 344          $get_posts = new WP_Query( $query );
 345          $posts     = array_merge( $posts, $get_posts->posts );
 346  
 347          // Create items for posts.
 348          foreach ( $posts as $post ) {
 349              $post_title = $post->post_title;
 350              if ( '' === $post_title ) {
 351                  /* translators: %d: ID of a post */
 352                  $post_title = sprintf( __( '#%d (no title)' ), $post->ID );
 353              }
 354              $items[] = array(
 355                  'id'         => 'post-' . $post->ID,
 356                  'title'      => html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ),
 357                  'type'       => 'post_type',
 358                  'type_label' => $post_type_objects[ $post->post_type ]->labels->singular_name,
 359                  'object'     => $post->post_type,
 360                  'object_id'  => intval( $post->ID ),
 361                  'url'        => get_permalink( intval( $post->ID ) ),
 362              );
 363          }
 364  
 365          // Query taxonomy terms.
 366          $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'names' );
 367          $terms      = get_terms(
 368              $taxonomies,
 369              array(
 370                  'name__like' => $args['s'],
 371                  'number'     => 20,
 372                  'offset'     => 20 * ( $args['pagenum'] - 1 ),
 373              )
 374          );
 375  
 376          // Check if any taxonomies were found.
 377          if ( ! empty( $terms ) ) {
 378              foreach ( $terms as $term ) {
 379                  $items[] = array(
 380                      'id'         => 'term-' . $term->term_id,
 381                      'title'      => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
 382                      'type'       => 'taxonomy',
 383                      'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
 384                      'object'     => $term->taxonomy,
 385                      'object_id'  => intval( $term->term_id ),
 386                      'url'        => get_term_link( intval( $term->term_id ), $term->taxonomy ),
 387                  );
 388              }
 389          }
 390  
 391          // Add "Home" link if search term matches. Treat as a page, but switch to custom on add.
 392          if ( isset( $args['s'] ) ) {
 393              $title   = _x( 'Home', 'nav menu home label' );
 394              $matches = function_exists( 'mb_stripos' ) ? false !== mb_stripos( $title, $args['s'] ) : false !== stripos( $title, $args['s'] );
 395              if ( $matches ) {
 396                  $items[] = array(
 397                      'id'         => 'home',
 398                      'title'      => $title,
 399                      'type'       => 'custom',
 400                      'type_label' => __( 'Custom Link' ),
 401                      'object'     => '',
 402                      'url'        => home_url(),
 403                  );
 404              }
 405          }
 406  
 407          /**
 408           * Filters the available menu items during a search request.
 409           *
 410           * @since 4.5.0
 411           *
 412           * @param array $items The array of menu items.
 413           * @param array $args  Includes 'pagenum' and 's' (search) arguments.
 414           */
 415          $items = apply_filters( 'customize_nav_menu_searched_items', $items, $args );
 416  
 417          return $items;
 418      }
 419  
 420      /**
 421       * Enqueue scripts and styles for Customizer pane.
 422       *
 423       * @since 4.3.0
 424       */
 425  	public function enqueue_scripts() {
 426          wp_enqueue_style( 'customize-nav-menus' );
 427          wp_enqueue_script( 'customize-nav-menus' );
 428  
 429          $temp_nav_menu_setting      = new WP_Customize_Nav_Menu_Setting( $this->manager, 'nav_menu[-1]' );
 430          $temp_nav_menu_item_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->manager, 'nav_menu_item[-1]' );
 431  
 432          $num_locations = count( get_registered_nav_menus() );
 433          if ( 1 === $num_locations ) {
 434              $locations_description = __( 'Your theme can display menus in one location.' );
 435          } else {
 436              /* translators: %s: number of menu locations */
 437              $locations_description = sprintf( _n( 'Your theme can display menus in %s location.', 'Your theme can display menus in %s locations.', $num_locations ), number_format_i18n( $num_locations ) );
 438          }
 439  
 440          // Pass data to JS.
 441          $settings = array(
 442              'allMenus'                 => wp_get_nav_menus(),
 443              'itemTypes'                => $this->available_item_types(),
 444              'l10n'                     => array(
 445                  'untitled'               => _x( '(no label)', 'missing menu item navigation label' ),
 446                  'unnamed'                => _x( '(unnamed)', 'Missing menu name.' ),
 447                  'custom_label'           => __( 'Custom Link' ),
 448                  'page_label'             => get_post_type_object( 'page' )->labels->singular_name,
 449                  /* translators: %s:      menu location */
 450                  'menuLocation'           => _x( '(Currently set to: %s)', 'menu' ),
 451                  'locationsTitle'         => 1 === $num_locations ? __( 'Menu Location' ) : __( 'Menu Locations' ),
 452                  'locationsDescription'   => $locations_description,
 453                  'menuNameLabel'          => __( 'Menu Name' ),
 454                  'newMenuNameDescription' => __( 'If your theme has multiple menus, giving them clear names will help you manage them.' ),
 455                  'itemAdded'              => __( 'Menu item added' ),
 456                  'itemDeleted'            => __( 'Menu item deleted' ),
 457                  'menuAdded'              => __( 'Menu created' ),
 458                  'menuDeleted'            => __( 'Menu deleted' ),
 459                  'movedUp'                => __( 'Menu item moved up' ),
 460                  'movedDown'              => __( 'Menu item moved down' ),
 461                  'movedLeft'              => __( 'Menu item moved out of submenu' ),
 462                  'movedRight'             => __( 'Menu item is now a sub-item' ),
 463                  /* translators: &#9656; is the unicode right-pointing triangle, and %s is the section title in the Customizer */
 464                  'customizingMenus'       => sprintf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'nav_menus' )->title ) ),
 465                  /* translators: %s: title of menu item which is invalid */
 466                  'invalidTitleTpl'        => __( '%s (Invalid)' ),
 467                  /* translators: %s: title of menu item in draft status */
 468                  'pendingTitleTpl'        => __( '%s (Pending)' ),
 469                  'itemsFound'             => __( 'Number of items found: %d' ),
 470                  'itemsFoundMore'         => __( 'Additional items found: %d' ),
 471                  'itemsLoadingMore'       => __( 'Loading more results... please wait.' ),
 472                  'reorderModeOn'          => __( 'Reorder mode enabled' ),
 473                  'reorderModeOff'         => __( 'Reorder mode closed' ),
 474                  'reorderLabelOn'         => esc_attr__( 'Reorder menu items' ),
 475                  'reorderLabelOff'        => esc_attr__( 'Close reorder mode' ),
 476              ),
 477              'settingTransport'         => 'postMessage',
 478              'phpIntMax'                => PHP_INT_MAX,
 479              'defaultSettingValues'     => array(
 480                  'nav_menu'      => $temp_nav_menu_setting->default,
 481                  'nav_menu_item' => $temp_nav_menu_item_setting->default,
 482              ),
 483              'locationSlugMappedToName' => get_registered_nav_menus(),
 484          );
 485  
 486          $data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings ) );
 487          wp_scripts()->add_data( 'customize-nav-menus', 'data', $data );
 488  
 489          // This is copied from nav-menus.php, and it has an unfortunate object name of `menus`.
 490          $nav_menus_l10n = array(
 491              'oneThemeLocationNoMenus' => null,
 492              'moveUp'                  => __( 'Move up one' ),
 493              'moveDown'                => __( 'Move down one' ),
 494              'moveToTop'               => __( 'Move to the top' ),
 495              /* translators: %s: previous item name */
 496              'moveUnder'               => __( 'Move under %s' ),
 497              /* translators: %s: previous item name */
 498              'moveOutFrom'             => __( 'Move out from under %s' ),
 499              /* translators: %s: previous item name */
 500              'under'                   => __( 'Under %s' ),
 501              /* translators: %s: previous item name */
 502              'outFrom'                 => __( 'Out from under %s' ),
 503              /* translators: 1: item name, 2: item position, 3: total number of items */
 504              'menuFocus'               => __( '%1$s. Menu item %2$d of %3$d.' ),
 505              /* translators: 1: item name, 2: item position, 3: parent item name */
 506              'subMenuFocus'            => __( '%1$s. Sub item number %2$d under %3$s.' ),
 507          );
 508          wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n );
 509      }
 510  
 511      /**
 512       * Filters a dynamic setting's constructor args.
 513       *
 514       * For a dynamic setting to be registered, this filter must be employed
 515       * to override the default false value with an array of args to pass to
 516       * the WP_Customize_Setting constructor.
 517       *
 518       * @since 4.3.0
 519       *
 520       * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
 521       * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
 522       * @return array|false
 523       */
 524  	public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
 525          if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
 526              $setting_args = array(
 527                  'type'      => WP_Customize_Nav_Menu_Setting::TYPE,
 528                  'transport' => 'postMessage',
 529              );
 530          } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
 531              $setting_args = array(
 532                  'type'      => WP_Customize_Nav_Menu_Item_Setting::TYPE,
 533                  'transport' => 'postMessage',
 534              );
 535          }
 536          return $setting_args;
 537      }
 538  
 539      /**
 540       * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
 541       *
 542       * @since 4.3.0
 543       *
 544       * @param string $setting_class WP_Customize_Setting or a subclass.
 545       * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
 546       * @param array  $setting_args  WP_Customize_Setting or a subclass.
 547       * @return string
 548       */
 549  	public function filter_dynamic_setting_class( $setting_class, $setting_id, $setting_args ) {
 550          unset( $setting_id );
 551  
 552          if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Setting::TYPE === $setting_args['type'] ) {
 553              $setting_class = 'WP_Customize_Nav_Menu_Setting';
 554          } elseif ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Item_Setting::TYPE === $setting_args['type'] ) {
 555              $setting_class = 'WP_Customize_Nav_Menu_Item_Setting';
 556          }
 557          return $setting_class;
 558      }
 559  
 560      /**
 561       * Add the customizer settings and controls.
 562       *
 563       * @since 4.3.0
 564       */
 565  	public function customize_register() {
 566          $changeset = $this->manager->unsanitized_post_values();
 567  
 568          // Preview settings for nav menus early so that the sections and controls will be added properly.
 569          $nav_menus_setting_ids = array();
 570          foreach ( array_keys( $changeset ) as $setting_id ) {
 571              if ( preg_match( '/^(nav_menu_locations|nav_menu|nav_menu_item)\[/', $setting_id ) ) {
 572                  $nav_menus_setting_ids[] = $setting_id;
 573              }
 574          }
 575          $settings = $this->manager->add_dynamic_settings( $nav_menus_setting_ids );
 576          if ( $this->manager->settings_previewed() ) {
 577              foreach ( $settings as $setting ) {
 578                  $setting->preview();
 579              }
 580          }
 581  
 582          // Require JS-rendered control types.
 583          $this->manager->register_panel_type( 'WP_Customize_Nav_Menus_Panel' );
 584          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' );
 585          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Name_Control' );
 586          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Locations_Control' );
 587          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Auto_Add_Control' );
 588          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Item_Control' );
 589  
 590          // Create a panel for Menus.
 591          $description = '<p>' . __( 'This panel is used for managing navigation menus for content you have already published on your site. You can create menus and add items for existing content such as pages, posts, categories, tags, formats, or custom links.' ) . '</p>';
 592          if ( current_theme_supports( 'widgets' ) ) {
 593              /* translators: URL to the widgets panel of the customizer */
 594              $description .= '<p>' . sprintf( __( 'Menus can be displayed in locations defined by your theme or in <a href="%s">widget areas</a> by adding a &#8220;Navigation Menu&#8221; widget.' ), "javascript:wp.customize.panel( 'widgets' ).focus();" ) . '</p>';
 595          } else {
 596              $description .= '<p>' . __( 'Menus can be displayed in locations defined by your theme.' ) . '</p>';
 597          }
 598  
 599          /*
 600           * Once multiple theme supports are allowed in WP_Customize_Panel,
 601           * this panel can be restricted to themes that support menus or widgets.
 602           */
 603          $this->manager->add_panel(
 604              new WP_Customize_Nav_Menus_Panel(
 605                  $this->manager,
 606                  'nav_menus',
 607                  array(
 608                      'title'       => __( 'Menus' ),
 609                      'description' => $description,
 610                      'priority'    => 100,
 611                  )
 612              )
 613          );
 614          $menus = wp_get_nav_menus();
 615  
 616          // Menu locations.
 617          $locations     = get_registered_nav_menus();
 618          $num_locations = count( $locations );
 619          if ( 1 == $num_locations ) {
 620              $description = '<p>' . __( 'Your theme can display menus in one location. Select which menu you would like to use.' ) . '</p>';
 621          } else {
 622              /* translators: %s: number of menu locations */
 623              $description = '<p>' . sprintf( _n( 'Your theme can display menus in %s location. Select which menu you would like to use.', 'Your theme can display menus in %s locations. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) ) . '</p>';
 624          }
 625  
 626          if ( current_theme_supports( 'widgets' ) ) {
 627              /* translators: URL to the widgets panel of the customizer */
 628              $description .= '<p>' . sprintf( __( 'If your theme has widget areas, you can also add menus there. Visit the <a href="%s">Widgets panel</a> and add a &#8220;Navigation Menu widget&#8221; to display a menu in a sidebar or footer.' ), "javascript:wp.customize.panel( 'widgets' ).focus();" ) . '</p>';
 629          }
 630  
 631          $this->manager->add_section(
 632              'menu_locations',
 633              array(
 634                  'title'       => 1 === $num_locations ? _x( 'View Location', 'menu locations' ) : _x( 'View All Locations', 'menu locations' ),
 635                  'panel'       => 'nav_menus',
 636                  'priority'    => 30,
 637                  'description' => $description,
 638              )
 639          );
 640  
 641          $choices = array( '0' => __( '&mdash; Select &mdash;' ) );
 642          foreach ( $menus as $menu ) {
 643              $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '&hellip;' );
 644          }
 645  
 646          // Attempt to re-map the nav menu location assignments when previewing a theme switch.
 647          $mapped_nav_menu_locations = array();
 648          if ( ! $this->manager->is_theme_active() ) {
 649              $theme_mods = get_option( 'theme_mods_' . $this->manager->get_stylesheet(), array() );
 650  
 651              // If there is no data from a previous activation, start fresh.
 652              if ( empty( $theme_mods['nav_menu_locations'] ) ) {
 653                  $theme_mods['nav_menu_locations'] = array();
 654              }
 655  
 656              $mapped_nav_menu_locations = wp_map_nav_menu_locations( $theme_mods['nav_menu_locations'], $this->original_nav_menu_locations );
 657          }
 658  
 659          foreach ( $locations as $location => $description ) {
 660              $setting_id = "nav_menu_locations[{$location}]";
 661  
 662              $setting = $this->manager->get_setting( $setting_id );
 663              if ( $setting ) {
 664                  $setting->transport = 'postMessage';
 665                  remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
 666                  add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
 667              } else {
 668                  $this->manager->add_setting(
 669                      $setting_id,
 670                      array(
 671                          'sanitize_callback' => array( $this, 'intval_base10' ),
 672                          'theme_supports'    => 'menus',
 673                          'type'              => 'theme_mod',
 674                          'transport'         => 'postMessage',
 675                          'default'           => 0,
 676                      )
 677                  );
 678              }
 679  
 680              // Override the assigned nav menu location if mapped during previewed theme switch.
 681              if ( empty( $changeset[ $setting_id ] ) && isset( $mapped_nav_menu_locations[ $location ] ) ) {
 682                  $this->manager->set_post_value( $setting_id, $mapped_nav_menu_locations[ $location ] );
 683              }
 684  
 685              $this->manager->add_control(
 686                  new WP_Customize_Nav_Menu_Location_Control(
 687                      $this->manager,
 688                      $setting_id,
 689                      array(
 690                          'label'       => $description,
 691                          'location_id' => $location,
 692                          'section'     => 'menu_locations',
 693                          'choices'     => $choices,
 694                      )
 695                  )
 696              );
 697          }
 698  
 699          // Register each menu as a Customizer section, and add each menu item to each menu.
 700          foreach ( $menus as $menu ) {
 701              $menu_id = $menu->term_id;
 702  
 703              // Create a section for each menu.
 704              $section_id = 'nav_menu[' . $menu_id . ']';
 705              $this->manager->add_section(
 706                  new WP_Customize_Nav_Menu_Section(
 707                      $this->manager,
 708                      $section_id,
 709                      array(
 710                          'title'    => html_entity_decode( $menu->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
 711                          'priority' => 10,
 712                          'panel'    => 'nav_menus',
 713                      )
 714                  )
 715              );
 716  
 717              $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
 718              $this->manager->add_setting(
 719                  new WP_Customize_Nav_Menu_Setting(
 720                      $this->manager,
 721                      $nav_menu_setting_id,
 722                      array(
 723                          'transport' => 'postMessage',
 724                      )
 725                  )
 726              );
 727  
 728              // Add the menu contents.
 729              $menu_items = (array) wp_get_nav_menu_items( $menu_id );
 730  
 731              foreach ( array_values( $menu_items ) as $i => $item ) {
 732  
 733                  // Create a setting for each menu item (which doesn't actually manage data, currently).
 734                  $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']';
 735  
 736                  $value = (array) $item;
 737                  if ( empty( $value['post_title'] ) ) {
 738                      $value['title'] = '';
 739                  }
 740  
 741                  $value['nav_menu_term_id'] = $menu_id;
 742                  $this->manager->add_setting(
 743                      new WP_Customize_Nav_Menu_Item_Setting(
 744                          $this->manager,
 745                          $menu_item_setting_id,
 746                          array(
 747                              'value'     => $value,
 748                              'transport' => 'postMessage',
 749                          )
 750                      )
 751                  );
 752  
 753                  // Create a control for each menu item.
 754                  $this->manager->add_control(
 755                      new WP_Customize_Nav_Menu_Item_Control(
 756                          $this->manager,
 757                          $menu_item_setting_id,
 758                          array(
 759                              'label'    => $item->title,
 760                              'section'  => $section_id,
 761                              'priority' => 10 + $i,
 762                          )
 763                      )
 764                  );
 765              }
 766  
 767              // Note: other controls inside of this section get added dynamically in JS via the MenuSection.ready() function.
 768          }
 769  
 770          // Add the add-new-menu section and controls.
 771          $this->manager->add_section(
 772              'add_menu',
 773              array(
 774                  'type'     => 'new_menu',
 775                  'title'    => __( 'New Menu' ),
 776                  'panel'    => 'nav_menus',
 777                  'priority' => 20,
 778              )
 779          );
 780  
 781          $this->manager->add_setting(
 782              new WP_Customize_Filter_Setting(
 783                  $this->manager,
 784                  'nav_menus_created_posts',
 785                  array(
 786                      'transport'         => 'postMessage',
 787                      'type'              => 'option', // To prevent theme prefix in changeset.
 788                      'default'           => array(),
 789                      'sanitize_callback' => array( $this, 'sanitize_nav_menus_created_posts' ),
 790                  )
 791              )
 792          );
 793      }
 794  
 795      /**
 796       * Get the base10 intval.
 797       *
 798       * This is used as a setting's sanitize_callback; we can't use just plain
 799       * intval because the second argument is not what intval() expects.
 800       *
 801       * @since 4.3.0
 802       *
 803       * @param mixed $value Number to convert.
 804       * @return int Integer.
 805       */
 806  	public function intval_base10( $value ) {
 807          return intval( $value, 10 );
 808      }
 809  
 810      /**
 811       * Return an array of all the available item types.
 812       *
 813       * @since 4.3.0
 814       * @since 4.7.0  Each array item now includes a `$type_label` in addition to `$title`, `$type`, and `$object`.
 815       *
 816       * @return array The available menu item types.
 817       */
 818  	public function available_item_types() {
 819          $item_types = array();
 820  
 821          $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
 822          if ( $post_types ) {
 823              foreach ( $post_types as $slug => $post_type ) {
 824                  $item_types[] = array(
 825                      'title'      => $post_type->labels->name,
 826                      'type_label' => $post_type->labels->singular_name,
 827                      'type'       => 'post_type',
 828                      'object'     => $post_type->name,
 829                  );
 830              }
 831          }
 832  
 833          $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' );
 834          if ( $taxonomies ) {
 835              foreach ( $taxonomies as $slug => $taxonomy ) {
 836                  if ( 'post_format' === $taxonomy && ! current_theme_supports( 'post-formats' ) ) {
 837                      continue;
 838                  }
 839                  $item_types[] = array(
 840                      'title'      => $taxonomy->labels->name,
 841                      'type_label' => $taxonomy->labels->singular_name,
 842                      'type'       => 'taxonomy',
 843                      'object'     => $taxonomy->name,
 844                  );
 845              }
 846          }
 847  
 848          /**
 849           * Filters the available menu item types.
 850           *
 851           * @since 4.3.0
 852           * @since 4.7.0  Each array item now includes a `$type_label` in addition to `$title`, `$type`, and `$object`.
 853           *
 854           * @param array $item_types Navigation menu item types.
 855           */
 856          $item_types = apply_filters( 'customize_nav_menu_available_item_types', $item_types );
 857  
 858          return $item_types;
 859      }
 860  
 861      /**
 862       * Add a new `auto-draft` post.
 863       *
 864       * @since 4.7.0
 865       *
 866       * @param array $postarr {
 867       *     Post array. Note that post_status is overridden to be `auto-draft`.
 868       *
 869       * @var string $post_title   Post title. Required.
 870       * @var string $post_type    Post type. Required.
 871       * @var string $post_name    Post name.
 872       * @var string $post_content Post content.
 873       * }
 874       * @return WP_Post|WP_Error Inserted auto-draft post object or error.
 875       */
 876  	public function insert_auto_draft_post( $postarr ) {
 877          if ( ! isset( $postarr['post_type'] ) ) {
 878              return new WP_Error( 'unknown_post_type', __( 'Invalid post type.' ) );
 879          }
 880          if ( empty( $postarr['post_title'] ) ) {
 881              return new WP_Error( 'empty_title', __( 'Empty title.' ) );
 882          }
 883          if ( ! empty( $postarr['post_status'] ) ) {
 884              return new WP_Error( 'status_forbidden', __( 'Status is forbidden.' ) );
 885          }
 886  
 887          /*
 888           * If the changeset is a draft, this will change to draft the next time the changeset
 889           * is updated; otherwise, auto-draft will persist in autosave revisions, until save.
 890           */
 891          $postarr['post_status'] = 'auto-draft';
 892  
 893          // Auto-drafts are allowed to have empty post_names, so it has to be explicitly set.
 894          if ( empty( $postarr['post_name'] ) ) {
 895              $postarr['post_name'] = sanitize_title( $postarr['post_title'] );
 896          }
 897          if ( ! isset( $postarr['meta_input'] ) ) {
 898              $postarr['meta_input'] = array();
 899          }
 900          $postarr['meta_input']['_customize_draft_post_name'] = $postarr['post_name'];
 901          $postarr['meta_input']['_customize_changeset_uuid']  = $this->manager->changeset_uuid();
 902          unset( $postarr['post_name'] );
 903  
 904          add_filter( 'wp_insert_post_empty_content', '__return_false', 1000 );
 905          $r = wp_insert_post( wp_slash( $postarr ), true );
 906          remove_filter( 'wp_insert_post_empty_content', '__return_false', 1000 );
 907  
 908          if ( is_wp_error( $r ) ) {
 909              return $r;
 910          } else {
 911              return get_post( $r );
 912          }
 913      }
 914  
 915      /**
 916       * Ajax handler for adding a new auto-draft post.
 917       *
 918       * @since 4.7.0
 919       */
 920  	public function ajax_insert_auto_draft_post() {
 921          if ( ! check_ajax_referer( 'customize-menus', 'customize-menus-nonce', false ) ) {
 922              wp_send_json_error( 'bad_nonce', 400 );
 923          }
 924  
 925          if ( ! current_user_can( 'customize' ) ) {
 926              wp_send_json_error( 'customize_not_allowed', 403 );
 927          }
 928  
 929          if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
 930              wp_send_json_error( 'missing_params', 400 );
 931          }
 932  
 933          $params         = wp_unslash( $_POST['params'] );
 934          $illegal_params = array_diff( array_keys( $params ), array( 'post_type', 'post_title' ) );
 935          if ( ! empty( $illegal_params ) ) {
 936              wp_send_json_error( 'illegal_params', 400 );
 937          }
 938  
 939          $params = array_merge(
 940              array(
 941                  'post_type'  => '',
 942                  'post_title' => '',
 943              ),
 944              $params
 945          );
 946  
 947          if ( empty( $params['post_type'] ) || ! post_type_exists( $params['post_type'] ) ) {
 948              status_header( 400 );
 949              wp_send_json_error( 'missing_post_type_param' );
 950          }
 951  
 952          $post_type_object = get_post_type_object( $params['post_type'] );
 953          if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
 954              status_header( 403 );
 955              wp_send_json_error( 'insufficient_post_permissions' );
 956          }
 957  
 958          $params['post_title'] = trim( $params['post_title'] );
 959          if ( '' === $params['post_title'] ) {
 960              status_header( 400 );
 961              wp_send_json_error( 'missing_post_title' );
 962          }
 963  
 964          $r = $this->insert_auto_draft_post( $params );
 965          if ( is_wp_error( $r ) ) {
 966              $error = $r;
 967              if ( ! empty( $post_type_object->labels->singular_name ) ) {
 968                  $singular_name = $post_type_object->labels->singular_name;
 969              } else {
 970                  $singular_name = __( 'Post' );
 971              }
 972  
 973              $data = array(
 974                  /* translators: 1: post type name, 2: error message */
 975                  'message' => sprintf( __( '%1$s could not be created: %2$s' ), $singular_name, $error->get_error_message() ),
 976              );
 977              wp_send_json_error( $data );
 978          } else {
 979              $post = $r;
 980              $data = array(
 981                  'post_id' => $post->ID,
 982                  'url'     => get_permalink( $post->ID ),
 983              );
 984              wp_send_json_success( $data );
 985          }
 986      }
 987  
 988      /**
 989       * Print the JavaScript templates used to render Menu Customizer components.
 990       *
 991       * Templates are imported into the JS use wp.template.
 992       *
 993       * @since 4.3.0
 994       */
 995  	public function print_templates() {
 996          ?>
 997          <script type="text/html" id="tmpl-available-menu-item">
 998              <li id="menu-item-tpl-{{ data.id }}" class="menu-item-tpl" data-menu-item-id="{{ data.id }}">
 999                  <div class="menu-item-bar">
1000                      <div class="menu-item-handle">
1001                          <span class="item-type" aria-hidden="true">{{ data.type_label }}</span>
1002                          <span class="item-title" aria-hidden="true">
1003                              <span class="menu-item-title<# if ( ! data.title ) { #> no-title<# } #>">{{ data.title || wp.customize.Menus.data.l10n.untitled }}</span>
1004                          </span>
1005                          <button type="button" class="button-link item-add">
1006                              <span class="screen-reader-text">
1007                              <?php
1008                                  /* translators: 1: title of a menu item, 2: type of a menu item */
1009                                  printf( __( 'Add to menu: %1$s (%2$s)' ), '{{ data.title || wp.customize.Menus.data.l10n.untitled }}', '{{ data.type_label }}' );
1010                              ?>
1011                              </span>
1012                          </button>
1013                      </div>
1014                  </div>
1015              </li>
1016          </script>
1017  
1018          <script type="text/html" id="tmpl-menu-item-reorder-nav">
1019              <div class="menu-item-reorder-nav">
1020                  <?php
1021                  printf(
1022                      '<button type="button" class="menus-move-up">%1$s</button><button type="button" class="menus-move-down">%2$s</button><button type="button" class="menus-move-left">%3$s</button><button type="button" class="menus-move-right">%4$s</button>',
1023                      __( 'Move up' ),
1024                      __( 'Move down' ),
1025                      __( 'Move one level up' ),
1026                      __( 'Move one level down' )
1027                  );
1028                  ?>
1029              </div>
1030          </script>
1031  
1032          <script type="text/html" id="tmpl-nav-menu-delete-button">
1033              <div class="menu-delete-item">
1034                  <button type="button" class="button-link button-link-delete">
1035                      <?php _e( 'Delete Menu' ); ?>
1036                  </button>
1037              </div>
1038          </script>
1039  
1040          <script type="text/html" id="tmpl-nav-menu-submit-new-button">
1041              <p id="customize-new-menu-submit-description"><?php _e( 'Click &#8220;Next&#8221; to start adding links to your new menu.' ); ?></p>
1042              <button id="customize-new-menu-submit" type="button" class="button" aria-describedby="customize-new-menu-submit-description"><?php _e( 'Next' ); ?></button>
1043          </script>
1044  
1045          <script type="text/html" id="tmpl-nav-menu-locations-header">
1046              <span class="customize-control-title customize-section-title-menu_locations-heading">{{ data.l10n.locationsTitle }}</span>
1047              <p class="customize-control-description customize-section-title-menu_locations-description">{{ data.l10n.locationsDescription }}</p>
1048          </script>
1049  
1050          <script type="text/html" id="tmpl-nav-menu-create-menu-section-title">
1051              <p class="add-new-menu-notice">
1052                  <?php _e( 'It doesn&#8217;t look like your site has any menus yet. Want to build one? Click the button to start.' ); ?>
1053              </p>
1054              <p class="add-new-menu-notice">
1055                  <?php _e( 'You&#8217;ll create a menu, assign it a location, and add menu items like links to pages and categories. If your theme has multiple menu areas, you might need to create more than one.' ); ?>
1056              </p>
1057              <h3>
1058                  <button type="button" class="button customize-add-menu-button">
1059                      <?php _e( 'Create New Menu' ); ?>
1060                  </button>
1061              </h3>
1062          </script>
1063          <?php
1064      }
1065  
1066      /**
1067       * Print the html template used to render the add-menu-item frame.
1068       *
1069       * @since 4.3.0
1070       */
1071  	public function available_items_template() {
1072          ?>
1073          <div id="available-menu-items" class="accordion-container">
1074              <div class="customize-section-title">
1075                  <button type="button" class="customize-section-back" tabindex="-1">
1076                      <span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
1077                  </button>
1078                  <h3>
1079                      <span class="customize-action">
1080                          <?php
1081                              /* translators: &#9656; is the unicode right-pointing triangle, and %s is the section title in the Customizer */
1082                              printf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'nav_menus' )->title ) );
1083                          ?>
1084                      </span>
1085                      <?php _e( 'Add Menu Items' ); ?>
1086                  </h3>
1087              </div>
1088              <div id="available-menu-items-search" class="accordion-section cannot-expand">
1089                  <div class="accordion-section-title">
1090                      <label class="screen-reader-text" for="menu-items-search"><?php _e( 'Search Menu Items' ); ?></label>
1091                      <input type="text" id="menu-items-search" placeholder="<?php esc_attr_e( 'Search menu items&hellip;' ); ?>" aria-describedby="menu-items-search-desc" />
1092                      <p class="screen-reader-text" id="menu-items-search-desc"><?php _e( 'The search results will be updated as you type.' ); ?></p>
1093                      <span class="spinner"></span>
1094                  </div>
1095                  <div class="search-icon" aria-hidden="true"></div>
1096                  <button type="button" class="clear-results"><span class="screen-reader-text"><?php _e( 'Clear Results' ); ?></span></button>
1097                  <ul class="accordion-section-content available-menu-items-list" data-type="search"></ul>
1098              </div>
1099              <?php
1100  
1101              // Ensure the page post type comes first in the list.
1102              $item_types     = $this->available_item_types();
1103              $page_item_type = null;
1104              foreach ( $item_types as $i => $item_type ) {
1105                  if ( isset( $item_type['object'] ) && 'page' === $item_type['object'] ) {
1106                      $page_item_type = $item_type;
1107                      unset( $item_types[ $i ] );
1108                  }
1109              }
1110  
1111              $this->print_custom_links_available_menu_item();
1112              if ( $page_item_type ) {
1113                  $this->print_post_type_container( $page_item_type );
1114              }
1115              // Containers for per-post-type item browsing; items are added with JS.
1116              foreach ( $item_types as $item_type ) {
1117                  $this->print_post_type_container( $item_type );
1118              }
1119              ?>
1120          </div><!-- #available-menu-items -->
1121          <?php
1122      }
1123  
1124      /**
1125       * Print the markup for new menu items.
1126       *
1127       * To be used in the template #available-menu-items.
1128       *
1129       * @since 4.7.0
1130       *
1131       * @param array $available_item_type Menu item data to output, including title, type, and label.
1132       * @return void
1133       */
1134  	protected function print_post_type_container( $available_item_type ) {
1135          $id = sprintf( 'available-menu-items-%s-%s', $available_item_type['type'], $available_item_type['object'] );
1136          ?>
1137          <div id="<?php echo esc_attr( $id ); ?>" class="accordion-section">
1138              <h4 class="accordion-section-title" role="presentation">
1139                  <?php echo esc_html( $available_item_type['title'] ); ?>
1140                  <span class="spinner"></span>
1141                  <span class="no-items"><?php _e( 'No items' ); ?></span>
1142                  <button type="button" class="button-link" aria-expanded="false">
1143                      <span class="screen-reader-text">
1144                      <?php
1145                          /* translators: %s: Title of a section with menu items */
1146                          printf( __( 'Toggle section: %s' ), esc_html( $available_item_type['title'] ) );
1147                      ?>
1148                          </span>
1149                      <span class="toggle-indicator" aria-hidden="true"></span>
1150                  </button>
1151              </h4>
1152              <div class="accordion-section-content">
1153                  <?php if ( 'post_type' === $available_item_type['type'] ) : ?>
1154                      <?php $post_type_obj = get_post_type_object( $available_item_type['object'] ); ?>
1155                      <?php if ( current_user_can( $post_type_obj->cap->create_posts ) && current_user_can( $post_type_obj->cap->publish_posts ) ) : ?>
1156                          <div class="new-content-item">
1157                              <label for="<?php echo esc_attr( 'create-item-input-' . $available_item_type['object'] ); ?>" class="screen-reader-text"><?php echo esc_html( $post_type_obj->labels->add_new_item ); ?></label>
1158                              <input type="text" id="<?php echo esc_attr( 'create-item-input-' . $available_item_type['object'] ); ?>" class="create-item-input" placeholder="<?php echo esc_attr( $post_type_obj->labels->add_new_item ); ?>">
1159                              <button type="button" class="button add-content"><?php _e( 'Add' ); ?></button>
1160                          </div>
1161                      <?php endif; ?>
1162                  <?php endif; ?>
1163                  <ul class="available-menu-items-list" data-type="<?php echo esc_attr( $available_item_type['type'] ); ?>" data-object="<?php echo esc_attr( $available_item_type['object'] ); ?>" data-type_label="<?php echo esc_attr( isset( $available_item_type['type_label'] ) ? $available_item_type['type_label'] : $available_item_type['type'] ); ?>"></ul>
1164              </div>
1165          </div>
1166          <?php
1167      }
1168  
1169      /**
1170       * Print the markup for available menu item custom links.
1171       *
1172       * @since 4.7.0
1173       *
1174       * @return void
1175       */
1176  	protected function print_custom_links_available_menu_item() {
1177          ?>
1178          <div id="new-custom-menu-item" class="accordion-section">
1179              <h4 class="accordion-section-title" role="presentation">
1180                  <?php _e( 'Custom Links' ); ?>
1181                  <button type="button" class="button-link" aria-expanded="false">
1182                      <span class="screen-reader-text"><?php _e( 'Toggle section: Custom Links' ); ?></span>
1183                      <span class="toggle-indicator" aria-hidden="true"></span>
1184                  </button>
1185              </h4>
1186              <div class="accordion-section-content customlinkdiv">
1187                  <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
1188                  <p id="menu-item-url-wrap" class="wp-clearfix">
1189                      <label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
1190                      <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
1191                  </p>
1192                  <p id="menu-item-name-wrap" class="wp-clearfix">
1193                      <label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
1194                      <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
1195                  </p>
1196                  <p class="button-controls">
1197                      <span class="add-to-menu">
1198                          <input type="submit" class="button submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu' ); ?>" name="add-custom-menu-item" id="custom-menu-item-submit">
1199                          <span class="spinner"></span>
1200                      </span>
1201                  </p>
1202              </div>
1203          </div>
1204          <?php
1205      }
1206  
1207      //
1208      // Start functionality specific to partial-refresh of menu changes in Customizer preview.
1209      //
1210  
1211      /**
1212       * Nav menu args used for each instance, keyed by the args HMAC.
1213       *
1214       * @since 4.3.0
1215       * @var array
1216       */
1217      public $preview_nav_menu_instance_args = array();
1218  
1219      /**
1220       * Filters arguments for dynamic nav_menu selective refresh partials.
1221       *
1222       * @since 4.5.0
1223       *
1224       * @param array|false $partial_args Partial args.
1225       * @param string      $partial_id   Partial ID.
1226       * @return array Partial args.
1227       */
1228  	public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
1229  
1230          if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) {
1231              if ( false === $partial_args ) {
1232                  $partial_args = array();
1233              }
1234              $partial_args = array_merge(
1235                  $partial_args,
1236                  array(
1237                      'type'                => 'nav_menu_instance',
1238                      'render_callback'     => array( $this, 'render_nav_menu_partial' ),
1239                      'container_inclusive' => true,
1240                      'settings'            => array(), // Empty because the nav menu instance may relate to a menu or a location.
1241                      'capability'          => 'edit_theme_options',
1242                  )
1243              );
1244          }
1245  
1246          return $partial_args;
1247      }
1248  
1249      /**
1250       * Add hooks for the Customizer preview.
1251       *
1252       * @since 4.3.0
1253       */
1254  	public function customize_preview_init() {
1255          add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
1256          add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
1257          add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
1258          add_filter( 'wp_footer', array( $this, 'export_preview_data' ), 1 );
1259          add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) );
1260      }
1261  
1262      /**
1263       * Make the auto-draft status protected so that it can be queried.
1264       *
1265       * @since 4.7.0
1266       *
1267       * @global array $wp_post_statuses List of post statuses.
1268       */
1269  	public function make_auto_draft_status_previewable() {
1270          global $wp_post_statuses;
1271          $wp_post_statuses['auto-draft']->protected = true;
1272      }
1273  
1274      /**
1275       * Sanitize post IDs for posts created for nav menu items to be published.
1276       *
1277       * @since 4.7.0
1278       *
1279       * @param array $value Post IDs.
1280       * @returns array Post IDs.
1281       */
1282  	public function sanitize_nav_menus_created_posts( $value ) {
1283          $post_ids = array();
1284          foreach ( wp_parse_id_list( $value ) as $post_id ) {
1285              if ( empty( $post_id ) ) {
1286                  continue;
1287              }
1288              $post = get_post( $post_id );
1289              if ( 'auto-draft' !== $post->post_status && 'draft' !== $post->post_status ) {
1290                  continue;
1291              }
1292              $post_type_obj = get_post_type_object( $post->post_type );
1293              if ( ! $post_type_obj ) {
1294                  continue;
1295              }
1296              if ( ! current_user_can( $post_type_obj->cap->publish_posts ) || ! current_user_can( $post_type_obj->cap->edit_post, $post_id ) ) {
1297                  continue;
1298              }
1299              $post_ids[] = $post->ID;
1300          }
1301          return $post_ids;
1302      }
1303  
1304      /**
1305       * Publish the auto-draft posts that were created for nav menu items.
1306       *
1307       * The post IDs will have been sanitized by already by
1308       * `WP_Customize_Nav_Menu_Items::sanitize_nav_menus_created_posts()` to
1309       * remove any post IDs for which the user cannot publish or for which the
1310       * post is not an auto-draft.
1311       *
1312       * @since 4.7.0
1313       *
1314       * @param WP_Customize_Setting $setting Customizer setting object.
1315       */
1316  	public function save_nav_menus_created_posts( $setting ) {
1317          $post_ids = $setting->post_value();
1318          if ( ! empty( $post_ids ) ) {
1319              foreach ( $post_ids as $post_id ) {
1320  
1321                  // Prevent overriding the status that a user may have prematurely updated the post to.
1322                  $current_status = get_post_status( $post_id );
1323                  if ( 'auto-draft' !== $current_status && 'draft' !== $current_status ) {
1324                      continue;
1325                  }
1326  
1327                  $target_status = 'attachment' === get_post_type( $post_id ) ? 'inherit' : 'publish';
1328                  $args          = array(
1329                      'ID'          => $post_id,
1330                      'post_status' => $target_status,
1331                  );
1332                  $post_name     = get_post_meta( $post_id, '_customize_draft_post_name', true );
1333                  if ( $post_name ) {
1334                      $args['post_name'] = $post_name;
1335                  }
1336  
1337                  // Note that wp_publish_post() cannot be used because unique slugs need to be assigned.
1338                  wp_update_post( wp_slash( $args ) );
1339  
1340                  delete_post_meta( $post_id, '_customize_draft_post_name' );
1341              }
1342          }
1343      }
1344  
1345      /**
1346       * Keep track of the arguments that are being passed to wp_nav_menu().
1347       *
1348       * @since 4.3.0
1349       * @see wp_nav_menu()
1350       * @see WP_Customize_Widgets::filter_dynamic_sidebar_params()
1351       *
1352       * @param array $args An array containing wp_nav_menu() arguments.
1353       * @return array Arguments.
1354       */
1355  	public function filter_wp_nav_menu_args( $args ) {
1356          /*
1357           * The following conditions determine whether or not this instance of
1358           * wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be
1359           * selective refreshed if...
1360           */
1361          $can_partial_refresh = (
1362              // ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
1363              ! empty( $args['echo'] )
1364              &&
1365              // ...and if the fallback_cb can be serialized to JSON, since it will be included in the placement context data,
1366              ( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) )
1367              &&
1368              // ...and if the walker can also be serialized to JSON, since it will be included in the placement context data as well,
1369              ( empty( $args['walker'] ) || is_string( $args['walker'] ) )
1370              // ...and if it has a theme location assigned or an assigned menu to display,
1371              && (
1372                  ! empty( $args['theme_location'] )
1373                  ||
1374                  ( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) )
1375              )
1376              &&
1377              // ...and if the nav menu would be rendered with a wrapper container element (upon which to attach data-* attributes).
1378              (
1379                  ! empty( $args['container'] )
1380                  ||
1381                  ( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
1382              )
1383          );
1384          $args['can_partial_refresh'] = $can_partial_refresh;
1385  
1386          $exported_args = $args;
1387  
1388          // Empty out args which may not be JSON-serializable.
1389          if ( ! $can_partial_refresh ) {
1390              $exported_args['fallback_cb'] = '';
1391              $exported_args['walker']      = '';
1392          }
1393  
1394          /*
1395           * Replace object menu arg with a term_id menu arg, as this exports better
1396           * to JS and is easier to compare hashes.
1397           */
1398          if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) {
1399              $exported_args['menu'] = $exported_args['menu']->term_id;
1400          }
1401  
1402          ksort( $exported_args );
1403          $exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
1404  
1405          $args['customize_preview_nav_menus_args']                            = $exported_args;
1406          $this->preview_nav_menu_instance_args[ $exported_args['args_hmac'] ] = $exported_args;
1407          return $args;
1408      }
1409  
1410      /**
1411       * Prepares wp_nav_menu() calls for partial refresh.
1412       *
1413       * Injects attributes into container element.
1414       *
1415       * @since 4.3.0
1416       *
1417       * @see wp_nav_menu()
1418       *
1419       * @param string $nav_menu_content The HTML content for the navigation menu.
1420       * @param object $args             An object containing wp_nav_menu() arguments.
1421       * @return string Nav menu HTML with selective refresh attributes added if partial can be refreshed.
1422       */
1423  	public function filter_wp_nav_menu( $nav_menu_content, $args ) {
1424          if ( isset( $args->customize_preview_nav_menus_args['can_partial_refresh'] ) && $args->customize_preview_nav_menus_args['can_partial_refresh'] ) {
1425              $attributes       = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
1426              $attributes      .= ' data-customize-partial-type="nav_menu_instance"';
1427              $attributes      .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
1428              $nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . str_replace( '\\', '\\\\', $attributes ), $nav_menu_content, 1 );
1429          }
1430          return $nav_menu_content;
1431      }
1432  
1433      /**
1434       * Hashes (hmac) the nav menu arguments to ensure they are not tampered with when
1435       * submitted in the Ajax request.
1436       *
1437       * Note that the array is expected to be pre-sorted.
1438       *
1439       * @since 4.3.0
1440       *
1441       * @param array $args The arguments to hash.
1442       * @return string Hashed nav menu arguments.
1443       */
1444  	public function hash_nav_menu_args( $args ) {
1445          return wp_hash( serialize( $args ) );
1446      }
1447  
1448      /**
1449       * Enqueue scripts for the Customizer preview.
1450       *
1451       * @since 4.3.0
1452       */
1453  	public function customize_preview_enqueue_deps() {
1454          wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this.
1455      }
1456  
1457      /**
1458       * Exports data from PHP to JS.
1459       *
1460       * @since 4.3.0
1461       */
1462  	public function export_preview_data() {
1463  
1464          // Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
1465          $exports = array(
1466              'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
1467          );
1468          printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
1469      }
1470  
1471      /**
1472       * Export any wp_nav_menu() calls during the rendering of any partials.
1473       *
1474       * @since 4.5.0
1475       *
1476       * @param array $response Response.
1477       * @return array Response.
1478       */
1479  	public function export_partial_rendered_nav_menu_instances( $response ) {
1480          $response['nav_menu_instance_args'] = $this->preview_nav_menu_instance_args;
1481          return $response;
1482      }
1483  
1484      /**
1485       * Render a specific menu via wp_nav_menu() using the supplied arguments.
1486       *
1487       * @since 4.3.0
1488       *
1489       * @see wp_nav_menu()
1490       *
1491       * @param WP_Customize_Partial $partial       Partial.
1492       * @param array                $nav_menu_args Nav menu args supplied as container context.
1493       * @return string|false
1494       */
1495  	public function render_nav_menu_partial( $partial, $nav_menu_args ) {
1496          unset( $partial );
1497  
1498          if ( ! isset( $nav_menu_args['args_hmac'] ) ) {
1499              // Error: missing_args_hmac.
1500              return false;
1501          }
1502  
1503          $nav_menu_args_hmac = $nav_menu_args['args_hmac'];
1504          unset( $nav_menu_args['args_hmac'] );
1505  
1506          ksort( $nav_menu_args );
1507          if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) {
1508              // Error: args_hmac_mismatch.
1509              return false;
1510          }
1511  
1512          ob_start();
1513          wp_nav_menu( $nav_menu_args );
1514          $content = ob_get_clean();
1515  
1516          return $content;
1517      }
1518  }


Generated: Mon Jun 17 08:20:02 2019 Cross-referenced by PHPXref 0.7