diff --git a/src/wp-admin/js/customize-nav-menus.js b/src/wp-admin/js/customize-nav-menus.js index 58af669947..c51cff32cc 100644 --- a/src/wp-admin/js/customize-nav-menus.js +++ b/src/wp-admin/js/customize-nav-menus.js @@ -235,7 +235,9 @@ self.doSearch( self.pages.search ); } } else { - self.loadItems( type, object ); + self.loadItems( [ + { type: type, object: object } + ] ); } } }); @@ -360,53 +362,82 @@ // Render the template for each item by type. _.each( api.Menus.data.itemTypes, function( itemType ) { self.pages[ itemType.type + ':' + itemType.object ] = 0; - self.loadItems( itemType.type, itemType.object ); // @todo we need to combine these Ajax requests. } ); + self.loadItems( api.Menus.data.itemTypes ); }, - // Load available menu items. - loadItems: function( type, object ) { - var self = this, params, request, itemTemplate, availableMenuItemContainer; + /** + * Load available nav menu items. + * + * @since 4.3.0 + * @since 4.7.0 Changed function signature to take list of item types instead of single type/object. + * @access private + * + * @param {Array.} itemTypes List of objects containing type and key. + * @param {string} deprecated Formerly the object parameter. + * @returns {void} + */ + loadItems: function( itemTypes, deprecated ) { + var self = this, _itemTypes, requestItemTypes = [], request, itemTemplate, availableMenuItemContainers = {}; itemTemplate = wp.template( 'available-menu-item' ); - if ( -1 === self.pages[ type + ':' + object ] ) { + if ( _.isString( itemTypes ) && _.isString( deprecated ) ) { + _itemTypes = [ { type: itemTypes, object: deprecated } ]; + } else { + _itemTypes = itemTypes; + } + + _.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; + + requestItemTypes.push( { + object: itemType.object, + type: itemType.type, + page: self.pages[ name ] + } ); + } ); + + if ( 0 === requestItemTypes.length ) { return; } - availableMenuItemContainer = $( '#available-menu-items-' + type + '-' + object ); - availableMenuItemContainer.find( '.accordion-section-title' ).addClass( 'loading' ); + self.loading = true; - params = { + request = wp.ajax.post( 'load-available-menu-items-customizer', { 'customize-menus-nonce': api.settings.nonce['customize-menus'], 'wp_customize': 'on', - 'type': type, - 'object': object, - 'page': self.pages[ type + ':' + object ] - }; - request = wp.ajax.post( 'load-available-menu-items-customizer', params ); + 'item_types': requestItemTypes + } ); request.done(function( data ) { - var items, typeInner; - items = data.items; - if ( 0 === items.length ) { - if ( 0 === self.pages[ type + ':' + object ] ) { - availableMenuItemContainer - .addClass( 'cannot-expand' ) - .removeClass( 'loading' ) - .find( '.accordion-section-title > button' ) - .prop( 'tabIndex', -1 ); + var typeInner; + _.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' ) + .prop( 'tabIndex', -1 ); + } + self.pages[ name ] = -1; + return; + } else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) { + availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).click(); } - self.pages[ type + ':' + object ] = -1; - return; - } else if ( ( 'page' === object ) && ( ! availableMenuItemContainer.hasClass( 'open' ) ) ) { - availableMenuItemContainer.find( '.accordion-section-title > button' ).click(); - } - items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away? - self.collection.add( items.models ); - typeInner = availableMenuItemContainer.find( '.available-menu-items-list' ); - items.each(function( menuItem ) { - typeInner.append( itemTemplate( menuItem.attributes ) ); + 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 ) ); + } ); + self.pages[ name ] += 1; }); - self.pages[ type + ':' + object ] += 1; }); request.fail(function( data ) { if ( typeof console !== 'undefined' && console.error ) { @@ -414,7 +445,9 @@ } }); request.always(function() { - availableMenuItemContainer.find( '.accordion-section-title' ).removeClass( 'loading' ); + _.each( availableMenuItemContainers, function( container ) { + container.find( '.accordion-section-title' ).removeClass( 'loading' ); + } ); self.loading = false; }); }, diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php index 66dd4660a5..89b5b02a4a 100644 --- a/src/wp-includes/class-wp-customize-nav-menus.php +++ b/src/wp-includes/class-wp-customize-nav-menus.php @@ -100,20 +100,35 @@ final class WP_Customize_Nav_Menus { wp_die( -1 ); } - if ( empty( $_POST['type'] ) || empty( $_POST['object'] ) ) { + $all_items = array(); + $item_types = array(); + if ( isset( $_POST['item_types'] ) && is_array( $_POST['item_types'] ) ) { + $item_types = wp_unslash( $_POST['item_types'] ); + } elseif ( isset( $_POST['type'] ) && isset( $_POST['object'] ) ) { // Back compat. + $item_types[] = array( + 'type' => wp_unslash( $_POST['type'] ), + 'object' => wp_unslash( $_POST['object'] ), + 'page' => empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ), + ); + } else { wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' ); } - $type = sanitize_key( $_POST['type'] ); - $object = sanitize_key( $_POST['object'] ); - $page = empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ); - $items = $this->load_available_items_query( $type, $object, $page ); - - if ( is_wp_error( $items ) ) { - wp_send_json_error( $items->get_error_code() ); - } else { - wp_send_json_success( array( 'items' => $items ) ); + foreach ( $item_types as $item_type ) { + if ( empty( $item_type['type'] ) || empty( $item_type['object'] ) ) { + wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' ); + } + $type = sanitize_key( $item_type['type'] ); + $object = sanitize_key( $item_type['object'] ); + $page = empty( $item_type['page'] ) ? 0 : absint( $item_type['page'] ); + $items = $this->load_available_items_query( $type, $object, $page ); + if ( is_wp_error( $items ) ) { + wp_send_json_error( $items->get_error_code() ); + } + $all_items[ $item_type['type'] . ':' . $item_type['object'] ] = $items; } + + wp_send_json_success( array( 'items' => $all_items ) ); } /** diff --git a/tests/phpunit/tests/ajax/CustomizeMenus.php b/tests/phpunit/tests/ajax/CustomizeMenus.php index 8614350b2b..5013701d28 100644 --- a/tests/phpunit/tests/ajax/CustomizeMenus.php +++ b/tests/phpunit/tests/ajax/CustomizeMenus.php @@ -174,8 +174,8 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase { // Testing empty obj_type. array( array( - 'type' => '', - 'object' => 'post', + 'type' => 'post_type', + 'object' => '', ), array( 'success' => false, @@ -193,6 +193,25 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase { 'data' => 'nav_menus_missing_type_or_object_parameter', ), ), + // Testing empty type of a bulk request. + array( + array( + 'item_types' => array( + array( + 'type' => 'post_type', + 'object' => 'post', + ), + array( + 'type' => 'post_type', + 'object' => '', + ), + ), + ), + array( + 'success' => false, + 'data' => 'nav_menus_missing_type_or_object_parameter', + ), + ), // Testing incorrect type option. array( array( @@ -276,6 +295,22 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase { ), true, ), + // Testing a bulk request. + array( + array( + 'item_types' => array( + array( + 'type' => 'post_type', + 'object' => 'post', + ), + array( + 'type' => 'post_type', + 'object' => 'page', + ), + ), + ), + true, + ), ); } @@ -313,10 +348,11 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase { // Get the results. $response = json_decode( $this->_last_response, true ); - $this->assertNotEmpty( $response['data']['items'] ); + $this->assertNotEmpty( current( $response['data']['items'] ) ); // Get the second index to avoid the home page edge case. - $test_item = $response['data']['items'][1]; + $first_prop = current( $response['data']['items'] ); + $test_item = $first_prop[1]; foreach ( $expected_keys as $key ) { $this->assertArrayHasKey( $key, $test_item ); @@ -325,7 +361,8 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase { // Special test for the home page. if ( 'page' === $test_item['object'] ) { - $home = $response['data']['items'][0]; + $first_prop = current( $response['data']['items'] ); + $home = $first_prop[0]; foreach ( $expected_keys as $key ) { if ( 'object_id' !== $key ) { $this->assertArrayHasKey( $key, $home );