: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @output wp-admin/js/customize-nav-menus.js
/* global menus, _wpCustomizeNavMenusSettings, wpNavMenu, console */
( function( api, wp, $ ) {
* Set up wpNavMenu for drag and drop.
wpNavMenu.originalInit = wpNavMenu.init;
wpNavMenu.options.menuItemDepthPerLevel = 20;
wpNavMenu.options.sortableItems = '> .customize-control-nav_menu_item';
wpNavMenu.options.targetTolerance = 10;
wpNavMenu.init = function() {
* @namespace wp.customize.Menus
api.Menus = api.Menus || {};
settingTransport: 'refresh',
locationSlugMappedToName: {}
if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) {
$.extend( api.Menus.data, _wpCustomizeNavMenusSettings );
* Newly-created Nav Menus and Nav Menu Items have negative integer IDs which
* serve as placeholders until Save & Publish happens.
* @alias wp.customize.Menus.generatePlaceholderAutoIncrementId
api.Menus.generatePlaceholderAutoIncrementId = function() {
return -Math.ceil( api.Menus.data.phpIntMax * Math.random() );
* wp.customize.Menus.AvailableItemModel
* A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class.
* @class wp.customize.Menus.AvailableItemModel
* @augments Backbone.Model
api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend(
id: null // This is only used by Backbone.
api.Menus.data.defaultSettingValues.nav_menu_item
* wp.customize.Menus.AvailableItemCollection
* Collection for available menu item models.
* @class wp.customize.Menus.AvailableItemCollection
* @augments Backbone.Collection
api.Menus.AvailableItemCollection = Backbone.Collection.extend(/** @lends wp.customize.Menus.AvailableItemCollection.prototype */{
model: api.Menus.AvailableItemModel,
comparator: function( item ) {
return -item.get( this.sort_key );
sortByField: function( fieldName ) {
this.sort_key = fieldName;
api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
* Insert a new `auto-draft` post.
* @alias wp.customize.Menus.insertAutoDraftPost
* @param {Object} params - Parameters for the draft post to create.
* @param {string} params.post_type - Post type to add.
* @param {string} params.post_title - Post title to use.
* @return {jQuery.promise} Promise resolved with the added post.
api.Menus.insertAutoDraftPost = function insertAutoDraftPost( params ) {
var request, deferred = $.Deferred();
request = wp.ajax.post( 'customize-nav-menus-insert-auto-draft', {
'customize-menus-nonce': api.settings.nonce['customize-menus'],
'customize_changeset_uuid': api.settings.changeset.uuid,
request.done( function( response ) {
if ( response.post_id ) {
api( 'nav_menus_created_posts' ).set(
api( 'nav_menus_created_posts' ).get().concat( [ response.post_id ] )
if ( 'page' === params.post_type ) {
// Activate static front page controls as this could be the first page created.
if ( api.section.has( 'static_front_page' ) ) {
api.section( 'static_front_page' ).activate();
// Add new page to dropdown-pages controls.
api.control.each( function( control ) {
if ( 'dropdown-pages' === control.params.type ) {
select = control.container.find( 'select[name^="_customize-dropdown-pages-"]' );
select.append( new Option( params.post_title, response.post_id ) );
deferred.resolve( response );
request.fail( function( response ) {
var error = response || '';
if ( 'undefined' !== typeof response.message ) {
error = response.message;
deferred.rejectWith( error );
return deferred.promise();
api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Menus.AvailableMenuItemsPanelView.prototype */{
el: '#available-menu-items',
'input #menu-items-search': 'debounceSearch',
'focus .menu-item-tpl': 'focus',
'click .menu-item-tpl': '_submit',
'click #custom-menu-item-submit': '_submitLink',
'keypress #custom-menu-item-name': '_submitLink',
'click .new-content-item .add-content': '_submitNew',
'keypress .create-item-input': '_submitNew',
'keydown': 'keyboardAccessible'
// Cache current selected menu item.
// Cache menu control that opened the panel.
currentMenuControl: null,
* wp.customize.Menus.AvailableMenuItemsPanelView
* View class for the available menu items panel.
* @constructs wp.customize.Menus.AvailableMenuItemsPanelView
* @augments wp.Backbone.View
if ( ! api.panel.has( 'nav_menus' ) ) {
this.$search = $( '#menu-items-search' );
this.$clearResults = this.$el.find( '.clear-results' );
this.sectionContent = this.$el.find( '.available-menu-items-list' );
this.debounceSearch = _.debounce( self.search, 500 );
_.bindAll( this, 'close' );
* If the available menu items panel is open and the customize controls
* are interacted with (other than an item being deleted), then close
* the available menu items panel. Also close on back button click.
$( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
// Clear the search results and trigger an `input` event to fire a new search.
this.$clearResults.on( 'click', function() {
self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' );
this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
$( this ).removeClass( 'invalid' );
// Load available items if it looks like we'll need them.
api.panel( 'nav_menus' ).container.on( 'expanded', function() {
this.sectionContent.on( 'scroll', function() {
var totalHeight = self.$el.find( '.accordion-section.open .available-menu-items-list' ).prop( 'scrollHeight' ),
visibleHeight = self.$el.find( '.accordion-section.open' ).height();
if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
var type = $( this ).data( 'type' ),
object = $( this ).data( 'object' );
if ( 'search' === type ) {
self.doSearch( self.pages.search );
{ type: type, object: object }
// Close the panel if the URL in the preview changes.
api.previewer.bind( 'url', this.close );
// Search input change handler.
search: function( event ) {
var $searchSection = $( '#available-menu-items-search' ),
$otherSections = $( '#available-menu-items .accordion-section' ).not( $searchSection );
if ( this.searchTerm === event.target.value ) {
if ( '' !== event.target.value && ! $searchSection.hasClass( 'open' ) ) {
$otherSections.fadeOut( 100 );
$searchSection.find( '.accordion-section-content' ).slideDown( 'fast' );
$searchSection.addClass( 'open' );
this.$clearResults.addClass( 'is-visible' );
} else if ( '' === event.target.value ) {
$searchSection.removeClass( 'open' );
this.$clearResults.removeClass( 'is-visible' );
this.searchTerm = event.target.value;
doSearch: function( page ) {
$section = $( '#available-menu-items-search' ),
$content = $section.find( '.accordion-section-content' ),
itemTemplate = wp.template( 'available-menu-item' );
if ( self.currentRequest ) {
self.currentRequest.abort();
$section.addClass( 'loading-more' );
$content.attr( 'aria-busy', 'true' );
wp.a11y.speak( api.Menus.data.l10n.itemsLoadingMore );
} else if ( '' === self.searchTerm ) {
$section.addClass( 'loading' );
params = api.previewer.query( { excludeCustomizedSaved: true } );
'customize-menus-nonce': api.settings.nonce['customize-menus'],
'search': self.searchTerm,
self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
self.currentRequest.done(function( data ) {
// Clear previous results as it's a new search.
$section.removeClass( 'loading loading-more' );
$content.attr( 'aria-busy', 'false' );
$section.addClass( 'open' );
items = new api.Menus.AvailableItemCollection( data.items );
self.collection.add( items.models );
items.each( function( menuItem ) {
$content.append( itemTemplate( menuItem.attributes ) );
if ( 20 > items.length ) {
self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
self.pages.search = self.pages.search + 1;
if ( items && page > 1 ) {
wp.a11y.speak( api.Menus.data.l10n.itemsFoundMore.replace( '%d', items.length ) );
} else if ( items && page === 1 ) {
wp.a11y.speak( api.Menus.data.l10n.itemsFound.replace( '%d', items.length ) );
self.currentRequest.fail(function( data ) {
// data.message may be undefined, for example when typing slow and the request is aborted.
$content.empty().append( $( '<li class="nothing-found"></li>' ).text( data.message ) );
wp.a11y.speak( data.message );
self.currentRequest.always(function() {
$section.removeClass( 'loading loading-more' );
$content.attr( 'aria-busy', 'false' );
self.currentRequest = null;
// Render the individual items.
// Render the template for each item by type.
_.each( api.Menus.data.itemTypes, function( itemType ) {
self.pages[ itemType.type + ':' + itemType.object ] = 0;
self.loadItems( api.Menus.data.itemTypes );
* Load available nav menu items.
* @since 4.7.0 Changed function signature to take list of item types instead of single type/object.
* @param {Array.<Object>} itemTypes List of objects containing type and key.
* @param {string} deprecated Formerly the object parameter.
loadItems: function( itemTypes, deprecated ) {
var self = this, _itemTypes, requestItemTypes = [], params, request, itemTemplate, availableMenuItemContainers = {};
itemTemplate = wp.template( 'available-menu-item' );
if ( _.isString( itemTypes ) && _.isString( deprecated ) ) {
_itemTypes = [ { type: itemTypes, object: deprecated } ];
_.each( _itemTypes, function( itemType ) {
var container, name = itemType.type + ':' + itemType.object;
if ( -1 === self.pages[ name ] ) {
return; // Skip types for which there are no more results.
container = $( '#available-menu-items-' + itemType.type + '-' + itemType.object );
container.find( '.accordion-section-title' ).addClass( 'loading' );
availableMenuItemContainers[ name ] = container;
if ( 0 === requestItemTypes.length ) {
params = api.previewer.query( { excludeCustomizedSaved: true } );
'customize-menus-nonce': api.settings.nonce['customize-menus'],
'item_types': requestItemTypes
request = wp.ajax.post( 'load-available-menu-items-customizer', params );
request.done(function( data ) {
_.each( data.items, function( typeItems, name ) {
if ( 0 === typeItems.length ) {
if ( 0 === self.pages[ name ] ) {
availableMenuItemContainers[ name ].find( '.accordion-section-title' )
.addClass( 'cannot-expand' )
.removeClass( 'loading' )
.find( '.accordion-section-title > button' )
} else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) {
availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).trigger( 'click' );
typeItems = new api.Menus.AvailableItemCollection( typeItems ); // @todo Why is this collection created and then thrown away?
self.collection.add( typeItems.models );
typeInner = availableMenuItemContainers[ name ].find( '.available-menu-items-list' );
typeItems.each( function( menuItem ) {
typeInner.append( itemTemplate( menuItem.attributes ) );
request.fail(function( data ) {
if ( typeof console !== 'undefined' && console.error ) {
request.always(function() {
_.each( availableMenuItemContainers, function( container ) {
container.find( '.accordion-section-title' ).removeClass( 'loading' );
// Adjust the height of each section of items to fit the screen.
itemSectionHeight: function() {
var sections, lists, totalHeight, accordionHeight, diff;
totalHeight = window.innerHeight;
sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
lists = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")' );
accordionHeight = 46 * ( 1 + sections.length ) + 14; // Magic numbers.
diff = totalHeight - accordionHeight;
if ( 120 < diff && 290 > diff ) {
sections.css( 'max-height', diff );
lists.css( 'max-height', ( diff - 60 ) );
// Highlights a menu item.
select: function( menuitemTpl ) {
this.selected = $( menuitemTpl );
this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
this.selected.addClass( 'selected' );
// Highlights a menu item on focus.
focus: function( event ) {
this.select( $( event.currentTarget ) );
// Submit handler for keypress and click on menu item.
_submit: function( event ) {