[ Index ]

PHP Cross Reference of WordPress

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                  array(
 214                      'taxonomy'     => $object,
 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              array(
 369                  'taxonomies' => $taxonomies,
 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. %s: 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 an invalid menu item. */
 466                  'invalidTitleTpl'        => __( '%s (Invalid)' ),
 467                  /* translators: %s: Title of a menu item in draft status. */
 468                  'pendingTitleTpl'        => __( '%s (Pending)' ),
 469                  /* translators: %d: Number of menu items found. */
 470                  'itemsFound'             => __( 'Number of items found: %d' ),
 471                  /* translators: %d: Number of additional menu items found. */
 472                  'itemsFoundMore'         => __( 'Additional items found: %d' ),
 473                  'itemsLoadingMore'       => __( 'Loading more results... please wait.' ),
 474                  'reorderModeOn'          => __( 'Reorder mode enabled' ),
 475                  'reorderModeOff'         => __( 'Reorder mode closed' ),
 476                  'reorderLabelOn'         => esc_attr__( 'Reorder menu items' ),
 477                  'reorderLabelOff'        => esc_attr__( 'Close reorder mode' ),
 478              ),
 479              'settingTransport'         => 'postMessage',
 480              'phpIntMax'                => PHP_INT_MAX,
 481              'defaultSettingValues'     => array(
 482                  'nav_menu'      => $temp_nav_menu_setting->default,
 483                  'nav_menu_item' => $temp_nav_menu_item_setting->default,
 484              ),
 485              'locationSlugMappedToName' => get_registered_nav_menus(),
 486          );
 487  
 488          $data = sprintf( 'var _wpCustomizeNavMenusSettings = %s;', wp_json_encode( $settings ) );
 489          wp_scripts()->add_data( 'customize-nav-menus', 'data', $data );
 490  
 491          // This is copied from nav-menus.php, and it has an unfortunate object name of `menus`.
 492          $nav_menus_l10n = array(
 493              'oneThemeLocationNoMenus' => null,
 494              'moveUp'                  => __( 'Move up one' ),
 495              'moveDown'                => __( 'Move down one' ),
 496              'moveToTop'               => __( 'Move to the top' ),
 497              /* translators: %s: Previous item name. */
 498              'moveUnder'               => __( 'Move under %s' ),
 499              /* translators: %s: Previous item name. */
 500              'moveOutFrom'             => __( 'Move out from under %s' ),
 501              /* translators: %s: Previous item name. */
 502              'under'                   => __( 'Under %s' ),
 503              /* translators: %s: Previous item name. */
 504              'outFrom'                 => __( 'Out from under %s' ),
 505              /* translators: 1: Item name, 2: Item position, 3: Total number of items. */
 506              'menuFocus'               => __( '%1$s. Menu item %2$d of %3$d.' ),
 507              /* translators: 1: Item name, 2: Item position, 3: Parent item name. */
 508              'subMenuFocus'            => __( '%1$s. Sub item number %2$d under %3$s.' ),
 509          );
 510          wp_localize_script( 'nav-menu', 'menus', $nav_menus_l10n );
 511      }
 512  
 513      /**
 514       * Filters a dynamic setting's constructor args.
 515       *
 516       * For a dynamic setting to be registered, this filter must be employed
 517       * to override the default false value with an array of args to pass to
 518       * the WP_Customize_Setting constructor.
 519       *
 520       * @since 4.3.0
 521       *
 522       * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
 523       * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
 524       * @return array|false
 525       */
 526  	public function filter_dynamic_setting_args( $setting_args, $setting_id ) {
 527          if ( preg_match( WP_Customize_Nav_Menu_Setting::ID_PATTERN, $setting_id ) ) {
 528              $setting_args = array(
 529                  'type'      => WP_Customize_Nav_Menu_Setting::TYPE,
 530                  'transport' => 'postMessage',
 531              );
 532          } elseif ( preg_match( WP_Customize_Nav_Menu_Item_Setting::ID_PATTERN, $setting_id ) ) {
 533              $setting_args = array(
 534                  'type'      => WP_Customize_Nav_Menu_Item_Setting::TYPE,
 535                  'transport' => 'postMessage',
 536              );
 537          }
 538          return $setting_args;
 539      }
 540  
 541      /**
 542       * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
 543       *
 544       * @since 4.3.0
 545       *
 546       * @param string $setting_class WP_Customize_Setting or a subclass.
 547       * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
 548       * @param array  $setting_args  WP_Customize_Setting or a subclass.
 549       * @return string
 550       */
 551  	public function filter_dynamic_setting_class( $setting_class, $setting_id, $setting_args ) {
 552          unset( $setting_id );
 553  
 554          if ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Setting::TYPE === $setting_args['type'] ) {
 555              $setting_class = 'WP_Customize_Nav_Menu_Setting';
 556          } elseif ( ! empty( $setting_args['type'] ) && WP_Customize_Nav_Menu_Item_Setting::TYPE === $setting_args['type'] ) {
 557              $setting_class = 'WP_Customize_Nav_Menu_Item_Setting';
 558          }
 559          return $setting_class;
 560      }
 561  
 562      /**
 563       * Add the customizer settings and controls.
 564       *
 565       * @since 4.3.0
 566       */
 567  	public function customize_register() {
 568          $changeset = $this->manager->unsanitized_post_values();
 569  
 570          // Preview settings for nav menus early so that the sections and controls will be added properly.
 571          $nav_menus_setting_ids = array();
 572          foreach ( array_keys( $changeset ) as $setting_id ) {
 573              if ( preg_match( '/^(nav_menu_locations|nav_menu|nav_menu_item)\[/', $setting_id ) ) {
 574                  $nav_menus_setting_ids[] = $setting_id;
 575              }
 576          }
 577          $settings = $this->manager->add_dynamic_settings( $nav_menus_setting_ids );
 578          if ( $this->manager->settings_previewed() ) {
 579              foreach ( $settings as $setting ) {
 580                  $setting->preview();
 581              }
 582          }
 583  
 584          // Require JS-rendered control types.
 585          $this->manager->register_panel_type( 'WP_Customize_Nav_Menus_Panel' );
 586          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Control' );
 587          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Name_Control' );
 588          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Locations_Control' );
 589          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Auto_Add_Control' );
 590          $this->manager->register_control_type( 'WP_Customize_Nav_Menu_Item_Control' );
 591  
 592          // Create a panel for Menus.
 593          $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>';
 594          if ( current_theme_supports( 'widgets' ) ) {
 595              $description .= '<p>' . sprintf(
 596                  /* translators: %s: URL to the Widgets panel of the Customizer. */
 597                  __( '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.' ),
 598                  "javascript:wp.customize.panel( 'widgets' ).focus();"
 599              ) . '</p>';
 600          } else {
 601              $description .= '<p>' . __( 'Menus can be displayed in locations defined by your theme.' ) . '</p>';
 602          }
 603  
 604          /*
 605           * Once multiple theme supports are allowed in WP_Customize_Panel,
 606           * this panel can be restricted to themes that support menus or widgets.
 607           */
 608          $this->manager->add_panel(
 609              new WP_Customize_Nav_Menus_Panel(
 610                  $this->manager,
 611                  'nav_menus',
 612                  array(
 613                      'title'       => __( 'Menus' ),
 614                      'description' => $description,
 615                      'priority'    => 100,
 616                  )
 617              )
 618          );
 619          $menus = wp_get_nav_menus();
 620  
 621          // Menu locations.
 622          $locations     = get_registered_nav_menus();
 623          $num_locations = count( $locations );
 624          if ( 1 == $num_locations ) {
 625              $description = '<p>' . __( 'Your theme can display menus in one location. Select which menu you would like to use.' ) . '</p>';
 626          } else {
 627              /* translators: %s: Number of menu locations. */
 628              $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>';
 629          }
 630  
 631          if ( current_theme_supports( 'widgets' ) ) {
 632              /* translators: URL to the Widgets panel of the Customizer. */
 633              $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>';
 634          }
 635  
 636          $this->manager->add_section(
 637              'menu_locations',
 638              array(
 639                  'title'       => 1 === $num_locations ? _x( 'View Location', 'menu locations' ) : _x( 'View All Locations', 'menu locations' ),
 640                  'panel'       => 'nav_menus',
 641                  'priority'    => 30,
 642                  'description' => $description,
 643              )
 644          );
 645  
 646          $choices = array( '0' => __( '&mdash; Select &mdash;' ) );
 647          foreach ( $menus as $menu ) {
 648              $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '&hellip;' );
 649          }
 650  
 651          // Attempt to re-map the nav menu location assignments when previewing a theme switch.
 652          $mapped_nav_menu_locations = array();
 653          if ( ! $this->manager->is_theme_active() ) {
 654              $theme_mods = get_option( 'theme_mods_' . $this->manager->get_stylesheet(), array() );
 655  
 656              // If there is no data from a previous activation, start fresh.
 657              if ( empty( $theme_mods['nav_menu_locations'] ) ) {
 658                  $theme_mods['nav_menu_locations'] = array();
 659              }
 660  
 661              $mapped_nav_menu_locations = wp_map_nav_menu_locations( $theme_mods['nav_menu_locations'], $this->original_nav_menu_locations );
 662          }
 663  
 664          foreach ( $locations as $location => $description ) {
 665              $setting_id = "nav_menu_locations[{$location}]";
 666  
 667              $setting = $this->manager->get_setting( $setting_id );
 668              if ( $setting ) {
 669                  $setting->transport = 'postMessage';
 670                  remove_filter( "customize_sanitize_{$setting_id}", 'absint' );
 671                  add_filter( "customize_sanitize_{$setting_id}", array( $this, 'intval_base10' ) );
 672              } else {
 673                  $this->manager->add_setting(
 674                      $setting_id,
 675                      array(
 676                          'sanitize_callback' => array( $this, 'intval_base10' ),
 677                          'theme_supports'    => 'menus',
 678                          'type'              => 'theme_mod',
 679                          'transport'         => 'postMessage',
 680                          'default'           => 0,
 681                      )
 682                  );
 683              }
 684  
 685              // Override the assigned nav menu location if mapped during previewed theme switch.
 686              if ( empty( $changeset[ $setting_id ] ) && isset( $mapped_nav_menu_locations[ $location ] ) ) {
 687                  $this->manager->set_post_value( $setting_id, $mapped_nav_menu_locations[ $location ] );
 688              }
 689  
 690              $this->manager->add_control(
 691                  new WP_Customize_Nav_Menu_Location_Control(
 692                      $this->manager,
 693                      $setting_id,
 694                      array(
 695                          'label'       => $description,
 696                          'location_id' => $location,
 697                          'section'     => 'menu_locations',
 698                          'choices'     => $choices,
 699                      )
 700                  )
 701              );
 702          }
 703  
 704          // Register each menu as a Customizer section, and add each menu item to each menu.
 705          foreach ( $menus as $menu ) {
 706              $menu_id = $menu->term_id;
 707  
 708              // Create a section for each menu.
 709              $section_id = 'nav_menu[' . $menu_id . ']';
 710              $this->manager->add_section(
 711                  new WP_Customize_Nav_Menu_Section(
 712                      $this->manager,
 713                      $section_id,
 714                      array(
 715                          'title'    => html_entity_decode( $menu->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
 716                          'priority' => 10,
 717                          'panel'    => 'nav_menus',
 718                      )
 719                  )
 720              );
 721  
 722              $nav_menu_setting_id = 'nav_menu[' . $menu_id . ']';
 723              $this->manager->add_setting(
 724                  new WP_Customize_Nav_Menu_Setting(
 725                      $this->manager,
 726                      $nav_menu_setting_id,
 727                      array(
 728                          'transport' => 'postMessage',
 729                      )
 730                  )
 731              );
 732  
 733              // Add the menu contents.
 734              $menu_items = (array) wp_get_nav_menu_items( $menu_id );
 735  
 736              foreach ( array_values( $menu_items ) as $i => $item ) {
 737  
 738                  // Create a setting for each menu item (which doesn't actually manage data, currently).
 739                  $menu_item_setting_id = 'nav_menu_item[' . $item->ID . ']';
 740  
 741                  $value = (array) $item;
 742                  if ( empty( $value['post_title'] ) ) {
 743                      $value['title'] = '';
 744                  }
 745  
 746                  $value['nav_menu_term_id'] = $menu_id;
 747                  $this->manager->add_setting(
 748                      new WP_Customize_Nav_Menu_Item_Setting(
 749                          $this->manager,
 750                          $menu_item_setting_id,
 751                          array(
 752                              'value'     => $value,
 753                              'transport' => 'postMessage',
 754                          )
 755                      )
 756                  );
 757  
 758                  // Create a control for each menu item.
 759                  $this->manager->add_control(
 760                      new WP_Customize_Nav_Menu_Item_Control(
 761                          $this->manager,
 762                          $menu_item_setting_id,
 763                          array(
 764                              'label'    => $item->title,
 765                              'section'  => $section_id,
 766                              'priority' => 10 + $i,
 767                          )
 768                      )
 769                  );
 770              }
 771  
 772              // Note: other controls inside of this section get added dynamically in JS via the MenuSection.ready() function.
 773          }
 774  
 775          // Add the add-new-menu section and controls.
 776          $this->manager->add_section(
 777              'add_menu',
 778              array(
 779                  'type'     => 'new_menu',
 780                  'title'    => __( 'New Menu' ),
 781                  'panel'    => 'nav_menus',
 782                  'priority' => 20,
 783              )
 784          );
 785  
 786          $this->manager->add_setting(
 787              new WP_Customize_Filter_Setting(
 788                  $this->manager,
 789                  'nav_menus_created_posts',
 790                  array(
 791                      'transport'         => 'postMessage',
 792                      'type'              => 'option', // To prevent theme prefix in changeset.
 793                      'default'           => array(),
 794                      'sanitize_callback' => array( $this, 'sanitize_nav_menus_created_posts' ),
 795                  )
 796              )
 797          );
 798      }
 799  
 800      /**
 801       * Get the base10 intval.
 802       *
 803       * This is used as a setting's sanitize_callback; we can't use just plain
 804       * intval because the second argument is not what intval() expects.
 805       *
 806       * @since 4.3.0
 807       *
 808       * @param mixed $value Number to convert.
 809       * @return int Integer.
 810       */
 811  	public function intval_base10( $value ) {
 812          return intval( $value, 10 );
 813      }
 814  
 815      /**
 816       * Return an array of all the available item types.
 817       *
 818       * @since 4.3.0
 819       * @since 4.7.0  Each array item now includes a `$type_label` in addition to `$title`, `$type`, and `$object`.
 820       *
 821       * @return array The available menu item types.
 822       */
 823  	public function available_item_types() {
 824          $item_types = array();
 825  
 826          $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
 827          if ( $post_types ) {
 828              foreach ( $post_types as $slug => $post_type ) {
 829                  $item_types[] = array(
 830                      'title'      => $post_type->labels->name,
 831                      'type_label' => $post_type->labels->singular_name,
 832                      'type'       => 'post_type',
 833                      'object'     => $post_type->name,
 834                  );
 835              }
 836          }
 837  
 838          $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' );
 839          if ( $taxonomies ) {
 840              foreach ( $taxonomies as $slug => $taxonomy ) {
 841                  if ( 'post_format' === $taxonomy && ! current_theme_supports( 'post-formats' ) ) {
 842                      continue;
 843                  }
 844                  $item_types[] = array(
 845                      'title'      => $taxonomy->labels->name,
 846                      'type_label' => $taxonomy->labels->singular_name,
 847                      'type'       => 'taxonomy',
 848                      'object'     => $taxonomy->name,
 849                  );
 850              }
 851          }
 852  
 853          /**
 854           * Filters the available menu item types.
 855           *
 856           * @since 4.3.0
 857           * @since 4.7.0  Each array item now includes a `$type_label` in addition to `$title`, `$type`, and `$object`.
 858           *
 859           * @param array $item_types Navigation menu item types.
 860           */
 861          $item_types = apply_filters( 'customize_nav_menu_available_item_types', $item_types );
 862  
 863          return $item_types;
 864      }
 865  
 866      /**
 867       * Add a new `auto-draft` post.
 868       *
 869       * @since 4.7.0
 870       *
 871       * @param array $postarr {
 872       *     Post array. Note that post_status is overridden to be `auto-draft`.
 873       *
 874       * @var string $post_title   Post title. Required.
 875       * @var string $post_type    Post type. Required.
 876       * @var string $post_name    Post name.
 877       * @var string $post_content Post content.
 878       * }
 879       * @return WP_Post|WP_Error Inserted auto-draft post object or error.
 880       */
 881  	public function insert_auto_draft_post( $postarr ) {
 882          if ( ! isset( $postarr['post_type'] ) ) {
 883              return new WP_Error( 'unknown_post_type', __( 'Invalid post type.' ) );
 884          }
 885          if ( empty( $postarr['post_title'] ) ) {
 886              return new WP_Error( 'empty_title', __( 'Empty title.' ) );
 887          }
 888          if ( ! empty( $postarr['post_status'] ) ) {
 889              return new WP_Error( 'status_forbidden', __( 'Status is forbidden.' ) );
 890          }
 891  
 892          /*
 893           * If the changeset is a draft, this will change to draft the next time the changeset
 894           * is updated; otherwise, auto-draft will persist in autosave revisions, until save.
 895           */
 896          $postarr['post_status'] = 'auto-draft';
 897  
 898          // Auto-drafts are allowed to have empty post_names, so it has to be explicitly set.
 899          if ( empty( $postarr['post_name'] ) ) {
 900              $postarr['post_name'] = sanitize_title( $postarr['post_title'] );
 901          }
 902          if ( ! isset( $postarr['meta_input'] ) ) {
 903              $postarr['meta_input'] = array();
 904          }
 905          $postarr['meta_input']['_customize_draft_post_name'] = $postarr['post_name'];
 906          $postarr['meta_input']['_customize_changeset_uuid']  = $this->manager->changeset_uuid();
 907          unset( $postarr['post_name'] );
 908  
 909          add_filter( 'wp_insert_post_empty_content', '__return_false', 1000 );
 910          $r = wp_insert_post( wp_slash( $postarr ), true );
 911          remove_filter( 'wp_insert_post_empty_content', '__return_false', 1000 );
 912  
 913          if ( is_wp_error( $r ) ) {
 914              return $r;
 915          } else {
 916              return get_post( $r );
 917          }
 918      }
 919  
 920      /**
 921       * Ajax handler for adding a new auto-draft post.
 922       *
 923       * @since 4.7.0
 924       */
 925  	public function ajax_insert_auto_draft_post() {
 926          if ( ! check_ajax_referer( 'customize-menus', 'customize-menus-nonce', false ) ) {
 927              wp_send_json_error( 'bad_nonce', 400 );
 928          }
 929  
 930          if ( ! current_user_can( 'customize' ) ) {
 931              wp_send_json_error( 'customize_not_allowed', 403 );
 932          }
 933  
 934          if ( empty( $_POST['params'] ) || ! is_array( $_POST['params'] ) ) {
 935              wp_send_json_error( 'missing_params', 400 );
 936          }
 937  
 938          $params         = wp_unslash( $_POST['params'] );
 939          $illegal_params = array_diff( array_keys( $params ), array( 'post_type', 'post_title' ) );
 940          if ( ! empty( $illegal_params ) ) {
 941              wp_send_json_error( 'illegal_params', 400 );
 942          }
 943  
 944          $params = array_merge(
 945              array(
 946                  'post_type'  => '',
 947                  'post_title' => '',
 948              ),
 949              $params
 950          );
 951  
 952          if ( empty( $params['post_type'] ) || ! post_type_exists( $params['post_type'] ) ) {
 953              status_header( 400 );
 954              wp_send_json_error( 'missing_post_type_param' );
 955          }
 956  
 957          $post_type_object = get_post_type_object( $params['post_type'] );
 958          if ( ! current_user_can( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) {
 959              status_header( 403 );
 960              wp_send_json_error( 'insufficient_post_permissions' );
 961          }
 962  
 963          $params['post_title'] = trim( $params['post_title'] );
 964          if ( '' === $params['post_title'] ) {
 965              status_header( 400 );
 966              wp_send_json_error( 'missing_post_title' );
 967          }
 968  
 969          $r = $this->insert_auto_draft_post( $params );
 970          if ( is_wp_error( $r ) ) {
 971              $error = $r;
 972              if ( ! empty( $post_type_object->labels->singular_name ) ) {
 973                  $singular_name = $post_type_object->labels->singular_name;
 974              } else {
 975                  $singular_name = __( 'Post' );
 976              }
 977  
 978              $data = array(
 979                  /* translators: 1: Post type name, 2: Error message. */
 980                  'message' => sprintf( __( '%1$s could not be created: %2$s' ), $singular_name, $error->get_error_message() ),
 981              );
 982              wp_send_json_error( $data );
 983          } else {
 984              $post = $r;
 985              $data = array(
 986                  'post_id' => $post->ID,
 987                  'url'     => get_permalink( $post->ID ),
 988              );
 989              wp_send_json_success( $data );
 990          }
 991      }
 992  
 993      /**
 994       * Print the JavaScript templates used to render Menu Customizer components.
 995       *
 996       * Templates are imported into the JS use wp.template.
 997       *
 998       * @since 4.3.0
 999       */
1000  	public function print_templates() {
1001          ?>
1002          <script type="text/html" id="tmpl-available-menu-item">
1003              <li id="menu-item-tpl-{{ data.id }}" class="menu-item-tpl" data-menu-item-id="{{ data.id }}">
1004                  <div class="menu-item-bar">
1005                      <div class="menu-item-handle">
1006                          <span class="item-type" aria-hidden="true">{{ data.type_label }}</span>
1007                          <span class="item-title" aria-hidden="true">
1008                              <span class="menu-item-title<# if ( ! data.title ) { #> no-title<# } #>">{{ data.title || wp.customize.Menus.data.l10n.untitled }}</span>
1009                          </span>
1010                          <button type="button" class="button-link item-add">
1011                              <span class="screen-reader-text">
1012                              <?php
1013                                  /* translators: 1: Title of a menu item, 2: Type of a menu item. */
1014                                  printf( __( 'Add to menu: %1$s (%2$s)' ), '{{ data.title || wp.customize.Menus.data.l10n.untitled }}', '{{ data.type_label }}' );
1015                              ?>
1016                              </span>
1017                          </button>
1018                      </div>
1019                  </div>
1020              </li>
1021          </script>
1022  
1023          <script type="text/html" id="tmpl-menu-item-reorder-nav">
1024              <div class="menu-item-reorder-nav">
1025                  <?php
1026                  printf(
1027                      '<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>',
1028                      __( 'Move up' ),
1029                      __( 'Move down' ),
1030                      __( 'Move one level up' ),
1031                      __( 'Move one level down' )
1032                  );
1033                  ?>
1034              </div>
1035          </script>
1036  
1037          <script type="text/html" id="tmpl-nav-menu-delete-button">
1038              <div class="menu-delete-item">
1039                  <button type="button" class="button-link button-link-delete">
1040                      <?php _e( 'Delete Menu' ); ?>
1041                  </button>
1042              </div>
1043          </script>
1044  
1045          <script type="text/html" id="tmpl-nav-menu-submit-new-button">
1046              <p id="customize-new-menu-submit-description"><?php _e( 'Click &#8220;Next&#8221; to start adding links to your new menu.' ); ?></p>
1047              <button id="customize-new-menu-submit" type="button" class="button" aria-describedby="customize-new-menu-submit-description"><?php _e( 'Next' ); ?></button>
1048          </script>
1049  
1050          <script type="text/html" id="tmpl-nav-menu-locations-header">
1051              <span class="customize-control-title customize-section-title-menu_locations-heading">{{ data.l10n.locationsTitle }}</span>
1052              <p class="customize-control-description customize-section-title-menu_locations-description">{{ data.l10n.locationsDescription }}</p>
1053          </script>
1054  
1055          <script type="text/html" id="tmpl-nav-menu-create-menu-section-title">
1056              <p class="add-new-menu-notice">
1057                  <?php _e( 'It doesn&#8217;t look like your site has any menus yet. Want to build one? Click the button to start.' ); ?>
1058              </p>
1059              <p class="add-new-menu-notice">
1060                  <?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.' ); ?>
1061              </p>
1062              <h3>
1063                  <button type="button" class="button customize-add-menu-button">
1064                      <?php _e( 'Create New Menu' ); ?>
1065                  </button>
1066              </h3>
1067          </script>
1068          <?php
1069      }
1070  
1071      /**
1072       * Print the html template used to render the add-menu-item frame.
1073       *
1074       * @since 4.3.0
1075       */
1076  	public function available_items_template() {
1077          ?>
1078          <div id="available-menu-items" class="accordion-container">
1079              <div class="customize-section-title">
1080                  <button type="button" class="customize-section-back" tabindex="-1">
1081                      <span class="screen-reader-text"><?php _e( 'Back' ); ?></span>
1082                  </button>
1083                  <h3>
1084                      <span class="customize-action">
1085                          <?php
1086                              /* translators: &#9656; is the unicode right-pointing triangle. %s: Section title in the Customizer. */
1087                              printf( __( 'Customizing &#9656; %s' ), esc_html( $this->manager->get_panel( 'nav_menus' )->title ) );
1088                          ?>
1089                      </span>
1090                      <?php _e( 'Add Menu Items' ); ?>
1091                  </h3>
1092              </div>
1093              <div id="available-menu-items-search" class="accordion-section cannot-expand">
1094                  <div class="accordion-section-title">
1095                      <label class="screen-reader-text" for="menu-items-search"><?php _e( 'Search Menu Items' ); ?></label>
1096                      <input type="text" id="menu-items-search" placeholder="<?php esc_attr_e( 'Search menu items&hellip;' ); ?>" aria-describedby="menu-items-search-desc" />
1097                      <p class="screen-reader-text" id="menu-items-search-desc"><?php _e( 'The search results will be updated as you type.' ); ?></p>
1098                      <span class="spinner"></span>
1099                  </div>
1100                  <div class="search-icon" aria-hidden="true"></div>
1101                  <button type="button" class="clear-results"><span class="screen-reader-text"><?php _e( 'Clear Results' ); ?></span></button>
1102                  <ul class="accordion-section-content available-menu-items-list" data-type="search"></ul>
1103              </div>
1104              <?php
1105  
1106              // Ensure the page post type comes first in the list.
1107              $item_types     = $this->available_item_types();
1108              $page_item_type = null;
1109              foreach ( $item_types as $i => $item_type ) {
1110                  if ( isset( $item_type['object'] ) && 'page' === $item_type['object'] ) {
1111                      $page_item_type = $item_type;
1112                      unset( $item_types[ $i ] );
1113                  }
1114              }
1115  
1116              $this->print_custom_links_available_menu_item();
1117              if ( $page_item_type ) {
1118                  $this->print_post_type_container( $page_item_type );
1119              }
1120              // Containers for per-post-type item browsing; items are added with JS.
1121              foreach ( $item_types as $item_type ) {
1122                  $this->print_post_type_container( $item_type );
1123              }
1124              ?>
1125          </div><!-- #available-menu-items -->
1126          <?php
1127      }
1128  
1129      /**
1130       * Print the markup for new menu items.
1131       *
1132       * To be used in the template #available-menu-items.
1133       *
1134       * @since 4.7.0
1135       *
1136       * @param array $available_item_type Menu item data to output, including title, type, and label.
1137       * @return void
1138       */
1139  	protected function print_post_type_container( $available_item_type ) {
1140          $id = sprintf( 'available-menu-items-%s-%s', $available_item_type['type'], $available_item_type['object'] );
1141          ?>
1142          <div id="<?php echo esc_attr( $id ); ?>" class="accordion-section">
1143              <h4 class="accordion-section-title" role="presentation">
1144                  <?php echo esc_html( $available_item_type['title'] ); ?>
1145                  <span class="spinner"></span>
1146                  <span class="no-items"><?php _e( 'No items' ); ?></span>
1147                  <button type="button" class="button-link" aria-expanded="false">
1148                      <span class="screen-reader-text">
1149                      <?php
1150                          /* translators: %s: Title of a section with menu items. */
1151                          printf( __( 'Toggle section: %s' ), esc_html( $available_item_type['title'] ) );
1152                      ?>
1153                          </span>
1154                      <span class="toggle-indicator" aria-hidden="true"></span>
1155                  </button>
1156              </h4>
1157              <div class="accordion-section-content">
1158                  <?php if ( 'post_type' === $available_item_type['type'] ) : ?>
1159                      <?php $post_type_obj = get_post_type_object( $available_item_type['object'] ); ?>
1160                      <?php if ( current_user_can( $post_type_obj->cap->create_posts ) && current_user_can( $post_type_obj->cap->publish_posts ) ) : ?>
1161                          <div class="new-content-item">
1162                              <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>
1163                              <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 ); ?>">
1164                              <button type="button" class="button add-content"><?php _e( 'Add' ); ?></button>
1165                          </div>
1166                      <?php endif; ?>
1167                  <?php endif; ?>
1168                  <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>
1169              </div>
1170          </div>
1171          <?php
1172      }
1173  
1174      /**
1175       * Print the markup for available menu item custom links.
1176       *
1177       * @since 4.7.0
1178       *
1179       * @return void
1180       */
1181  	protected function print_custom_links_available_menu_item() {
1182          ?>
1183          <div id="new-custom-menu-item" class="accordion-section">
1184              <h4 class="accordion-section-title" role="presentation">
1185                  <?php _e( 'Custom Links' ); ?>
1186                  <button type="button" class="button-link" aria-expanded="false">
1187                      <span class="screen-reader-text"><?php _e( 'Toggle section: Custom Links' ); ?></span>
1188                      <span class="toggle-indicator" aria-hidden="true"></span>
1189                  </button>
1190              </h4>
1191              <div class="accordion-section-content customlinkdiv">
1192                  <input type="hidden" value="custom" id="custom-menu-item-type" name="menu-item[-1][menu-item-type]" />
1193                  <p id="menu-item-url-wrap" class="wp-clearfix">
1194                      <label class="howto" for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
1195                      <input id="custom-menu-item-url" name="menu-item[-1][menu-item-url]" type="text" class="code menu-item-textbox" value="http://">
1196                  </p>
1197                  <p id="menu-item-name-wrap" class="wp-clearfix">
1198                      <label class="howto" for="custom-menu-item-name"><?php _e( 'Link Text' ); ?></label>
1199                      <input id="custom-menu-item-name" name="menu-item[-1][menu-item-title]" type="text" class="regular-text menu-item-textbox">
1200                  </p>
1201                  <p class="button-controls">
1202                      <span class="add-to-menu">
1203                          <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">
1204                          <span class="spinner"></span>
1205                      </span>
1206                  </p>
1207              </div>
1208          </div>
1209          <?php
1210      }
1211  
1212      //
1213      // Start functionality specific to partial-refresh of menu changes in Customizer preview.
1214      //
1215  
1216      /**
1217       * Nav menu args used for each instance, keyed by the args HMAC.
1218       *
1219       * @since 4.3.0
1220       * @var array
1221       */
1222      public $preview_nav_menu_instance_args = array();
1223  
1224      /**
1225       * Filters arguments for dynamic nav_menu selective refresh partials.
1226       *
1227       * @since 4.5.0
1228       *
1229       * @param array|false $partial_args Partial args.
1230       * @param string      $partial_id   Partial ID.
1231       * @return array Partial args.
1232       */
1233  	public function customize_dynamic_partial_args( $partial_args, $partial_id ) {
1234  
1235          if ( preg_match( '/^nav_menu_instance\[[0-9a-f]{32}\]$/', $partial_id ) ) {
1236              if ( false === $partial_args ) {
1237                  $partial_args = array();
1238              }
1239              $partial_args = array_merge(
1240                  $partial_args,
1241                  array(
1242                      'type'                => 'nav_menu_instance',
1243                      'render_callback'     => array( $this, 'render_nav_menu_partial' ),
1244                      'container_inclusive' => true,
1245                      'settings'            => array(), // Empty because the nav menu instance may relate to a menu or a location.
1246                      'capability'          => 'edit_theme_options',
1247                  )
1248              );
1249          }
1250  
1251          return $partial_args;
1252      }
1253  
1254      /**
1255       * Add hooks for the Customizer preview.
1256       *
1257       * @since 4.3.0
1258       */
1259  	public function customize_preview_init() {
1260          add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue_deps' ) );
1261          add_filter( 'wp_nav_menu_args', array( $this, 'filter_wp_nav_menu_args' ), 1000 );
1262          add_filter( 'wp_nav_menu', array( $this, 'filter_wp_nav_menu' ), 10, 2 );
1263          add_filter( 'wp_footer', array( $this, 'export_preview_data' ), 1 );
1264          add_filter( 'customize_render_partials_response', array( $this, 'export_partial_rendered_nav_menu_instances' ) );
1265      }
1266  
1267      /**
1268       * Make the auto-draft status protected so that it can be queried.
1269       *
1270       * @since 4.7.0
1271       *
1272       * @global array $wp_post_statuses List of post statuses.
1273       */
1274  	public function make_auto_draft_status_previewable() {
1275          global $wp_post_statuses;
1276          $wp_post_statuses['auto-draft']->protected = true;
1277      }
1278  
1279      /**
1280       * Sanitize post IDs for posts created for nav menu items to be published.
1281       *
1282       * @since 4.7.0
1283       *
1284       * @param array $value Post IDs.
1285       * @returns array Post IDs.
1286       */
1287  	public function sanitize_nav_menus_created_posts( $value ) {
1288          $post_ids = array();
1289          foreach ( wp_parse_id_list( $value ) as $post_id ) {
1290              if ( empty( $post_id ) ) {
1291                  continue;
1292              }
1293              $post = get_post( $post_id );
1294              if ( 'auto-draft' !== $post->post_status && 'draft' !== $post->post_status ) {
1295                  continue;
1296              }
1297              $post_type_obj = get_post_type_object( $post->post_type );
1298              if ( ! $post_type_obj ) {
1299                  continue;
1300              }
1301              if ( ! current_user_can( $post_type_obj->cap->publish_posts ) || ! current_user_can( $post_type_obj->cap->edit_post, $post_id ) ) {
1302                  continue;
1303              }
1304              $post_ids[] = $post->ID;
1305          }
1306          return $post_ids;
1307      }
1308  
1309      /**
1310       * Publish the auto-draft posts that were created for nav menu items.
1311       *
1312       * The post IDs will have been sanitized by already by
1313       * `WP_Customize_Nav_Menu_Items::sanitize_nav_menus_created_posts()` to
1314       * remove any post IDs for which the user cannot publish or for which the
1315       * post is not an auto-draft.
1316       *
1317       * @since 4.7.0
1318       *
1319       * @param WP_Customize_Setting $setting Customizer setting object.
1320       */
1321  	public function save_nav_menus_created_posts( $setting ) {
1322          $post_ids = $setting->post_value();
1323          if ( ! empty( $post_ids ) ) {
1324              foreach ( $post_ids as $post_id ) {
1325  
1326                  // Prevent overriding the status that a user may have prematurely updated the post to.
1327                  $current_status = get_post_status( $post_id );
1328                  if ( 'auto-draft' !== $current_status && 'draft' !== $current_status ) {
1329                      continue;
1330                  }
1331  
1332                  $target_status = 'attachment' === get_post_type( $post_id ) ? 'inherit' : 'publish';
1333                  $args          = array(
1334                      'ID'          => $post_id,
1335                      'post_status' => $target_status,
1336                  );
1337                  $post_name     = get_post_meta( $post_id, '_customize_draft_post_name', true );
1338                  if ( $post_name ) {
1339                      $args['post_name'] = $post_name;
1340                  }
1341  
1342                  // Note that wp_publish_post() cannot be used because unique slugs need to be assigned.
1343                  wp_update_post( wp_slash( $args ) );
1344  
1345                  delete_post_meta( $post_id, '_customize_draft_post_name' );
1346              }
1347          }
1348      }
1349  
1350      /**
1351       * Keep track of the arguments that are being passed to wp_nav_menu().
1352       *
1353       * @since 4.3.0
1354       * @see wp_nav_menu()
1355       * @see WP_Customize_Widgets::filter_dynamic_sidebar_params()
1356       *
1357       * @param array $args An array containing wp_nav_menu() arguments.
1358       * @return array Arguments.
1359       */
1360  	public function filter_wp_nav_menu_args( $args ) {
1361          /*
1362           * The following conditions determine whether or not this instance of
1363           * wp_nav_menu() can use selective refreshed. A wp_nav_menu() can be
1364           * selective refreshed if...
1365           */
1366          $can_partial_refresh = (
1367              // ...if wp_nav_menu() is directly echoing out the menu (and thus isn't manipulating the string after generated),
1368              ! empty( $args['echo'] )
1369              &&
1370              // ...and if the fallback_cb can be serialized to JSON, since it will be included in the placement context data,
1371              ( empty( $args['fallback_cb'] ) || is_string( $args['fallback_cb'] ) )
1372              &&
1373              // ...and if the walker can also be serialized to JSON, since it will be included in the placement context data as well,
1374              ( empty( $args['walker'] ) || is_string( $args['walker'] ) )
1375              // ...and if it has a theme location assigned or an assigned menu to display,
1376              && (
1377                  ! empty( $args['theme_location'] )
1378                  ||
1379                  ( ! empty( $args['menu'] ) && ( is_numeric( $args['menu'] ) || is_object( $args['menu'] ) ) )
1380              )
1381              &&
1382              // ...and if the nav menu would be rendered with a wrapper container element (upon which to attach data-* attributes).
1383              (
1384                  ! empty( $args['container'] )
1385                  ||
1386                  ( isset( $args['items_wrap'] ) && '<' === substr( $args['items_wrap'], 0, 1 ) )
1387              )
1388          );
1389          $args['can_partial_refresh'] = $can_partial_refresh;
1390  
1391          $exported_args = $args;
1392  
1393          // Empty out args which may not be JSON-serializable.
1394          if ( ! $can_partial_refresh ) {
1395              $exported_args['fallback_cb'] = '';
1396              $exported_args['walker']      = '';
1397          }
1398  
1399          /*
1400           * Replace object menu arg with a term_id menu arg, as this exports better
1401           * to JS and is easier to compare hashes.
1402           */
1403          if ( ! empty( $exported_args['menu'] ) && is_object( $exported_args['menu'] ) ) {
1404              $exported_args['menu'] = $exported_args['menu']->term_id;
1405          }
1406  
1407          ksort( $exported_args );
1408          $exported_args['args_hmac'] = $this->hash_nav_menu_args( $exported_args );
1409  
1410          $args['customize_preview_nav_menus_args']                            = $exported_args;
1411          $this->preview_nav_menu_instance_args[ $exported_args['args_hmac'] ] = $exported_args;
1412          return $args;
1413      }
1414  
1415      /**
1416       * Prepares wp_nav_menu() calls for partial refresh.
1417       *
1418       * Injects attributes into container element.
1419       *
1420       * @since 4.3.0
1421       *
1422       * @see wp_nav_menu()
1423       *
1424       * @param string $nav_menu_content The HTML content for the navigation menu.
1425       * @param object $args             An object containing wp_nav_menu() arguments.
1426       * @return string Nav menu HTML with selective refresh attributes added if partial can be refreshed.
1427       */
1428  	public function filter_wp_nav_menu( $nav_menu_content, $args ) {
1429          if ( isset( $args->customize_preview_nav_menus_args['can_partial_refresh'] ) && $args->customize_preview_nav_menus_args['can_partial_refresh'] ) {
1430              $attributes       = sprintf( ' data-customize-partial-id="%s"', esc_attr( 'nav_menu_instance[' . $args->customize_preview_nav_menus_args['args_hmac'] . ']' ) );
1431              $attributes      .= ' data-customize-partial-type="nav_menu_instance"';
1432              $attributes      .= sprintf( ' data-customize-partial-placement-context="%s"', esc_attr( wp_json_encode( $args->customize_preview_nav_menus_args ) ) );
1433              $nav_menu_content = preg_replace( '#^(<\w+)#', '$1 ' . str_replace( '\\', '\\\\', $attributes ), $nav_menu_content, 1 );
1434          }
1435          return $nav_menu_content;
1436      }
1437  
1438      /**
1439       * Hashes (hmac) the nav menu arguments to ensure they are not tampered with when
1440       * submitted in the Ajax request.
1441       *
1442       * Note that the array is expected to be pre-sorted.
1443       *
1444       * @since 4.3.0
1445       *
1446       * @param array $args The arguments to hash.
1447       * @return string Hashed nav menu arguments.
1448       */
1449  	public function hash_nav_menu_args( $args ) {
1450          return wp_hash( serialize( $args ) );
1451      }
1452  
1453      /**
1454       * Enqueue scripts for the Customizer preview.
1455       *
1456       * @since 4.3.0
1457       */
1458  	public function customize_preview_enqueue_deps() {
1459          wp_enqueue_script( 'customize-preview-nav-menus' ); // Note that we have overridden this.
1460      }
1461  
1462      /**
1463       * Exports data from PHP to JS.
1464       *
1465       * @since 4.3.0
1466       */
1467  	public function export_preview_data() {
1468  
1469          // Why not wp_localize_script? Because we're not localizing, and it forces values into strings.
1470          $exports = array(
1471              'navMenuInstanceArgs' => $this->preview_nav_menu_instance_args,
1472          );
1473          printf( '<script>var _wpCustomizePreviewNavMenusExports = %s;</script>', wp_json_encode( $exports ) );
1474      }
1475  
1476      /**
1477       * Export any wp_nav_menu() calls during the rendering of any partials.
1478       *
1479       * @since 4.5.0
1480       *
1481       * @param array $response Response.
1482       * @return array Response.
1483       */
1484  	public function export_partial_rendered_nav_menu_instances( $response ) {
1485          $response['nav_menu_instance_args'] = $this->preview_nav_menu_instance_args;
1486          return $response;
1487      }
1488  
1489      /**
1490       * Render a specific menu via wp_nav_menu() using the supplied arguments.
1491       *
1492       * @since 4.3.0
1493       *
1494       * @see wp_nav_menu()
1495       *
1496       * @param WP_Customize_Partial $partial       Partial.
1497       * @param array                $nav_menu_args Nav menu args supplied as container context.
1498       * @return string|false
1499       */
1500  	public function render_nav_menu_partial( $partial, $nav_menu_args ) {
1501          unset( $partial );
1502  
1503          if ( ! isset( $nav_menu_args['args_hmac'] ) ) {
1504              // Error: missing_args_hmac.
1505              return false;
1506          }
1507  
1508          $nav_menu_args_hmac = $nav_menu_args['args_hmac'];
1509          unset( $nav_menu_args['args_hmac'] );
1510  
1511          ksort( $nav_menu_args );
1512          if ( ! hash_equals( $this->hash_nav_menu_args( $nav_menu_args ), $nav_menu_args_hmac ) ) {
1513              // Error: args_hmac_mismatch.
1514              return false;
1515          }
1516  
1517          ob_start();
1518          wp_nav_menu( $nav_menu_args );
1519          $content = ob_get_clean();
1520  
1521          return $content;
1522      }
1523  }


Generated: Thu Sep 19 01:00:03 2019 Cross-referenced by PHPXref 0.7.1