ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

nav-menu.js (50595B)


      1 /**
      2  * WordPress Administration Navigation Menu
      3  * Interface JS functions
      4  *
      5  * @version 2.0.0
      6  *
      7  * @package WordPress
      8  * @subpackage Administration
      9  * @output wp-admin/js/nav-menu.js
     10  */
     11 
     12 /* global menus, postboxes, columns, isRtl, ajaxurl, wpNavMenu */
     13 
     14 (function($) {
     15 
     16 	var api;
     17 
     18 	/**
     19 	 * Contains all the functions to handle WordPress navigation menus administration.
     20 	 *
     21 	 * @namespace wpNavMenu
     22 	 */
     23 	api = window.wpNavMenu = {
     24 
     25 		options : {
     26 			menuItemDepthPerLevel : 30, // Do not use directly. Use depthToPx and pxToDepth instead.
     27 			globalMaxDepth:  11,
     28 			sortableItems:   '> *',
     29 			targetTolerance: 0
     30 		},
     31 
     32 		menuList : undefined,	// Set in init.
     33 		targetList : undefined, // Set in init.
     34 		menusChanged : false,
     35 		isRTL: !! ( 'undefined' != typeof isRtl && isRtl ),
     36 		negateIfRTL: ( 'undefined' != typeof isRtl && isRtl ) ? -1 : 1,
     37 		lastSearch: '',
     38 
     39 		// Functions that run on init.
     40 		init : function() {
     41 			api.menuList = $('#menu-to-edit');
     42 			api.targetList = api.menuList;
     43 
     44 			this.jQueryExtensions();
     45 
     46 			this.attachMenuEditListeners();
     47 
     48 			this.attachBulkSelectButtonListeners();
     49 			this.attachMenuCheckBoxListeners();
     50 			this.attachMenuItemDeleteButton();
     51 			this.attachPendingMenuItemsListForDeletion();
     52 
     53 			this.attachQuickSearchListeners();
     54 			this.attachThemeLocationsListeners();
     55 			this.attachMenuSaveSubmitListeners();
     56 
     57 			this.attachTabsPanelListeners();
     58 
     59 			this.attachUnsavedChangesListener();
     60 
     61 			if ( api.menuList.length )
     62 				this.initSortables();
     63 
     64 			if ( menus.oneThemeLocationNoMenus )
     65 				$( '#posttype-page' ).addSelectedToMenu( api.addMenuItemToBottom );
     66 
     67 			this.initManageLocations();
     68 
     69 			this.initAccessibility();
     70 
     71 			this.initToggles();
     72 
     73 			this.initPreviewing();
     74 		},
     75 
     76 		jQueryExtensions : function() {
     77 			// jQuery extensions.
     78 			$.fn.extend({
     79 				menuItemDepth : function() {
     80 					var margin = api.isRTL ? this.eq(0).css('margin-right') : this.eq(0).css('margin-left');
     81 					return api.pxToDepth( margin && -1 != margin.indexOf('px') ? margin.slice(0, -2) : 0 );
     82 				},
     83 				updateDepthClass : function(current, prev) {
     84 					return this.each(function(){
     85 						var t = $(this);
     86 						prev = prev || t.menuItemDepth();
     87 						$(this).removeClass('menu-item-depth-'+ prev )
     88 							.addClass('menu-item-depth-'+ current );
     89 					});
     90 				},
     91 				shiftDepthClass : function(change) {
     92 					return this.each(function(){
     93 						var t = $(this),
     94 							depth = t.menuItemDepth(),
     95 							newDepth = depth + change;
     96 
     97 						t.removeClass( 'menu-item-depth-'+ depth )
     98 							.addClass( 'menu-item-depth-'+ ( newDepth ) );
     99 
    100 						if ( 0 === newDepth ) {
    101 							t.find( '.is-submenu' ).hide();
    102 						}
    103 					});
    104 				},
    105 				childMenuItems : function() {
    106 					var result = $();
    107 					this.each(function(){
    108 						var t = $(this), depth = t.menuItemDepth(), next = t.next( '.menu-item' );
    109 						while( next.length && next.menuItemDepth() > depth ) {
    110 							result = result.add( next );
    111 							next = next.next( '.menu-item' );
    112 						}
    113 					});
    114 					return result;
    115 				},
    116 				shiftHorizontally : function( dir ) {
    117 					return this.each(function(){
    118 						var t = $(this),
    119 							depth = t.menuItemDepth(),
    120 							newDepth = depth + dir;
    121 
    122 						// Change .menu-item-depth-n class.
    123 						t.moveHorizontally( newDepth, depth );
    124 					});
    125 				},
    126 				moveHorizontally : function( newDepth, depth ) {
    127 					return this.each(function(){
    128 						var t = $(this),
    129 							children = t.childMenuItems(),
    130 							diff = newDepth - depth,
    131 							subItemText = t.find('.is-submenu');
    132 
    133 						// Change .menu-item-depth-n class.
    134 						t.updateDepthClass( newDepth, depth ).updateParentMenuItemDBId();
    135 
    136 						// If it has children, move those too.
    137 						if ( children ) {
    138 							children.each(function() {
    139 								var t = $(this),
    140 									thisDepth = t.menuItemDepth(),
    141 									newDepth = thisDepth + diff;
    142 								t.updateDepthClass(newDepth, thisDepth).updateParentMenuItemDBId();
    143 							});
    144 						}
    145 
    146 						// Show "Sub item" helper text.
    147 						if (0 === newDepth)
    148 							subItemText.hide();
    149 						else
    150 							subItemText.show();
    151 					});
    152 				},
    153 				updateParentMenuItemDBId : function() {
    154 					return this.each(function(){
    155 						var item = $(this),
    156 							input = item.find( '.menu-item-data-parent-id' ),
    157 							depth = parseInt( item.menuItemDepth(), 10 ),
    158 							parentDepth = depth - 1,
    159 							parent = item.prevAll( '.menu-item-depth-' + parentDepth ).first();
    160 
    161 						if ( 0 === depth ) { // Item is on the top level, has no parent.
    162 							input.val(0);
    163 						} else { // Find the parent item, and retrieve its object id.
    164 							input.val( parent.find( '.menu-item-data-db-id' ).val() );
    165 						}
    166 					});
    167 				},
    168 				hideAdvancedMenuItemFields : function() {
    169 					return this.each(function(){
    170 						var that = $(this);
    171 						$('.hide-column-tog').not(':checked').each(function(){
    172 							that.find('.field-' + $(this).val() ).addClass('hidden-field');
    173 						});
    174 					});
    175 				},
    176 				/**
    177 				 * Adds selected menu items to the menu.
    178 				 *
    179 				 * @ignore
    180 				 *
    181 				 * @param jQuery metabox The metabox jQuery object.
    182 				 */
    183 				addSelectedToMenu : function(processMethod) {
    184 					if ( 0 === $('#menu-to-edit').length ) {
    185 						return false;
    186 					}
    187 
    188 					return this.each(function() {
    189 						var t = $(this), menuItems = {},
    190 							checkboxes = ( menus.oneThemeLocationNoMenus && 0 === t.find( '.tabs-panel-active .categorychecklist li input:checked' ).length ) ? t.find( '#page-all li input[type="checkbox"]' ) : t.find( '.tabs-panel-active .categorychecklist li input:checked' ),
    191 							re = /menu-item\[([^\]]*)/;
    192 
    193 						processMethod = processMethod || api.addMenuItemToBottom;
    194 
    195 						// If no items are checked, bail.
    196 						if ( !checkboxes.length )
    197 							return false;
    198 
    199 						// Show the Ajax spinner.
    200 						t.find( '.button-controls .spinner' ).addClass( 'is-active' );
    201 
    202 						// Retrieve menu item data.
    203 						$(checkboxes).each(function(){
    204 							var t = $(this),
    205 								listItemDBIDMatch = re.exec( t.attr('name') ),
    206 								listItemDBID = 'undefined' == typeof listItemDBIDMatch[1] ? 0 : parseInt(listItemDBIDMatch[1], 10);
    207 
    208 							if ( this.className && -1 != this.className.indexOf('add-to-top') )
    209 								processMethod = api.addMenuItemToTop;
    210 							menuItems[listItemDBID] = t.closest('li').getItemData( 'add-menu-item', listItemDBID );
    211 						});
    212 
    213 						// Add the items.
    214 						api.addItemToMenu(menuItems, processMethod, function(){
    215 							// Deselect the items and hide the Ajax spinner.
    216 							checkboxes.prop( 'checked', false );
    217 							t.find( '.button-controls .select-all' ).prop( 'checked', false );
    218 							t.find( '.button-controls .spinner' ).removeClass( 'is-active' );
    219 						});
    220 					});
    221 				},
    222 				getItemData : function( itemType, id ) {
    223 					itemType = itemType || 'menu-item';
    224 
    225 					var itemData = {}, i,
    226 					fields = [
    227 						'menu-item-db-id',
    228 						'menu-item-object-id',
    229 						'menu-item-object',
    230 						'menu-item-parent-id',
    231 						'menu-item-position',
    232 						'menu-item-type',
    233 						'menu-item-title',
    234 						'menu-item-url',
    235 						'menu-item-description',
    236 						'menu-item-attr-title',
    237 						'menu-item-target',
    238 						'menu-item-classes',
    239 						'menu-item-xfn'
    240 					];
    241 
    242 					if( !id && itemType == 'menu-item' ) {
    243 						id = this.find('.menu-item-data-db-id').val();
    244 					}
    245 
    246 					if( !id ) return itemData;
    247 
    248 					this.find('input').each(function() {
    249 						var field;
    250 						i = fields.length;
    251 						while ( i-- ) {
    252 							if( itemType == 'menu-item' )
    253 								field = fields[i] + '[' + id + ']';
    254 							else if( itemType == 'add-menu-item' )
    255 								field = 'menu-item[' + id + '][' + fields[i] + ']';
    256 
    257 							if (
    258 								this.name &&
    259 								field == this.name
    260 							) {
    261 								itemData[fields[i]] = this.value;
    262 							}
    263 						}
    264 					});
    265 
    266 					return itemData;
    267 				},
    268 				setItemData : function( itemData, itemType, id ) { // Can take a type, such as 'menu-item', or an id.
    269 					itemType = itemType || 'menu-item';
    270 
    271 					if( !id && itemType == 'menu-item' ) {
    272 						id = $('.menu-item-data-db-id', this).val();
    273 					}
    274 
    275 					if( !id ) return this;
    276 
    277 					this.find('input').each(function() {
    278 						var t = $(this), field;
    279 						$.each( itemData, function( attr, val ) {
    280 							if( itemType == 'menu-item' )
    281 								field = attr + '[' + id + ']';
    282 							else if( itemType == 'add-menu-item' )
    283 								field = 'menu-item[' + id + '][' + attr + ']';
    284 
    285 							if ( field == t.attr('name') ) {
    286 								t.val( val );
    287 							}
    288 						});
    289 					});
    290 					return this;
    291 				}
    292 			});
    293 		},
    294 
    295 		countMenuItems : function( depth ) {
    296 			return $( '.menu-item-depth-' + depth ).length;
    297 		},
    298 
    299 		moveMenuItem : function( $this, dir ) {
    300 
    301 			var items, newItemPosition, newDepth,
    302 				menuItems = $( '#menu-to-edit li' ),
    303 				menuItemsCount = menuItems.length,
    304 				thisItem = $this.parents( 'li.menu-item' ),
    305 				thisItemChildren = thisItem.childMenuItems(),
    306 				thisItemData = thisItem.getItemData(),
    307 				thisItemDepth = parseInt( thisItem.menuItemDepth(), 10 ),
    308 				thisItemPosition = parseInt( thisItem.index(), 10 ),
    309 				nextItem = thisItem.next(),
    310 				nextItemChildren = nextItem.childMenuItems(),
    311 				nextItemDepth = parseInt( nextItem.menuItemDepth(), 10 ) + 1,
    312 				prevItem = thisItem.prev(),
    313 				prevItemDepth = parseInt( prevItem.menuItemDepth(), 10 ),
    314 				prevItemId = prevItem.getItemData()['menu-item-db-id'];
    315 
    316 			switch ( dir ) {
    317 			case 'up':
    318 				newItemPosition = thisItemPosition - 1;
    319 
    320 				// Already at top.
    321 				if ( 0 === thisItemPosition )
    322 					break;
    323 
    324 				// If a sub item is moved to top, shift it to 0 depth.
    325 				if ( 0 === newItemPosition && 0 !== thisItemDepth )
    326 					thisItem.moveHorizontally( 0, thisItemDepth );
    327 
    328 				// If prev item is sub item, shift to match depth.
    329 				if ( 0 !== prevItemDepth )
    330 					thisItem.moveHorizontally( prevItemDepth, thisItemDepth );
    331 
    332 				// Does this item have sub items?
    333 				if ( thisItemChildren ) {
    334 					items = thisItem.add( thisItemChildren );
    335 					// Move the entire block.
    336 					items.detach().insertBefore( menuItems.eq( newItemPosition ) ).updateParentMenuItemDBId();
    337 				} else {
    338 					thisItem.detach().insertBefore( menuItems.eq( newItemPosition ) ).updateParentMenuItemDBId();
    339 				}
    340 				break;
    341 			case 'down':
    342 				// Does this item have sub items?
    343 				if ( thisItemChildren ) {
    344 					items = thisItem.add( thisItemChildren ),
    345 						nextItem = menuItems.eq( items.length + thisItemPosition ),
    346 						nextItemChildren = 0 !== nextItem.childMenuItems().length;
    347 
    348 					if ( nextItemChildren ) {
    349 						newDepth = parseInt( nextItem.menuItemDepth(), 10 ) + 1;
    350 						thisItem.moveHorizontally( newDepth, thisItemDepth );
    351 					}
    352 
    353 					// Have we reached the bottom?
    354 					if ( menuItemsCount === thisItemPosition + items.length )
    355 						break;
    356 
    357 					items.detach().insertAfter( menuItems.eq( thisItemPosition + items.length ) ).updateParentMenuItemDBId();
    358 				} else {
    359 					// If next item has sub items, shift depth.
    360 					if ( 0 !== nextItemChildren.length )
    361 						thisItem.moveHorizontally( nextItemDepth, thisItemDepth );
    362 
    363 					// Have we reached the bottom?
    364 					if ( menuItemsCount === thisItemPosition + 1 )
    365 						break;
    366 					thisItem.detach().insertAfter( menuItems.eq( thisItemPosition + 1 ) ).updateParentMenuItemDBId();
    367 				}
    368 				break;
    369 			case 'top':
    370 				// Already at top.
    371 				if ( 0 === thisItemPosition )
    372 					break;
    373 				// Does this item have sub items?
    374 				if ( thisItemChildren ) {
    375 					items = thisItem.add( thisItemChildren );
    376 					// Move the entire block.
    377 					items.detach().insertBefore( menuItems.eq( 0 ) ).updateParentMenuItemDBId();
    378 				} else {
    379 					thisItem.detach().insertBefore( menuItems.eq( 0 ) ).updateParentMenuItemDBId();
    380 				}
    381 				break;
    382 			case 'left':
    383 				// As far left as possible.
    384 				if ( 0 === thisItemDepth )
    385 					break;
    386 				thisItem.shiftHorizontally( -1 );
    387 				break;
    388 			case 'right':
    389 				// Can't be sub item at top.
    390 				if ( 0 === thisItemPosition )
    391 					break;
    392 				// Already sub item of prevItem.
    393 				if ( thisItemData['menu-item-parent-id'] === prevItemId )
    394 					break;
    395 				thisItem.shiftHorizontally( 1 );
    396 				break;
    397 			}
    398 			$this.trigger( 'focus' );
    399 			api.registerChange();
    400 			api.refreshKeyboardAccessibility();
    401 			api.refreshAdvancedAccessibility();
    402 		},
    403 
    404 		initAccessibility : function() {
    405 			var menu = $( '#menu-to-edit' );
    406 
    407 			api.refreshKeyboardAccessibility();
    408 			api.refreshAdvancedAccessibility();
    409 
    410 			// Refresh the accessibility when the user comes close to the item in any way.
    411 			menu.on( 'mouseenter.refreshAccessibility focus.refreshAccessibility touchstart.refreshAccessibility' , '.menu-item' , function(){
    412 				api.refreshAdvancedAccessibilityOfItem( $( this ).find( 'a.item-edit' ) );
    413 			} );
    414 
    415 			// We have to update on click as well because we might hover first, change the item, and then click.
    416 			menu.on( 'click', 'a.item-edit', function() {
    417 				api.refreshAdvancedAccessibilityOfItem( $( this ) );
    418 			} );
    419 
    420 			// Links for moving items.
    421 			menu.on( 'click', '.menus-move', function () {
    422 				var $this = $( this ),
    423 					dir = $this.data( 'dir' );
    424 
    425 				if ( 'undefined' !== typeof dir ) {
    426 					api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), dir );
    427 				}
    428 			});
    429 		},
    430 
    431 		/**
    432 		 * refreshAdvancedAccessibilityOfItem( [itemToRefresh] )
    433 		 *
    434 		 * Refreshes advanced accessibility buttons for one menu item.
    435 		 * Shows or hides buttons based on the location of the menu item.
    436 		 *
    437 		 * @param {Object} itemToRefresh The menu item that might need its advanced accessibility buttons refreshed
    438 		 */
    439 		refreshAdvancedAccessibilityOfItem : function( itemToRefresh ) {
    440 
    441 			// Only refresh accessibility when necessary.
    442 			if ( true !== $( itemToRefresh ).data( 'needs_accessibility_refresh' ) ) {
    443 				return;
    444 			}
    445 
    446 			var thisLink, thisLinkText, primaryItems, itemPosition, title,
    447 				parentItem, parentItemId, parentItemName, subItems,
    448 				$this = $( itemToRefresh ),
    449 				menuItem = $this.closest( 'li.menu-item' ).first(),
    450 				depth = menuItem.menuItemDepth(),
    451 				isPrimaryMenuItem = ( 0 === depth ),
    452 				itemName = $this.closest( '.menu-item-handle' ).find( '.menu-item-title' ).text(),
    453 				position = parseInt( menuItem.index(), 10 ),
    454 				prevItemDepth = ( isPrimaryMenuItem ) ? depth : parseInt( depth - 1, 10 ),
    455 				prevItemNameLeft = menuItem.prevAll('.menu-item-depth-' + prevItemDepth).first().find( '.menu-item-title' ).text(),
    456 				prevItemNameRight = menuItem.prevAll('.menu-item-depth-' + depth).first().find( '.menu-item-title' ).text(),
    457 				totalMenuItems = $('#menu-to-edit li').length,
    458 				hasSameDepthSibling = menuItem.nextAll( '.menu-item-depth-' + depth ).length;
    459 
    460 				menuItem.find( '.field-move' ).toggle( totalMenuItems > 1 );
    461 
    462 			// Where can they move this menu item?
    463 			if ( 0 !== position ) {
    464 				thisLink = menuItem.find( '.menus-move-up' );
    465 				thisLink.attr( 'aria-label', menus.moveUp ).css( 'display', 'inline' );
    466 			}
    467 
    468 			if ( 0 !== position && isPrimaryMenuItem ) {
    469 				thisLink = menuItem.find( '.menus-move-top' );
    470 				thisLink.attr( 'aria-label', menus.moveToTop ).css( 'display', 'inline' );
    471 			}
    472 
    473 			if ( position + 1 !== totalMenuItems && 0 !== position ) {
    474 				thisLink = menuItem.find( '.menus-move-down' );
    475 				thisLink.attr( 'aria-label', menus.moveDown ).css( 'display', 'inline' );
    476 			}
    477 
    478 			if ( 0 === position && 0 !== hasSameDepthSibling ) {
    479 				thisLink = menuItem.find( '.menus-move-down' );
    480 				thisLink.attr( 'aria-label', menus.moveDown ).css( 'display', 'inline' );
    481 			}
    482 
    483 			if ( ! isPrimaryMenuItem ) {
    484 				thisLink = menuItem.find( '.menus-move-left' ),
    485 				thisLinkText = menus.outFrom.replace( '%s', prevItemNameLeft );
    486 				thisLink.attr( 'aria-label', menus.moveOutFrom.replace( '%s', prevItemNameLeft ) ).text( thisLinkText ).css( 'display', 'inline' );
    487 			}
    488 
    489 			if ( 0 !== position ) {
    490 				if ( menuItem.find( '.menu-item-data-parent-id' ).val() !== menuItem.prev().find( '.menu-item-data-db-id' ).val() ) {
    491 					thisLink = menuItem.find( '.menus-move-right' ),
    492 					thisLinkText = menus.under.replace( '%s', prevItemNameRight );
    493 					thisLink.attr( 'aria-label', menus.moveUnder.replace( '%s', prevItemNameRight ) ).text( thisLinkText ).css( 'display', 'inline' );
    494 				}
    495 			}
    496 
    497 			if ( isPrimaryMenuItem ) {
    498 				primaryItems = $( '.menu-item-depth-0' ),
    499 				itemPosition = primaryItems.index( menuItem ) + 1,
    500 				totalMenuItems = primaryItems.length,
    501 
    502 				// String together help text for primary menu items.
    503 				title = menus.menuFocus.replace( '%1$s', itemName ).replace( '%2$d', itemPosition ).replace( '%3$d', totalMenuItems );
    504 			} else {
    505 				parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(),
    506 				parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(),
    507 				parentItemName = parentItem.find( '.menu-item-title' ).text(),
    508 				subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ),
    509 				itemPosition = $( subItems.parents('.menu-item').get().reverse() ).index( menuItem ) + 1;
    510 
    511 				// String together help text for sub menu items.
    512 				title = menus.subMenuFocus.replace( '%1$s', itemName ).replace( '%2$d', itemPosition ).replace( '%3$s', parentItemName );
    513 			}
    514 
    515 			$this.attr( 'aria-label', title );
    516 
    517 			// Mark this item's accessibility as refreshed.
    518 			$this.data( 'needs_accessibility_refresh', false );
    519 		},
    520 
    521 		/**
    522 		 * refreshAdvancedAccessibility
    523 		 *
    524 		 * Hides all advanced accessibility buttons and marks them for refreshing.
    525 		 */
    526 		refreshAdvancedAccessibility : function() {
    527 
    528 			// Hide all the move buttons by default.
    529 			$( '.menu-item-settings .field-move .menus-move' ).hide();
    530 
    531 			// Mark all menu items as unprocessed.
    532 			$( 'a.item-edit' ).data( 'needs_accessibility_refresh', true );
    533 
    534 			// All open items have to be refreshed or they will show no links.
    535 			$( '.menu-item-edit-active a.item-edit' ).each( function() {
    536 				api.refreshAdvancedAccessibilityOfItem( this );
    537 			} );
    538 		},
    539 
    540 		refreshKeyboardAccessibility : function() {
    541 			$( 'a.item-edit' ).off( 'focus' ).on( 'focus', function(){
    542 				$(this).off( 'keydown' ).on( 'keydown', function(e){
    543 
    544 					var arrows,
    545 						$this = $( this ),
    546 						thisItem = $this.parents( 'li.menu-item' ),
    547 						thisItemData = thisItem.getItemData();
    548 
    549 					// Bail if it's not an arrow key.
    550 					if ( 37 != e.which && 38 != e.which && 39 != e.which && 40 != e.which )
    551 						return;
    552 
    553 					// Avoid multiple keydown events.
    554 					$this.off('keydown');
    555 
    556 					// Bail if there is only one menu item.
    557 					if ( 1 === $('#menu-to-edit li').length )
    558 						return;
    559 
    560 					// If RTL, swap left/right arrows.
    561 					arrows = { '38': 'up', '40': 'down', '37': 'left', '39': 'right' };
    562 					if ( $('body').hasClass('rtl') )
    563 						arrows = { '38' : 'up', '40' : 'down', '39' : 'left', '37' : 'right' };
    564 
    565 					switch ( arrows[e.which] ) {
    566 					case 'up':
    567 						api.moveMenuItem( $this, 'up' );
    568 						break;
    569 					case 'down':
    570 						api.moveMenuItem( $this, 'down' );
    571 						break;
    572 					case 'left':
    573 						api.moveMenuItem( $this, 'left' );
    574 						break;
    575 					case 'right':
    576 						api.moveMenuItem( $this, 'right' );
    577 						break;
    578 					}
    579 					// Put focus back on same menu item.
    580 					$( '#edit-' + thisItemData['menu-item-db-id'] ).trigger( 'focus' );
    581 					return false;
    582 				});
    583 			});
    584 		},
    585 
    586 		initPreviewing : function() {
    587 			// Update the item handle title when the navigation label is changed.
    588 			$( '#menu-to-edit' ).on( 'change input', '.edit-menu-item-title', function(e) {
    589 				var input = $( e.currentTarget ), title, titleEl;
    590 				title = input.val();
    591 				titleEl = input.closest( '.menu-item' ).find( '.menu-item-title' );
    592 				// Don't update to empty title.
    593 				if ( title ) {
    594 					titleEl.text( title ).removeClass( 'no-title' );
    595 				} else {
    596 					titleEl.text( wp.i18n._x( '(no label)', 'missing menu item navigation label' ) ).addClass( 'no-title' );
    597 				}
    598 			} );
    599 		},
    600 
    601 		initToggles : function() {
    602 			// Init postboxes.
    603 			postboxes.add_postbox_toggles('nav-menus');
    604 
    605 			// Adjust columns functions for menus UI.
    606 			columns.useCheckboxesForHidden();
    607 			columns.checked = function(field) {
    608 				$('.field-' + field).removeClass('hidden-field');
    609 			};
    610 			columns.unchecked = function(field) {
    611 				$('.field-' + field).addClass('hidden-field');
    612 			};
    613 			// Hide fields.
    614 			api.menuList.hideAdvancedMenuItemFields();
    615 
    616 			$('.hide-postbox-tog').on( 'click', function () {
    617 				var hidden = $( '.accordion-container li.accordion-section' ).filter(':hidden').map(function() { return this.id; }).get().join(',');
    618 				$.post(ajaxurl, {
    619 					action: 'closed-postboxes',
    620 					hidden: hidden,
    621 					closedpostboxesnonce: jQuery('#closedpostboxesnonce').val(),
    622 					page: 'nav-menus'
    623 				});
    624 			});
    625 		},
    626 
    627 		initSortables : function() {
    628 			var currentDepth = 0, originalDepth, minDepth, maxDepth,
    629 				prev, next, prevBottom, nextThreshold, helperHeight, transport,
    630 				menuEdge = api.menuList.offset().left,
    631 				body = $('body'), maxChildDepth,
    632 				menuMaxDepth = initialMenuMaxDepth();
    633 
    634 			if( 0 !== $( '#menu-to-edit li' ).length )
    635 				$( '.drag-instructions' ).show();
    636 
    637 			// Use the right edge if RTL.
    638 			menuEdge += api.isRTL ? api.menuList.width() : 0;
    639 
    640 			api.menuList.sortable({
    641 				handle: '.menu-item-handle',
    642 				placeholder: 'sortable-placeholder',
    643 				items: api.options.sortableItems,
    644 				start: function(e, ui) {
    645 					var height, width, parent, children, tempHolder;
    646 
    647 					// Handle placement for RTL orientation.
    648 					if ( api.isRTL )
    649 						ui.item[0].style.right = 'auto';
    650 
    651 					transport = ui.item.children('.menu-item-transport');
    652 
    653 					// Set depths. currentDepth must be set before children are located.
    654 					originalDepth = ui.item.menuItemDepth();
    655 					updateCurrentDepth(ui, originalDepth);
    656 
    657 					// Attach child elements to parent.
    658 					// Skip the placeholder.
    659 					parent = ( ui.item.next()[0] == ui.placeholder[0] ) ? ui.item.next() : ui.item;
    660 					children = parent.childMenuItems();
    661 					transport.append( children );
    662 
    663 					// Update the height of the placeholder to match the moving item.
    664 					height = transport.outerHeight();
    665 					// If there are children, account for distance between top of children and parent.
    666 					height += ( height > 0 ) ? (ui.placeholder.css('margin-top').slice(0, -2) * 1) : 0;
    667 					height += ui.helper.outerHeight();
    668 					helperHeight = height;
    669 					height -= 2;                                              // Subtract 2 for borders.
    670 					ui.placeholder.height(height);
    671 
    672 					// Update the width of the placeholder to match the moving item.
    673 					maxChildDepth = originalDepth;
    674 					children.each(function(){
    675 						var depth = $(this).menuItemDepth();
    676 						maxChildDepth = (depth > maxChildDepth) ? depth : maxChildDepth;
    677 					});
    678 					width = ui.helper.find('.menu-item-handle').outerWidth(); // Get original width.
    679 					width += api.depthToPx(maxChildDepth - originalDepth);    // Account for children.
    680 					width -= 2;                                               // Subtract 2 for borders.
    681 					ui.placeholder.width(width);
    682 
    683 					// Update the list of menu items.
    684 					tempHolder = ui.placeholder.next( '.menu-item' );
    685 					tempHolder.css( 'margin-top', helperHeight + 'px' ); // Set the margin to absorb the placeholder.
    686 					ui.placeholder.detach();         // Detach or jQuery UI will think the placeholder is a menu item.
    687 					$(this).sortable( 'refresh' );   // The children aren't sortable. We should let jQuery UI know.
    688 					ui.item.after( ui.placeholder ); // Reattach the placeholder.
    689 					tempHolder.css('margin-top', 0); // Reset the margin.
    690 
    691 					// Now that the element is complete, we can update...
    692 					updateSharedVars(ui);
    693 				},
    694 				stop: function(e, ui) {
    695 					var children, subMenuTitle,
    696 						depthChange = currentDepth - originalDepth;
    697 
    698 					// Return child elements to the list.
    699 					children = transport.children().insertAfter(ui.item);
    700 
    701 					// Add "sub menu" description.
    702 					subMenuTitle = ui.item.find( '.item-title .is-submenu' );
    703 					if ( 0 < currentDepth )
    704 						subMenuTitle.show();
    705 					else
    706 						subMenuTitle.hide();
    707 
    708 					// Update depth classes.
    709 					if ( 0 !== depthChange ) {
    710 						ui.item.updateDepthClass( currentDepth );
    711 						children.shiftDepthClass( depthChange );
    712 						updateMenuMaxDepth( depthChange );
    713 					}
    714 					// Register a change.
    715 					api.registerChange();
    716 					// Update the item data.
    717 					ui.item.updateParentMenuItemDBId();
    718 
    719 					// Address sortable's incorrectly-calculated top in Opera.
    720 					ui.item[0].style.top = 0;
    721 
    722 					// Handle drop placement for rtl orientation.
    723 					if ( api.isRTL ) {
    724 						ui.item[0].style.left = 'auto';
    725 						ui.item[0].style.right = 0;
    726 					}
    727 
    728 					api.refreshKeyboardAccessibility();
    729 					api.refreshAdvancedAccessibility();
    730 				},
    731 				change: function(e, ui) {
    732 					// Make sure the placeholder is inside the menu.
    733 					// Otherwise fix it, or we're in trouble.
    734 					if( ! ui.placeholder.parent().hasClass('menu') )
    735 						(prev.length) ? prev.after( ui.placeholder ) : api.menuList.prepend( ui.placeholder );
    736 
    737 					updateSharedVars(ui);
    738 				},
    739 				sort: function(e, ui) {
    740 					var offset = ui.helper.offset(),
    741 						edge = api.isRTL ? offset.left + ui.helper.width() : offset.left,
    742 						depth = api.negateIfRTL * api.pxToDepth( edge - menuEdge );
    743 
    744 					/*
    745 					 * Check and correct if depth is not within range.
    746 					 * Also, if the dragged element is dragged upwards over an item,
    747 					 * shift the placeholder to a child position.
    748 					 */
    749 					if ( depth > maxDepth || offset.top < ( prevBottom - api.options.targetTolerance ) ) {
    750 						depth = maxDepth;
    751 					} else if ( depth < minDepth ) {
    752 						depth = minDepth;
    753 					}
    754 
    755 					if( depth != currentDepth )
    756 						updateCurrentDepth(ui, depth);
    757 
    758 					// If we overlap the next element, manually shift downwards.
    759 					if( nextThreshold && offset.top + helperHeight > nextThreshold ) {
    760 						next.after( ui.placeholder );
    761 						updateSharedVars( ui );
    762 						$( this ).sortable( 'refreshPositions' );
    763 					}
    764 				}
    765 			});
    766 
    767 			function updateSharedVars(ui) {
    768 				var depth;
    769 
    770 				prev = ui.placeholder.prev( '.menu-item' );
    771 				next = ui.placeholder.next( '.menu-item' );
    772 
    773 				// Make sure we don't select the moving item.
    774 				if( prev[0] == ui.item[0] ) prev = prev.prev( '.menu-item' );
    775 				if( next[0] == ui.item[0] ) next = next.next( '.menu-item' );
    776 
    777 				prevBottom = (prev.length) ? prev.offset().top + prev.height() : 0;
    778 				nextThreshold = (next.length) ? next.offset().top + next.height() / 3 : 0;
    779 				minDepth = (next.length) ? next.menuItemDepth() : 0;
    780 
    781 				if( prev.length )
    782 					maxDepth = ( (depth = prev.menuItemDepth() + 1) > api.options.globalMaxDepth ) ? api.options.globalMaxDepth : depth;
    783 				else
    784 					maxDepth = 0;
    785 			}
    786 
    787 			function updateCurrentDepth(ui, depth) {
    788 				ui.placeholder.updateDepthClass( depth, currentDepth );
    789 				currentDepth = depth;
    790 			}
    791 
    792 			function initialMenuMaxDepth() {
    793 				if( ! body[0].className ) return 0;
    794 				var match = body[0].className.match(/menu-max-depth-(\d+)/);
    795 				return match && match[1] ? parseInt( match[1], 10 ) : 0;
    796 			}
    797 
    798 			function updateMenuMaxDepth( depthChange ) {
    799 				var depth, newDepth = menuMaxDepth;
    800 				if ( depthChange === 0 ) {
    801 					return;
    802 				} else if ( depthChange > 0 ) {
    803 					depth = maxChildDepth + depthChange;
    804 					if( depth > menuMaxDepth )
    805 						newDepth = depth;
    806 				} else if ( depthChange < 0 && maxChildDepth == menuMaxDepth ) {
    807 					while( ! $('.menu-item-depth-' + newDepth, api.menuList).length && newDepth > 0 )
    808 						newDepth--;
    809 				}
    810 				// Update the depth class.
    811 				body.removeClass( 'menu-max-depth-' + menuMaxDepth ).addClass( 'menu-max-depth-' + newDepth );
    812 				menuMaxDepth = newDepth;
    813 			}
    814 		},
    815 
    816 		initManageLocations : function () {
    817 			$('#menu-locations-wrap form').on( 'submit', function(){
    818 				window.onbeforeunload = null;
    819 			});
    820 			$('.menu-location-menus select').on('change', function () {
    821 				var editLink = $(this).closest('tr').find('.locations-edit-menu-link');
    822 				if ($(this).find('option:selected').data('orig'))
    823 					editLink.show();
    824 				else
    825 					editLink.hide();
    826 			});
    827 		},
    828 
    829 		attachMenuEditListeners : function() {
    830 			var that = this;
    831 			$('#update-nav-menu').on('click', function(e) {
    832 				if ( e.target && e.target.className ) {
    833 					if ( -1 != e.target.className.indexOf('item-edit') ) {
    834 						return that.eventOnClickEditLink(e.target);
    835 					} else if ( -1 != e.target.className.indexOf('menu-save') ) {
    836 						return that.eventOnClickMenuSave(e.target);
    837 					} else if ( -1 != e.target.className.indexOf('menu-delete') ) {
    838 						return that.eventOnClickMenuDelete(e.target);
    839 					} else if ( -1 != e.target.className.indexOf('item-delete') ) {
    840 						return that.eventOnClickMenuItemDelete(e.target);
    841 					} else if ( -1 != e.target.className.indexOf('item-cancel') ) {
    842 						return that.eventOnClickCancelLink(e.target);
    843 					}
    844 				}
    845 			});
    846 
    847 			$( '#menu-name' ).on( 'input', _.debounce( function () {
    848 				var menuName = $( document.getElementById( 'menu-name' ) ),
    849 					menuNameVal = menuName.val();
    850 
    851 				if ( ! menuNameVal || ! menuNameVal.replace( /\s+/, '' ) ) {
    852 					// Add warning for invalid menu name.
    853 					menuName.parent().addClass( 'form-invalid' );
    854 				} else {
    855 					// Remove warning for valid menu name.
    856 					menuName.parent().removeClass( 'form-invalid' );
    857 				}
    858 			}, 500 ) );
    859 
    860 			$('#add-custom-links input[type="text"]').on( 'keypress', function(e){
    861 				$('#customlinkdiv').removeClass('form-invalid');
    862 
    863 				if ( e.keyCode === 13 ) {
    864 					e.preventDefault();
    865 					$( '#submit-customlinkdiv' ).trigger( 'click' );
    866 				}
    867 			});
    868 		},
    869 
    870 		/**
    871 		 * Handle toggling bulk selection checkboxes for menu items.
    872 		 *
    873 		 * @since 5.8.0
    874 		 */ 
    875 		attachBulkSelectButtonListeners : function() {
    876 			var that = this;
    877 
    878 			$( '.bulk-select-switcher' ).on( 'change', function() {
    879 				if ( this.checked ) {
    880 					$( '.bulk-select-switcher' ).prop( 'checked', true );
    881 					that.enableBulkSelection();
    882 				} else {
    883 					$( '.bulk-select-switcher' ).prop( 'checked', false );
    884 					that.disableBulkSelection();
    885 				}
    886 			});
    887 		},
    888 
    889 		/**
    890 		 * Enable bulk selection checkboxes for menu items.
    891 		 *
    892 		 * @since 5.8.0
    893 		 */ 
    894 		enableBulkSelection : function() {
    895 			var checkbox = $( '#menu-to-edit .menu-item-checkbox' );
    896 
    897 			$( '#menu-to-edit' ).addClass( 'bulk-selection' );
    898 			$( '#nav-menu-bulk-actions-top' ).addClass( 'bulk-selection' );
    899 			$( '#nav-menu-bulk-actions-bottom' ).addClass( 'bulk-selection' );
    900 
    901 			$.each( checkbox, function() {
    902 				$(this).prop( 'disabled', false );
    903 			});
    904 		},
    905 
    906 		/**
    907 		 * Disable bulk selection checkboxes for menu items.
    908 		 *
    909 		 * @since 5.8.0
    910 		 */ 
    911 		disableBulkSelection : function() {
    912 			var checkbox = $( '#menu-to-edit .menu-item-checkbox' );
    913 
    914 			$( '#menu-to-edit' ).removeClass( 'bulk-selection' );
    915 			$( '#nav-menu-bulk-actions-top' ).removeClass( 'bulk-selection' );
    916 			$( '#nav-menu-bulk-actions-bottom' ).removeClass( 'bulk-selection' );
    917 
    918 			if ( $( '.menu-items-delete' ).is( '[aria-describedby="pending-menu-items-to-delete"]' ) ) {
    919 				$( '.menu-items-delete' ).removeAttr( 'aria-describedby' );
    920 			}
    921 
    922 			$.each( checkbox, function() {
    923 				$(this).prop( 'disabled', true ).prop( 'checked', false );
    924 			});
    925 
    926 			$( '.menu-items-delete' ).addClass( 'disabled' );
    927 			$( '#pending-menu-items-to-delete ul' ).empty();
    928 		},
    929 
    930 		/**
    931 		 * Listen for state changes on bulk action checkboxes.
    932 		 *
    933 		 * @since 5.8.0
    934 		 */ 
    935 		attachMenuCheckBoxListeners : function() {
    936 			var that = this;
    937 
    938 			$( '#menu-to-edit' ).on( 'change', '.menu-item-checkbox', function() {
    939 				that.setRemoveSelectedButtonStatus();
    940 			});
    941 		},
    942 
    943 		/**
    944 		 * Create delete button to remove menu items from collection.
    945 		 *
    946 		 * @since 5.8.0
    947 		 */ 
    948 		attachMenuItemDeleteButton : function() {
    949 			var that = this;
    950 
    951 			$( document ).on( 'click', '.menu-items-delete', function( e ) {
    952 				var itemsPendingDeletion, itemsPendingDeletionList, deletionSpeech;
    953 
    954 				e.preventDefault();
    955 
    956 				if ( ! $(this).hasClass( 'disabled' ) ) {
    957 					$.each( $( '.menu-item-checkbox:checked' ), function( index, element ) {
    958 						$( element ).parents( 'li' ).find( 'a.item-delete' ).trigger( 'click' );
    959 					});
    960 
    961 					$( '.menu-items-delete' ).addClass( 'disabled' );
    962 					$( '.bulk-select-switcher' ).prop( 'checked', false );
    963 
    964 					itemsPendingDeletion     = '';
    965 					itemsPendingDeletionList = $( '#pending-menu-items-to-delete ul li' );
    966 
    967 					$.each( itemsPendingDeletionList, function( index, element ) {
    968 						var itemName = $( element ).find( '.pending-menu-item-name' ).text();
    969 						var itemSpeech = menus.menuItemDeletion.replace( '%s', itemName );
    970 
    971 						itemsPendingDeletion += itemSpeech;
    972 						if ( ( index + 1 ) < itemsPendingDeletionList.length ) {
    973 							itemsPendingDeletion += ', ';
    974 						}
    975 					});
    976 
    977 					deletionSpeech = menus.itemsDeleted.replace( '%s', itemsPendingDeletion );
    978 					wp.a11y.speak( deletionSpeech, 'polite' );
    979 					that.disableBulkSelection();
    980 				}
    981 			});
    982 		},
    983 
    984 		/**
    985 		 * List menu items awaiting deletion.
    986 		 *
    987 		 * @since 5.8.0
    988 		 */ 
    989 		attachPendingMenuItemsListForDeletion : function() {
    990 			$( '#post-body-content' ).on( 'change', '.menu-item-checkbox', function() {
    991 				var menuItemName, menuItemType, menuItemID, listedMenuItem;
    992 
    993 				if ( ! $( '.menu-items-delete' ).is( '[aria-describedby="pending-menu-items-to-delete"]' ) ) {
    994 					$( '.menu-items-delete' ).attr( 'aria-describedby', 'pending-menu-items-to-delete' );
    995 				}
    996 
    997 				menuItemName = $(this).next().text();
    998 				menuItemType = $(this).parent().next( '.item-controls' ).find( '.item-type' ).text();
    999 				menuItemID   = $(this).attr( 'data-menu-item-id' );
   1000 
   1001 				listedMenuItem = $( '#pending-menu-items-to-delete ul' ).find( '[data-menu-item-id=' + menuItemID + ']' );
   1002 				if ( listedMenuItem.length > 0 ) {
   1003 					listedMenuItem.remove();
   1004 				}
   1005 
   1006 				if ( this.checked === true ) {
   1007 					$( '#pending-menu-items-to-delete ul' ).append(
   1008 						'<li data-menu-item-id="' + menuItemID + '">' +
   1009 							'<span class="pending-menu-item-name">' + menuItemName + '</span> ' +
   1010 							'<span class="pending-menu-item-type">(' + menuItemType + ')</span>' +
   1011 							'<span class="separator"></span>' +
   1012 						'</li>'
   1013 					);
   1014 				}
   1015 
   1016 				$( '#pending-menu-items-to-delete li .separator' ).html( ', ' );
   1017 				$( '#pending-menu-items-to-delete li .separator' ).last().html( '.' );
   1018 			});
   1019 		},
   1020 
   1021 		/**
   1022 		 * Set status of bulk delete checkbox.
   1023 		 *
   1024 		 * @since 5.8.0
   1025 		 */ 
   1026 		setBulkDeleteCheckboxStatus : function() {
   1027 			var that = this;
   1028 			var checkbox = $( '#menu-to-edit .menu-item-checkbox' );
   1029 
   1030 			$.each( checkbox, function() {
   1031 				if ( $(this).prop( 'disabled' ) ) {
   1032 					$(this).prop( 'disabled', false );
   1033 				} else {
   1034 					$(this).prop( 'disabled', true );
   1035 				}
   1036 
   1037 				if ( $(this).is( ':checked' ) ) {
   1038 					$(this).prop( 'checked', false );
   1039 				}
   1040 			});
   1041 
   1042 			that.setRemoveSelectedButtonStatus();
   1043 		},
   1044 
   1045 		/**
   1046 		 * Set status of menu items removal button.
   1047 		 *
   1048 		 * @since 5.8.0
   1049 		 */ 
   1050 		setRemoveSelectedButtonStatus : function() {
   1051 			var button = $( '.menu-items-delete' );
   1052 
   1053 			if ( $( '.menu-item-checkbox:checked' ).length > 0 ) {
   1054 				button.removeClass( 'disabled' );
   1055 			} else {
   1056 				button.addClass( 'disabled' );
   1057 			}
   1058 		},
   1059 
   1060 		attachMenuSaveSubmitListeners : function() {
   1061 			/*
   1062 			 * When a navigation menu is saved, store a JSON representation of all form data
   1063 			 * in a single input to avoid PHP `max_input_vars` limitations. See #14134.
   1064 			 */
   1065 			$( '#update-nav-menu' ).on( 'submit', function() {
   1066 				var navMenuData = $( '#update-nav-menu' ).serializeArray();
   1067 				$( '[name="nav-menu-data"]' ).val( JSON.stringify( navMenuData ) );
   1068 			});
   1069 		},
   1070 
   1071 		attachThemeLocationsListeners : function() {
   1072 			var loc = $('#nav-menu-theme-locations'), params = {};
   1073 			params.action = 'menu-locations-save';
   1074 			params['menu-settings-column-nonce'] = $('#menu-settings-column-nonce').val();
   1075 			loc.find('input[type="submit"]').on( 'click', function() {
   1076 				loc.find('select').each(function() {
   1077 					params[this.name] = $(this).val();
   1078 				});
   1079 				loc.find( '.spinner' ).addClass( 'is-active' );
   1080 				$.post( ajaxurl, params, function() {
   1081 					loc.find( '.spinner' ).removeClass( 'is-active' );
   1082 				});
   1083 				return false;
   1084 			});
   1085 		},
   1086 
   1087 		attachQuickSearchListeners : function() {
   1088 			var searchTimer;
   1089 
   1090 			// Prevent form submission.
   1091 			$( '#nav-menu-meta' ).on( 'submit', function( event ) {
   1092 				event.preventDefault();
   1093 			});
   1094 
   1095 			$( '#nav-menu-meta' ).on( 'input', '.quick-search', function() {
   1096 				var $this = $( this );
   1097 
   1098 				$this.attr( 'autocomplete', 'off' );
   1099 
   1100 				if ( searchTimer ) {
   1101 					clearTimeout( searchTimer );
   1102 				}
   1103 
   1104 				searchTimer = setTimeout( function() {
   1105 					api.updateQuickSearchResults( $this );
   1106 				}, 500 );
   1107 			}).on( 'blur', '.quick-search', function() {
   1108 				api.lastSearch = '';
   1109 			});
   1110 		},
   1111 
   1112 		updateQuickSearchResults : function(input) {
   1113 			var panel, params,
   1114 				minSearchLength = 2,
   1115 				q = input.val();
   1116 
   1117 			/*
   1118 			 * Minimum characters for a search. Also avoid a new Ajax search when
   1119 			 * the pressed key (e.g. arrows) doesn't change the searched term.
   1120 			 */
   1121 			if ( q.length < minSearchLength || api.lastSearch == q ) {
   1122 				return;
   1123 			}
   1124 
   1125 			api.lastSearch = q;
   1126 
   1127 			panel = input.parents('.tabs-panel');
   1128 			params = {
   1129 				'action': 'menu-quick-search',
   1130 				'response-format': 'markup',
   1131 				'menu': $('#menu').val(),
   1132 				'menu-settings-column-nonce': $('#menu-settings-column-nonce').val(),
   1133 				'q': q,
   1134 				'type': input.attr('name')
   1135 			};
   1136 
   1137 			$( '.spinner', panel ).addClass( 'is-active' );
   1138 
   1139 			$.post( ajaxurl, params, function(menuMarkup) {
   1140 				api.processQuickSearchQueryResponse(menuMarkup, params, panel);
   1141 			});
   1142 		},
   1143 
   1144 		addCustomLink : function( processMethod ) {
   1145 			var url = $('#custom-menu-item-url').val().toString(),
   1146 				label = $('#custom-menu-item-name').val();
   1147 
   1148 			if ( '' !== url ) {
   1149 				url = url.trim();
   1150 			}
   1151 
   1152 			processMethod = processMethod || api.addMenuItemToBottom;
   1153 
   1154 			if ( '' === url || 'https://' == url || 'http://' == url ) {
   1155 				$('#customlinkdiv').addClass('form-invalid');
   1156 				return false;
   1157 			}
   1158 
   1159 			// Show the Ajax spinner.
   1160 			$( '.customlinkdiv .spinner' ).addClass( 'is-active' );
   1161 			this.addLinkToMenu( url, label, processMethod, function() {
   1162 				// Remove the Ajax spinner.
   1163 				$( '.customlinkdiv .spinner' ).removeClass( 'is-active' );
   1164 				// Set custom link form back to defaults.
   1165 				$('#custom-menu-item-name').val('').trigger( 'blur' );
   1166 				$( '#custom-menu-item-url' ).val( '' ).attr( 'placeholder', 'https://' );
   1167 			});
   1168 		},
   1169 
   1170 		addLinkToMenu : function(url, label, processMethod, callback) {
   1171 			processMethod = processMethod || api.addMenuItemToBottom;
   1172 			callback = callback || function(){};
   1173 
   1174 			api.addItemToMenu({
   1175 				'-1': {
   1176 					'menu-item-type': 'custom',
   1177 					'menu-item-url': url,
   1178 					'menu-item-title': label
   1179 				}
   1180 			}, processMethod, callback);
   1181 		},
   1182 
   1183 		addItemToMenu : function(menuItem, processMethod, callback) {
   1184 			var menu = $('#menu').val(),
   1185 				nonce = $('#menu-settings-column-nonce').val(),
   1186 				params;
   1187 
   1188 			processMethod = processMethod || function(){};
   1189 			callback = callback || function(){};
   1190 
   1191 			params = {
   1192 				'action': 'add-menu-item',
   1193 				'menu': menu,
   1194 				'menu-settings-column-nonce': nonce,
   1195 				'menu-item': menuItem
   1196 			};
   1197 
   1198 			$.post( ajaxurl, params, function(menuMarkup) {
   1199 				var ins = $('#menu-instructions');
   1200 
   1201 				menuMarkup = menuMarkup || '';
   1202 				menuMarkup = menuMarkup.toString().trim(); // Trim leading whitespaces.
   1203 				processMethod(menuMarkup, params);
   1204 
   1205 				// Make it stand out a bit more visually, by adding a fadeIn.
   1206 				$( 'li.pending' ).hide().fadeIn('slow');
   1207 				$( '.drag-instructions' ).show();
   1208 				if( ! ins.hasClass( 'menu-instructions-inactive' ) && ins.siblings().length )
   1209 					ins.addClass( 'menu-instructions-inactive' );
   1210 
   1211 				callback();
   1212 			});
   1213 		},
   1214 
   1215 		/**
   1216 		 * Process the add menu item request response into menu list item. Appends to menu.
   1217 		 *
   1218 		 * @param {string} menuMarkup The text server response of menu item markup.
   1219 		 *
   1220 		 * @fires document#menu-item-added Passes menuMarkup as a jQuery object.
   1221 		 */
   1222 		addMenuItemToBottom : function( menuMarkup ) {
   1223 			var $menuMarkup = $( menuMarkup );
   1224 			$menuMarkup.hideAdvancedMenuItemFields().appendTo( api.targetList );
   1225 			api.refreshKeyboardAccessibility();
   1226 			api.refreshAdvancedAccessibility();
   1227 			$( document ).trigger( 'menu-item-added', [ $menuMarkup ] );
   1228 		},
   1229 
   1230 		/**
   1231 		 * Process the add menu item request response into menu list item. Prepends to menu.
   1232 		 *
   1233 		 * @param {string} menuMarkup The text server response of menu item markup.
   1234 		 *
   1235 		 * @fires document#menu-item-added Passes menuMarkup as a jQuery object.
   1236 		 */
   1237 		addMenuItemToTop : function( menuMarkup ) {
   1238 			var $menuMarkup = $( menuMarkup );
   1239 			$menuMarkup.hideAdvancedMenuItemFields().prependTo( api.targetList );
   1240 			api.refreshKeyboardAccessibility();
   1241 			api.refreshAdvancedAccessibility();
   1242 			$( document ).trigger( 'menu-item-added', [ $menuMarkup ] );
   1243 		},
   1244 
   1245 		attachUnsavedChangesListener : function() {
   1246 			$('#menu-management input, #menu-management select, #menu-management, #menu-management textarea, .menu-location-menus select').on( 'change', function(){
   1247 				api.registerChange();
   1248 			});
   1249 
   1250 			if ( 0 !== $('#menu-to-edit').length || 0 !== $('.menu-location-menus select').length ) {
   1251 				window.onbeforeunload = function(){
   1252 					if ( api.menusChanged )
   1253 						return wp.i18n.__( 'The changes you made will be lost if you navigate away from this page.' );
   1254 				};
   1255 			} else {
   1256 				// Make the post boxes read-only, as they can't be used yet.
   1257 				$( '#menu-settings-column' ).find( 'input,select' ).end().find( 'a' ).attr( 'href', '#' ).off( 'click' );
   1258 			}
   1259 		},
   1260 
   1261 		registerChange : function() {
   1262 			api.menusChanged = true;
   1263 		},
   1264 
   1265 		attachTabsPanelListeners : function() {
   1266 			$('#menu-settings-column').on('click', function(e) {
   1267 				var selectAreaMatch, selectAll, panelId, wrapper, items,
   1268 					target = $(e.target);
   1269 
   1270 				if ( target.hasClass('nav-tab-link') ) {
   1271 
   1272 					panelId = target.data( 'type' );
   1273 
   1274 					wrapper = target.parents('.accordion-section-content').first();
   1275 
   1276 					// Upon changing tabs, we want to uncheck all checkboxes.
   1277 					$( 'input', wrapper ).prop( 'checked', false );
   1278 
   1279 					$('.tabs-panel-active', wrapper).removeClass('tabs-panel-active').addClass('tabs-panel-inactive');
   1280 					$('#' + panelId, wrapper).removeClass('tabs-panel-inactive').addClass('tabs-panel-active');
   1281 
   1282 					$('.tabs', wrapper).removeClass('tabs');
   1283 					target.parent().addClass('tabs');
   1284 
   1285 					// Select the search bar.
   1286 					$('.quick-search', wrapper).trigger( 'focus' );
   1287 
   1288 					// Hide controls in the search tab if no items found.
   1289 					if ( ! wrapper.find( '.tabs-panel-active .menu-item-title' ).length ) {
   1290 						wrapper.addClass( 'has-no-menu-item' );
   1291 					} else {
   1292 						wrapper.removeClass( 'has-no-menu-item' );
   1293 					}
   1294 
   1295 					e.preventDefault();
   1296 				} else if ( target.hasClass( 'select-all' ) ) {
   1297 					selectAreaMatch = target.closest( '.button-controls' ).data( 'items-type' );
   1298 					if ( selectAreaMatch ) {
   1299 						items = $( '#' + selectAreaMatch + ' .tabs-panel-active .menu-item-title input' );
   1300 
   1301 						if ( items.length === items.filter( ':checked' ).length && ! target.is( ':checked' ) ) {
   1302 							items.prop( 'checked', false );
   1303 						} else if ( target.is( ':checked' ) ) {
   1304 							items.prop( 'checked', true );
   1305 						}
   1306 					}
   1307 				} else if ( target.hasClass( 'menu-item-checkbox' ) ) {
   1308 					selectAreaMatch = target.closest( '.tabs-panel-active' ).parent().attr( 'id' );
   1309 					if ( selectAreaMatch ) {
   1310 						items     = $( '#' + selectAreaMatch + ' .tabs-panel-active .menu-item-title input' );
   1311 						selectAll = $( '.button-controls[data-items-type="' + selectAreaMatch + '"] .select-all' );
   1312 
   1313 						if ( items.length === items.filter( ':checked' ).length && ! selectAll.is( ':checked' ) ) {
   1314 							selectAll.prop( 'checked', true );
   1315 						} else if ( selectAll.is( ':checked' ) ) {
   1316 							selectAll.prop( 'checked', false );
   1317 						}
   1318 					}
   1319 				} else if ( target.hasClass('submit-add-to-menu') ) {
   1320 					api.registerChange();
   1321 
   1322 					if ( e.target.id && 'submit-customlinkdiv' == e.target.id )
   1323 						api.addCustomLink( api.addMenuItemToBottom );
   1324 					else if ( e.target.id && -1 != e.target.id.indexOf('submit-') )
   1325 						$('#' + e.target.id.replace(/submit-/, '')).addSelectedToMenu( api.addMenuItemToBottom );
   1326 					return false;
   1327 				}
   1328 			});
   1329 
   1330 			/*
   1331 			 * Delegate the `click` event and attach it just to the pagination
   1332 			 * links thus excluding the current page `<span>`. See ticket #35577.
   1333 			 */
   1334 			$( '#nav-menu-meta' ).on( 'click', 'a.page-numbers', function() {
   1335 				var $container = $( this ).closest( '.inside' );
   1336 
   1337 				$.post( ajaxurl, this.href.replace( /.*\?/, '' ).replace( /action=([^&]*)/, '' ) + '&action=menu-get-metabox',
   1338 					function( resp ) {
   1339 						var metaBoxData = JSON.parse( resp ),
   1340 							toReplace;
   1341 
   1342 						if ( -1 === resp.indexOf( 'replace-id' ) ) {
   1343 							return;
   1344 						}
   1345 
   1346 						// Get the post type menu meta box to update.
   1347 						toReplace = document.getElementById( metaBoxData['replace-id'] );
   1348 
   1349 						if ( ! metaBoxData.markup || ! toReplace ) {
   1350 							return;
   1351 						}
   1352 
   1353 						// Update the post type menu meta box with new content from the response.
   1354 						$container.html( metaBoxData.markup );
   1355 					}
   1356 				);
   1357 
   1358 				return false;
   1359 			});
   1360 		},
   1361 
   1362 		eventOnClickEditLink : function(clickedEl) {
   1363 			var settings, item,
   1364 			matchedSection = /#(.*)$/.exec(clickedEl.href);
   1365 
   1366 			if ( matchedSection && matchedSection[1] ) {
   1367 				settings = $('#'+matchedSection[1]);
   1368 				item = settings.parent();
   1369 				if( 0 !== item.length ) {
   1370 					if( item.hasClass('menu-item-edit-inactive') ) {
   1371 						if( ! settings.data('menu-item-data') ) {
   1372 							settings.data( 'menu-item-data', settings.getItemData() );
   1373 						}
   1374 						settings.slideDown('fast');
   1375 						item.removeClass('menu-item-edit-inactive')
   1376 							.addClass('menu-item-edit-active');
   1377 					} else {
   1378 						settings.slideUp('fast');
   1379 						item.removeClass('menu-item-edit-active')
   1380 							.addClass('menu-item-edit-inactive');
   1381 					}
   1382 					return false;
   1383 				}
   1384 			}
   1385 		},
   1386 
   1387 		eventOnClickCancelLink : function(clickedEl) {
   1388 			var settings = $( clickedEl ).closest( '.menu-item-settings' ),
   1389 				thisMenuItem = $( clickedEl ).closest( '.menu-item' );
   1390 
   1391 			thisMenuItem.removeClass( 'menu-item-edit-active' ).addClass( 'menu-item-edit-inactive' );
   1392 			settings.setItemData( settings.data( 'menu-item-data' ) ).hide();
   1393 			// Restore the title of the currently active/expanded menu item.
   1394 			thisMenuItem.find( '.menu-item-title' ).text( settings.data( 'menu-item-data' )['menu-item-title'] );
   1395 
   1396 			return false;
   1397 		},
   1398 
   1399 		eventOnClickMenuSave : function() {
   1400 			var locs = '',
   1401 			menuName = $('#menu-name'),
   1402 			menuNameVal = menuName.val();
   1403 
   1404 			// Cancel and warn if invalid menu name.
   1405 			if ( ! menuNameVal || ! menuNameVal.replace( /\s+/, '' ) ) {
   1406 				menuName.parent().addClass( 'form-invalid' );
   1407 				return false;
   1408 			}
   1409 			// Copy menu theme locations.
   1410 			$('#nav-menu-theme-locations select').each(function() {
   1411 				locs += '<input type="hidden" name="' + this.name + '" value="' + $(this).val() + '" />';
   1412 			});
   1413 			$('#update-nav-menu').append( locs );
   1414 			// Update menu item position data.
   1415 			api.menuList.find('.menu-item-data-position').val( function(index) { return index + 1; } );
   1416 			window.onbeforeunload = null;
   1417 
   1418 			return true;
   1419 		},
   1420 
   1421 		eventOnClickMenuDelete : function() {
   1422 			// Delete warning AYS.
   1423 			if ( window.confirm( wp.i18n.__( 'You are about to permanently delete this menu.\n\'Cancel\' to stop, \'OK\' to delete.' ) ) ) {
   1424 				window.onbeforeunload = null;
   1425 				return true;
   1426 			}
   1427 			return false;
   1428 		},
   1429 
   1430 		eventOnClickMenuItemDelete : function(clickedEl) {
   1431 			var itemID = parseInt(clickedEl.id.replace('delete-', ''), 10);
   1432 
   1433 			api.removeMenuItem( $('#menu-item-' + itemID) );
   1434 			api.registerChange();
   1435 			return false;
   1436 		},
   1437 
   1438 		/**
   1439 		 * Process the quick search response into a search result
   1440 		 *
   1441 		 * @param string resp The server response to the query.
   1442 		 * @param object req The request arguments.
   1443 		 * @param jQuery panel The tabs panel we're searching in.
   1444 		 */
   1445 		processQuickSearchQueryResponse : function(resp, req, panel) {
   1446 			var matched, newID,
   1447 			takenIDs = {},
   1448 			form = document.getElementById('nav-menu-meta'),
   1449 			pattern = /menu-item[(\[^]\]*/,
   1450 			$items = $('<div>').html(resp).find('li'),
   1451 			wrapper = panel.closest( '.accordion-section-content' ),
   1452 			selectAll = wrapper.find( '.button-controls .select-all' ),
   1453 			$item;
   1454 
   1455 			if( ! $items.length ) {
   1456 				$('.categorychecklist', panel).html( '<li><p>' + wp.i18n.__( 'No results found.' ) + '</p></li>' );
   1457 				$( '.spinner', panel ).removeClass( 'is-active' );
   1458 				wrapper.addClass( 'has-no-menu-item' );
   1459 				return;
   1460 			}
   1461 
   1462 			$items.each(function(){
   1463 				$item = $(this);
   1464 
   1465 				// Make a unique DB ID number.
   1466 				matched = pattern.exec($item.html());
   1467 
   1468 				if ( matched && matched[1] ) {
   1469 					newID = matched[1];
   1470 					while( form.elements['menu-item[' + newID + '][menu-item-type]'] || takenIDs[ newID ] ) {
   1471 						newID--;
   1472 					}
   1473 
   1474 					takenIDs[newID] = true;
   1475 					if ( newID != matched[1] ) {
   1476 						$item.html( $item.html().replace(new RegExp(
   1477 							'menu-item\\[' + matched[1] + '\\]', 'g'),
   1478 							'menu-item[' + newID + ']'
   1479 						) );
   1480 					}
   1481 				}
   1482 			});
   1483 
   1484 			$('.categorychecklist', panel).html( $items );
   1485 			$( '.spinner', panel ).removeClass( 'is-active' );
   1486 			wrapper.removeClass( 'has-no-menu-item' );
   1487 
   1488 			if ( selectAll.is( ':checked' ) ) {
   1489 				selectAll.prop( 'checked', false );
   1490 			}
   1491 		},
   1492 
   1493 		/**
   1494 		 * Remove a menu item.
   1495 		 *
   1496 		 * @param {Object} el The element to be removed as a jQuery object.
   1497 		 *
   1498 		 * @fires document#menu-removing-item Passes the element to be removed.
   1499 		 */
   1500 		removeMenuItem : function(el) {
   1501 			var children = el.childMenuItems();
   1502 
   1503 			$( document ).trigger( 'menu-removing-item', [ el ] );
   1504 			el.addClass('deleting').animate({
   1505 					opacity : 0,
   1506 					height: 0
   1507 				}, 350, function() {
   1508 					var ins = $('#menu-instructions');
   1509 					el.remove();
   1510 					children.shiftDepthClass( -1 ).updateParentMenuItemDBId();
   1511 					if ( 0 === $( '#menu-to-edit li' ).length ) {
   1512 						$( '.drag-instructions' ).hide();
   1513 						ins.removeClass( 'menu-instructions-inactive' );
   1514 					}
   1515 					api.refreshAdvancedAccessibility();
   1516 				});
   1517 		},
   1518 
   1519 		depthToPx : function(depth) {
   1520 			return depth * api.options.menuItemDepthPerLevel;
   1521 		},
   1522 
   1523 		pxToDepth : function(px) {
   1524 			return Math.floor(px / api.options.menuItemDepthPerLevel);
   1525 		}
   1526 
   1527 	};
   1528 
   1529 	$( function() {
   1530 
   1531 		wpNavMenu.init();
   1532 
   1533 		// Prevent focused element from being hidden by the sticky footer.
   1534 		$( '.menu-edit a, .menu-edit button, .menu-edit input, .menu-edit textarea, .menu-edit select' ).on('focus', function() {
   1535 			if ( window.innerWidth >= 783 ) {
   1536 				var navMenuHeight = $( '#nav-menu-footer' ).height() + 20;
   1537 				var bottomOffset = $(this).offset().top - ( $(window).scrollTop() + $(window).height() - $(this).height() );
   1538 
   1539 				if ( bottomOffset > 0 ) {
   1540 					bottomOffset = 0;
   1541 				}
   1542 				bottomOffset = bottomOffset * -1;
   1543 
   1544 				if( bottomOffset < navMenuHeight ) {
   1545 					var scrollTop = $(document).scrollTop();
   1546 					$(document).scrollTop( scrollTop + ( navMenuHeight - bottomOffset ) );
   1547 				}
   1548 			}
   1549 		});
   1550 	});
   1551 
   1552 })(jQuery);