[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

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


Generated : Sun Dec 22 08:20:01 2024 Cross-referenced by PHPXref