2 * WordPress Administration Navigation Menu
3 * Interface JS functions
8 * @subpackage Administration
11 /* global menus, postboxes, columns, isRtl, navMenuL10n, ajaxurl */
22 menuItemDepthPerLevel : 30, // Do not use directly. Use depthToPx and pxToDepth instead.
26 menuList : undefined, // Set in init.
27 targetList : undefined, // Set in init.
29 isRTL: !! ( 'undefined' != typeof isRtl && isRtl ),
30 negateIfRTL: ( 'undefined' != typeof isRtl && isRtl ) ? -1 : 1,
32 // Functions that run on init.
34 api.menuList = $('#menu-to-edit');
35 api.targetList = api.menuList;
37 this.jQueryExtensions();
39 this.attachMenuEditListeners();
41 this.setupInputWithDefaultTitle();
42 this.attachQuickSearchListeners();
43 this.attachThemeLocationsListeners();
45 this.attachTabsPanelListeners();
47 this.attachUnsavedChangesListener();
49 if ( api.menuList.length )
52 if ( menus.oneThemeLocationNoMenus )
53 $( '#posttype-page' ).addSelectedToMenu( api.addMenuItemToBottom );
55 this.initManageLocations();
57 this.initAccessibility();
62 jQueryExtensions : function() {
65 menuItemDepth : function() {
66 var margin = api.isRTL ? this.eq(0).css('margin-right') : this.eq(0).css('margin-left');
67 return api.pxToDepth( margin && -1 != margin.indexOf('px') ? margin.slice(0, -2) : 0 );
69 updateDepthClass : function(current, prev) {
70 return this.each(function(){
72 prev = prev || t.menuItemDepth();
73 $(this).removeClass('menu-item-depth-'+ prev )
74 .addClass('menu-item-depth-'+ current );
77 shiftDepthClass : function(change) {
78 return this.each(function(){
80 depth = t.menuItemDepth();
81 $(this).removeClass('menu-item-depth-'+ depth )
82 .addClass('menu-item-depth-'+ (depth + change) );
85 childMenuItems : function() {
88 var t = $(this), depth = t.menuItemDepth(), next = t.next();
89 while( next.length && next.menuItemDepth() > depth ) {
90 result = result.add( next );
96 shiftHorizontally : function( dir ) {
97 return this.each(function(){
99 depth = t.menuItemDepth(),
100 newDepth = depth + dir;
102 // Change .menu-item-depth-n class
103 t.moveHorizontally( newDepth, depth );
106 moveHorizontally : function( newDepth, depth ) {
107 return this.each(function(){
109 children = t.childMenuItems(),
110 diff = newDepth - depth,
111 subItemText = t.find('.is-submenu');
113 // Change .menu-item-depth-n class
114 t.updateDepthClass( newDepth, depth ).updateParentMenuItemDBId();
116 // If it has children, move those too
118 children.each(function() {
120 thisDepth = t.menuItemDepth(),
121 newDepth = thisDepth + diff;
122 t.updateDepthClass(newDepth, thisDepth).updateParentMenuItemDBId();
126 // Show "Sub item" helper text
133 updateParentMenuItemDBId : function() {
134 return this.each(function(){
136 input = item.find( '.menu-item-data-parent-id' ),
137 depth = parseInt( item.menuItemDepth(), 10 ),
138 parentDepth = depth - 1,
139 parent = item.prevAll( '.menu-item-depth-' + parentDepth ).first();
141 if ( 0 === depth ) { // Item is on the top level, has no parent
143 } else { // Find the parent item, and retrieve its object id.
144 input.val( parent.find( '.menu-item-data-db-id' ).val() );
148 hideAdvancedMenuItemFields : function() {
149 return this.each(function(){
151 $('.hide-column-tog').not(':checked').each(function(){
152 that.find('.field-' + $(this).val() ).addClass('hidden-field');
157 * Adds selected menu items to the menu.
159 * @param jQuery metabox The metabox jQuery object.
161 addSelectedToMenu : function(processMethod) {
162 if ( 0 === $('#menu-to-edit').length ) {
166 return this.each(function() {
167 var t = $(this), menuItems = {},
168 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' ),
169 re = /menu-item\[([^\]]*)/;
171 processMethod = processMethod || api.addMenuItemToBottom;
173 // If no items are checked, bail.
174 if ( !checkboxes.length )
177 // Show the ajax spinner
178 t.find('.spinner').show();
180 // Retrieve menu item data
181 $(checkboxes).each(function(){
183 listItemDBIDMatch = re.exec( t.attr('name') ),
184 listItemDBID = 'undefined' == typeof listItemDBIDMatch[1] ? 0 : parseInt(listItemDBIDMatch[1], 10);
186 if ( this.className && -1 != this.className.indexOf('add-to-top') )
187 processMethod = api.addMenuItemToTop;
188 menuItems[listItemDBID] = t.closest('li').getItemData( 'add-menu-item', listItemDBID );
192 api.addItemToMenu(menuItems, processMethod, function(){
193 // Deselect the items and hide the ajax spinner
194 checkboxes.removeAttr('checked');
195 t.find('.spinner').hide();
199 getItemData : function( itemType, id ) {
200 itemType = itemType || 'menu-item';
202 var itemData = {}, i,
205 'menu-item-object-id',
207 'menu-item-parent-id',
208 'menu-item-position',
212 'menu-item-description',
213 'menu-item-attr-title',
219 if( !id && itemType == 'menu-item' ) {
220 id = this.find('.menu-item-data-db-id').val();
223 if( !id ) return itemData;
225 this.find('input').each(function() {
229 if( itemType == 'menu-item' )
230 field = fields[i] + '[' + id + ']';
231 else if( itemType == 'add-menu-item' )
232 field = 'menu-item[' + id + '][' + fields[i] + ']';
238 itemData[fields[i]] = this.value;
245 setItemData : function( itemData, itemType, id ) { // Can take a type, such as 'menu-item', or an id.
246 itemType = itemType || 'menu-item';
248 if( !id && itemType == 'menu-item' ) {
249 id = $('.menu-item-data-db-id', this).val();
252 if( !id ) return this;
254 this.find('input').each(function() {
255 var t = $(this), field;
256 $.each( itemData, function( attr, val ) {
257 if( itemType == 'menu-item' )
258 field = attr + '[' + id + ']';
259 else if( itemType == 'add-menu-item' )
260 field = 'menu-item[' + id + '][' + attr + ']';
262 if ( field == t.attr('name') ) {
272 countMenuItems : function( depth ) {
273 return $( '.menu-item-depth-' + depth ).length;
276 moveMenuItem : function( $this, dir ) {
278 var items, newItemPosition, newDepth,
279 menuItems = $( '#menu-to-edit li' ),
280 menuItemsCount = menuItems.length,
281 thisItem = $this.parents( 'li.menu-item' ),
282 thisItemChildren = thisItem.childMenuItems(),
283 thisItemData = thisItem.getItemData(),
284 thisItemDepth = parseInt( thisItem.menuItemDepth(), 10 ),
285 thisItemPosition = parseInt( thisItem.index(), 10 ),
286 nextItem = thisItem.next(),
287 nextItemChildren = nextItem.childMenuItems(),
288 nextItemDepth = parseInt( nextItem.menuItemDepth(), 10 ) + 1,
289 prevItem = thisItem.prev(),
290 prevItemDepth = parseInt( prevItem.menuItemDepth(), 10 ),
291 prevItemId = prevItem.getItemData()['menu-item-db-id'];
295 newItemPosition = thisItemPosition - 1;
298 if ( 0 === thisItemPosition )
301 // If a sub item is moved to top, shift it to 0 depth
302 if ( 0 === newItemPosition && 0 !== thisItemDepth )
303 thisItem.moveHorizontally( 0, thisItemDepth );
305 // If prev item is sub item, shift to match depth
306 if ( 0 !== prevItemDepth )
307 thisItem.moveHorizontally( prevItemDepth, thisItemDepth );
309 // Does this item have sub items?
310 if ( thisItemChildren ) {
311 items = thisItem.add( thisItemChildren );
312 // Move the entire block
313 items.detach().insertBefore( menuItems.eq( newItemPosition ) ).updateParentMenuItemDBId();
315 thisItem.detach().insertBefore( menuItems.eq( newItemPosition ) ).updateParentMenuItemDBId();
319 // Does this item have sub items?
320 if ( thisItemChildren ) {
321 items = thisItem.add( thisItemChildren ),
322 nextItem = menuItems.eq( items.length + thisItemPosition ),
323 nextItemChildren = 0 !== nextItem.childMenuItems().length;
325 if ( nextItemChildren ) {
326 newDepth = parseInt( nextItem.menuItemDepth(), 10 ) + 1;
327 thisItem.moveHorizontally( newDepth, thisItemDepth );
330 // Have we reached the bottom?
331 if ( menuItemsCount === thisItemPosition + items.length )
334 items.detach().insertAfter( menuItems.eq( thisItemPosition + items.length ) ).updateParentMenuItemDBId();
336 // If next item has sub items, shift depth
337 if ( 0 !== nextItemChildren.length )
338 thisItem.moveHorizontally( nextItemDepth, thisItemDepth );
340 // Have we reached the bottom
341 if ( menuItemsCount === thisItemPosition + 1 )
343 thisItem.detach().insertAfter( menuItems.eq( thisItemPosition + 1 ) ).updateParentMenuItemDBId();
348 if ( 0 === thisItemPosition )
350 // Does this item have sub items?
351 if ( thisItemChildren ) {
352 items = thisItem.add( thisItemChildren );
353 // Move the entire block
354 items.detach().insertBefore( menuItems.eq( 0 ) ).updateParentMenuItemDBId();
356 thisItem.detach().insertBefore( menuItems.eq( 0 ) ).updateParentMenuItemDBId();
360 // As far left as possible
361 if ( 0 === thisItemDepth )
363 thisItem.shiftHorizontally( -1 );
366 // Can't be sub item at top
367 if ( 0 === thisItemPosition )
369 // Already sub item of prevItem
370 if ( thisItemData['menu-item-parent-id'] === prevItemId )
372 thisItem.shiftHorizontally( 1 );
376 api.registerChange();
377 api.refreshKeyboardAccessibility();
378 api.refreshAdvancedAccessibility();
381 initAccessibility : function() {
382 var menu = $( '#menu-to-edit' );
384 api.refreshKeyboardAccessibility();
385 api.refreshAdvancedAccessibility();
388 menu.on( 'click', '.menus-move-up', function ( e ) {
389 api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), 'up' );
392 menu.on( 'click', '.menus-move-down', function ( e ) {
393 api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), 'down' );
396 menu.on( 'click', '.menus-move-top', function ( e ) {
397 api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), 'top' );
400 menu.on( 'click', '.menus-move-left', function ( e ) {
401 api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), 'left' );
404 menu.on( 'click', '.menus-move-right', function ( e ) {
405 api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), 'right' );
410 refreshAdvancedAccessibility : function() {
412 // Hide all links by default
413 $( '.menu-item-settings .field-move a' ).css( 'display', 'none' );
415 $( '.item-edit' ).each( function() {
416 var thisLink, thisLinkText, primaryItems, itemPosition, title,
417 parentItem, parentItemId, parentItemName, subItems,
419 menuItem = $this.closest( 'li.menu-item' ).first(),
420 depth = menuItem.menuItemDepth(),
421 isPrimaryMenuItem = ( 0 === depth ),
422 itemName = $this.closest( '.menu-item-handle' ).find( '.menu-item-title' ).text(),
423 position = parseInt( menuItem.index(), 10 ),
424 prevItemDepth = ( isPrimaryMenuItem ) ? depth : parseInt( depth - 1, 10 ),
425 prevItemNameLeft = menuItem.prevAll('.menu-item-depth-' + prevItemDepth).first().find( '.menu-item-title' ).text(),
426 prevItemNameRight = menuItem.prevAll('.menu-item-depth-' + depth).first().find( '.menu-item-title' ).text(),
427 totalMenuItems = $('#menu-to-edit li').length,
428 hasSameDepthSibling = menuItem.nextAll( '.menu-item-depth-' + depth ).length;
430 // Where can they move this menu item?
431 if ( 0 !== position ) {
432 thisLink = menuItem.find( '.menus-move-up' );
433 thisLink.prop( 'title', menus.moveUp ).css( 'display', 'inline' );
436 if ( 0 !== position && isPrimaryMenuItem ) {
437 thisLink = menuItem.find( '.menus-move-top' );
438 thisLink.prop( 'title', menus.moveToTop ).css( 'display', 'inline' );
441 if ( position + 1 !== totalMenuItems && 0 !== position ) {
442 thisLink = menuItem.find( '.menus-move-down' );
443 thisLink.prop( 'title', menus.moveDown ).css( 'display', 'inline' );
446 if ( 0 === position && 0 !== hasSameDepthSibling ) {
447 thisLink = menuItem.find( '.menus-move-down' );
448 thisLink.prop( 'title', menus.moveDown ).css( 'display', 'inline' );
451 if ( ! isPrimaryMenuItem ) {
452 thisLink = menuItem.find( '.menus-move-left' ),
453 thisLinkText = menus.outFrom.replace( '%s', prevItemNameLeft );
454 thisLink.prop( 'title', menus.moveOutFrom.replace( '%s', prevItemNameLeft ) ).html( thisLinkText ).css( 'display', 'inline' );
457 if ( 0 !== position ) {
458 if ( menuItem.find( '.menu-item-data-parent-id' ).val() !== menuItem.prev().find( '.menu-item-data-db-id' ).val() ) {
459 thisLink = menuItem.find( '.menus-move-right' ),
460 thisLinkText = menus.under.replace( '%s', prevItemNameRight );
461 thisLink.prop( 'title', menus.moveUnder.replace( '%s', prevItemNameRight ) ).html( thisLinkText ).css( 'display', 'inline' );
465 if ( isPrimaryMenuItem ) {
466 primaryItems = $( '.menu-item-depth-0' ),
467 itemPosition = primaryItems.index( menuItem ) + 1,
468 totalMenuItems = primaryItems.length,
470 // String together help text for primary menu items
471 title = menus.menuFocus.replace( '%1$s', itemName ).replace( '%2$d', itemPosition ).replace( '%3$d', totalMenuItems );
473 parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(),
474 parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(),
475 parentItemName = parentItem.find( '.menu-item-title' ).text(),
476 subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ),
477 itemPosition = $( subItems.parents('.menu-item').get().reverse() ).index( menuItem ) + 1;
479 // String together help text for sub menu items
480 title = menus.subMenuFocus.replace( '%1$s', itemName ).replace( '%2$d', itemPosition ).replace( '%3$s', parentItemName );
483 $this.prop('title', title).html( title );
487 refreshKeyboardAccessibility : function() {
488 $( '.item-edit' ).off( 'focus' ).on( 'focus', function(){
489 $(this).off( 'keydown' ).on( 'keydown', function(e){
493 thisItem = $this.parents( 'li.menu-item' ),
494 thisItemData = thisItem.getItemData();
496 // Bail if it's not an arrow key
497 if ( 37 != e.which && 38 != e.which && 39 != e.which && 40 != e.which )
500 // Avoid multiple keydown events
501 $this.off('keydown');
503 // Bail if there is only one menu item
504 if ( 1 === $('#menu-to-edit li').length )
507 // If RTL, swap left/right arrows
508 arrows = { '38': 'up', '40': 'down', '37': 'left', '39': 'right' };
509 if ( $('body').hasClass('rtl') )
510 arrows = { '38' : 'up', '40' : 'down', '39' : 'left', '37' : 'right' };
512 switch ( arrows[e.which] ) {
514 api.moveMenuItem( $this, 'up' );
517 api.moveMenuItem( $this, 'down' );
520 api.moveMenuItem( $this, 'left' );
523 api.moveMenuItem( $this, 'right' );
526 // Put focus back on same menu item
527 $( '#edit-' + thisItemData['menu-item-db-id'] ).focus();
533 initToggles : function() {
535 postboxes.add_postbox_toggles('nav-menus');
537 // adjust columns functions for menus UI
538 columns.useCheckboxesForHidden();
539 columns.checked = function(field) {
540 $('.field-' + field).removeClass('hidden-field');
542 columns.unchecked = function(field) {
543 $('.field-' + field).addClass('hidden-field');
546 api.menuList.hideAdvancedMenuItemFields();
548 $('.hide-postbox-tog').click(function () {
549 var hidden = $( '.accordion-container li.accordion-section' ).filter(':hidden').map(function() { return this.id; }).get().join(',');
551 action: 'closed-postboxes',
553 closedpostboxesnonce: jQuery('#closedpostboxesnonce').val(),
559 initSortables : function() {
560 var currentDepth = 0, originalDepth, minDepth, maxDepth,
561 prev, next, prevBottom, nextThreshold, helperHeight, transport,
562 menuEdge = api.menuList.offset().left,
563 body = $('body'), maxChildDepth,
564 menuMaxDepth = initialMenuMaxDepth();
566 if( 0 !== $( '#menu-to-edit li' ).length )
567 $( '.drag-instructions' ).show();
569 // Use the right edge if RTL.
570 menuEdge += api.isRTL ? api.menuList.width() : 0;
572 api.menuList.sortable({
573 handle: '.menu-item-handle',
574 placeholder: 'sortable-placeholder',
575 start: function(e, ui) {
576 var height, width, parent, children, tempHolder;
578 // handle placement for rtl orientation
580 ui.item[0].style.right = 'auto';
582 transport = ui.item.children('.menu-item-transport');
584 // Set depths. currentDepth must be set before children are located.
585 originalDepth = ui.item.menuItemDepth();
586 updateCurrentDepth(ui, originalDepth);
588 // Attach child elements to parent
589 // Skip the placeholder
590 parent = ( ui.item.next()[0] == ui.placeholder[0] ) ? ui.item.next() : ui.item;
591 children = parent.childMenuItems();
592 transport.append( children );
594 // Update the height of the placeholder to match the moving item.
595 height = transport.outerHeight();
596 // If there are children, account for distance between top of children and parent
597 height += ( height > 0 ) ? (ui.placeholder.css('margin-top').slice(0, -2) * 1) : 0;
598 height += ui.helper.outerHeight();
599 helperHeight = height;
600 height -= 2; // Subtract 2 for borders
601 ui.placeholder.height(height);
603 // Update the width of the placeholder to match the moving item.
604 maxChildDepth = originalDepth;
605 children.each(function(){
606 var depth = $(this).menuItemDepth();
607 maxChildDepth = (depth > maxChildDepth) ? depth : maxChildDepth;
609 width = ui.helper.find('.menu-item-handle').outerWidth(); // Get original width
610 width += api.depthToPx(maxChildDepth - originalDepth); // Account for children
611 width -= 2; // Subtract 2 for borders
612 ui.placeholder.width(width);
614 // Update the list of menu items.
615 tempHolder = ui.placeholder.next();
616 tempHolder.css( 'margin-top', helperHeight + 'px' ); // Set the margin to absorb the placeholder
617 ui.placeholder.detach(); // detach or jQuery UI will think the placeholder is a menu item
618 $(this).sortable( 'refresh' ); // The children aren't sortable. We should let jQ UI know.
619 ui.item.after( ui.placeholder ); // reattach the placeholder.
620 tempHolder.css('margin-top', 0); // reset the margin
622 // Now that the element is complete, we can update...
623 updateSharedVars(ui);
625 stop: function(e, ui) {
626 var children, subMenuTitle,
627 depthChange = currentDepth - originalDepth;
629 // Return child elements to the list
630 children = transport.children().insertAfter(ui.item);
632 // Add "sub menu" description
633 subMenuTitle = ui.item.find( '.item-title .is-submenu' );
634 if ( 0 < currentDepth )
639 // Update depth classes
640 if ( 0 !== depthChange ) {
641 ui.item.updateDepthClass( currentDepth );
642 children.shiftDepthClass( depthChange );
643 updateMenuMaxDepth( depthChange );
646 api.registerChange();
647 // Update the item data.
648 ui.item.updateParentMenuItemDBId();
650 // address sortable's incorrectly-calculated top in opera
651 ui.item[0].style.top = 0;
653 // handle drop placement for rtl orientation
655 ui.item[0].style.left = 'auto';
656 ui.item[0].style.right = 0;
659 api.refreshKeyboardAccessibility();
660 api.refreshAdvancedAccessibility();
662 change: function(e, ui) {
663 // Make sure the placeholder is inside the menu.
664 // Otherwise fix it, or we're in trouble.
665 if( ! ui.placeholder.parent().hasClass('menu') )
666 (prev.length) ? prev.after( ui.placeholder ) : api.menuList.prepend( ui.placeholder );
668 updateSharedVars(ui);
670 sort: function(e, ui) {
671 var offset = ui.helper.offset(),
672 edge = api.isRTL ? offset.left + ui.helper.width() : offset.left,
673 depth = api.negateIfRTL * api.pxToDepth( edge - menuEdge );
674 // Check and correct if depth is not within range.
675 // Also, if the dragged element is dragged upwards over
676 // an item, shift the placeholder to a child position.
677 if ( depth > maxDepth || offset.top < prevBottom ) depth = maxDepth;
678 else if ( depth < minDepth ) depth = minDepth;
680 if( depth != currentDepth )
681 updateCurrentDepth(ui, depth);
683 // If we overlap the next element, manually shift downwards
684 if( nextThreshold && offset.top + helperHeight > nextThreshold ) {
685 next.after( ui.placeholder );
686 updateSharedVars( ui );
687 $( this ).sortable( 'refreshPositions' );
692 function updateSharedVars(ui) {
695 prev = ui.placeholder.prev();
696 next = ui.placeholder.next();
698 // Make sure we don't select the moving item.
699 if( prev[0] == ui.item[0] ) prev = prev.prev();
700 if( next[0] == ui.item[0] ) next = next.next();
702 prevBottom = (prev.length) ? prev.offset().top + prev.height() : 0;
703 nextThreshold = (next.length) ? next.offset().top + next.height() / 3 : 0;
704 minDepth = (next.length) ? next.menuItemDepth() : 0;
707 maxDepth = ( (depth = prev.menuItemDepth() + 1) > api.options.globalMaxDepth ) ? api.options.globalMaxDepth : depth;
712 function updateCurrentDepth(ui, depth) {
713 ui.placeholder.updateDepthClass( depth, currentDepth );
714 currentDepth = depth;
717 function initialMenuMaxDepth() {
718 if( ! body[0].className ) return 0;
719 var match = body[0].className.match(/menu-max-depth-(\d+)/);
720 return match && match[1] ? parseInt( match[1], 10 ) : 0;
723 function updateMenuMaxDepth( depthChange ) {
724 var depth, newDepth = menuMaxDepth;
725 if ( depthChange === 0 ) {
727 } else if ( depthChange > 0 ) {
728 depth = maxChildDepth + depthChange;
729 if( depth > menuMaxDepth )
731 } else if ( depthChange < 0 && maxChildDepth == menuMaxDepth ) {
732 while( ! $('.menu-item-depth-' + newDepth, api.menuList).length && newDepth > 0 )
735 // Update the depth class.
736 body.removeClass( 'menu-max-depth-' + menuMaxDepth ).addClass( 'menu-max-depth-' + newDepth );
737 menuMaxDepth = newDepth;
741 initManageLocations : function () {
742 $('#menu-locations-wrap form').submit(function(){
743 window.onbeforeunload = null;
745 $('.menu-location-menus select').on('change', function () {
746 var editLink = $(this).closest('tr').find('.locations-edit-menu-link');
747 if ($(this).find('option:selected').data('orig'))
754 attachMenuEditListeners : function() {
756 $('#update-nav-menu').bind('click', function(e) {
757 if ( e.target && e.target.className ) {
758 if ( -1 != e.target.className.indexOf('item-edit') ) {
759 return that.eventOnClickEditLink(e.target);
760 } else if ( -1 != e.target.className.indexOf('menu-save') ) {
761 return that.eventOnClickMenuSave(e.target);
762 } else if ( -1 != e.target.className.indexOf('menu-delete') ) {
763 return that.eventOnClickMenuDelete(e.target);
764 } else if ( -1 != e.target.className.indexOf('item-delete') ) {
765 return that.eventOnClickMenuItemDelete(e.target);
766 } else if ( -1 != e.target.className.indexOf('item-cancel') ) {
767 return that.eventOnClickCancelLink(e.target);
771 $('#add-custom-links input[type="text"]').keypress(function(e){
772 if ( e.keyCode === 13 ) {
774 $( '#submit-customlinkdiv' ).click();
780 * An interface for managing default values for input elements
781 * that is both JS and accessibility-friendly.
783 * Input elements that add the class 'input-with-default-title'
784 * will have their values set to the provided HTML title when empty.
786 setupInputWithDefaultTitle : function() {
787 var name = 'input-with-default-title';
789 $('.' + name).each( function(){
790 var $t = $(this), title = $t.attr('title'), val = $t.val();
791 $t.data( name, title );
793 if( '' === val ) $t.val( title );
794 else if ( title == val ) return;
795 else $t.removeClass( name );
796 }).focus( function(){
798 if( $t.val() == $t.data(name) )
799 $t.val('').removeClass( name );
802 if( '' === $t.val() )
803 $t.addClass( name ).val( $t.data(name) );
806 $( '.blank-slate .input-with-default-title' ).focus();
809 attachThemeLocationsListeners : function() {
810 var loc = $('#nav-menu-theme-locations'), params = {};
811 params.action = 'menu-locations-save';
812 params['menu-settings-column-nonce'] = $('#menu-settings-column-nonce').val();
813 loc.find('input[type="submit"]').click(function() {
814 loc.find('select').each(function() {
815 params[this.name] = $(this).val();
817 loc.find('.spinner').show();
818 $.post( ajaxurl, params, function() {
819 loc.find('.spinner').hide();
825 attachQuickSearchListeners : function() {
828 $('.quick-search').keypress(function(e){
831 if( 13 == e.which ) {
832 api.updateQuickSearchResults( t );
836 if( searchTimer ) clearTimeout(searchTimer);
838 searchTimer = setTimeout(function(){
839 api.updateQuickSearchResults( t );
841 }).attr('autocomplete','off');
844 updateQuickSearchResults : function(input) {
849 if( q.length < minSearchLength ) return;
851 panel = input.parents('.tabs-panel');
853 'action': 'menu-quick-search',
854 'response-format': 'markup',
855 'menu': $('#menu').val(),
856 'menu-settings-column-nonce': $('#menu-settings-column-nonce').val(),
858 'type': input.attr('name')
861 $('.spinner', panel).show();
863 $.post( ajaxurl, params, function(menuMarkup) {
864 api.processQuickSearchQueryResponse(menuMarkup, params, panel);
868 addCustomLink : function( processMethod ) {
869 var url = $('#custom-menu-item-url').val(),
870 label = $('#custom-menu-item-name').val();
872 processMethod = processMethod || api.addMenuItemToBottom;
874 if ( '' === url || 'http://' == url )
877 // Show the ajax spinner
878 $('.customlinkdiv .spinner').show();
879 this.addLinkToMenu( url, label, processMethod, function() {
880 // Remove the ajax spinner
881 $('.customlinkdiv .spinner').hide();
882 // Set custom link form back to defaults
883 $('#custom-menu-item-name').val('').blur();
884 $('#custom-menu-item-url').val('http://');
888 addLinkToMenu : function(url, label, processMethod, callback) {
889 processMethod = processMethod || api.addMenuItemToBottom;
890 callback = callback || function(){};
894 'menu-item-type': 'custom',
895 'menu-item-url': url,
896 'menu-item-title': label
898 }, processMethod, callback);
901 addItemToMenu : function(menuItem, processMethod, callback) {
902 var menu = $('#menu').val(),
903 nonce = $('#menu-settings-column-nonce').val(),
906 processMethod = processMethod || function(){};
907 callback = callback || function(){};
910 'action': 'add-menu-item',
912 'menu-settings-column-nonce': nonce,
913 'menu-item': menuItem
916 $.post( ajaxurl, params, function(menuMarkup) {
917 var ins = $('#menu-instructions');
919 menuMarkup = $.trim( menuMarkup ); // Trim leading whitespaces
920 processMethod(menuMarkup, params);
922 // Make it stand out a bit more visually, by adding a fadeIn
923 $( 'li.pending' ).hide().fadeIn('slow');
924 $( '.drag-instructions' ).show();
925 if( ! ins.hasClass( 'menu-instructions-inactive' ) && ins.siblings().length )
926 ins.addClass( 'menu-instructions-inactive' );
933 * Process the add menu item request response into menu list item.
935 * @param string menuMarkup The text server response of menu item markup.
936 * @param object req The request arguments.
938 addMenuItemToBottom : function( menuMarkup ) {
939 $(menuMarkup).hideAdvancedMenuItemFields().appendTo( api.targetList );
940 api.refreshKeyboardAccessibility();
941 api.refreshAdvancedAccessibility();
944 addMenuItemToTop : function( menuMarkup ) {
945 $(menuMarkup).hideAdvancedMenuItemFields().prependTo( api.targetList );
946 api.refreshKeyboardAccessibility();
947 api.refreshAdvancedAccessibility();
950 attachUnsavedChangesListener : function() {
951 $('#menu-management input, #menu-management select, #menu-management, #menu-management textarea, .menu-location-menus select').change(function(){
952 api.registerChange();
955 if ( 0 !== $('#menu-to-edit').length || 0 !== $('.menu-location-menus select').length ) {
956 window.onbeforeunload = function(){
957 if ( api.menusChanged )
958 return navMenuL10n.saveAlert;
961 // Make the post boxes read-only, as they can't be used yet
962 $( '#menu-settings-column' ).find( 'input,select' ).end().find( 'a' ).attr( 'href', '#' ).unbind( 'click' );
966 registerChange : function() {
967 api.menusChanged = true;
970 attachTabsPanelListeners : function() {
971 $('#menu-settings-column').bind('click', function(e) {
972 var selectAreaMatch, panelId, wrapper, items,
973 target = $(e.target);
975 if ( target.hasClass('nav-tab-link') ) {
977 panelId = target.data( 'type' );
979 wrapper = target.parents('.accordion-section-content').first();
981 // upon changing tabs, we want to uncheck all checkboxes
982 $('input', wrapper).removeAttr('checked');
984 $('.tabs-panel-active', wrapper).removeClass('tabs-panel-active').addClass('tabs-panel-inactive');
985 $('#' + panelId, wrapper).removeClass('tabs-panel-inactive').addClass('tabs-panel-active');
987 $('.tabs', wrapper).removeClass('tabs');
988 target.parent().addClass('tabs');
990 // select the search bar
991 $('.quick-search', wrapper).focus();
994 } else if ( target.hasClass('select-all') ) {
995 selectAreaMatch = /#(.*)$/.exec(e.target.href);
996 if ( selectAreaMatch && selectAreaMatch[1] ) {
997 items = $('#' + selectAreaMatch[1] + ' .tabs-panel-active .menu-item-title input');
998 if( items.length === items.filter(':checked').length )
999 items.removeAttr('checked');
1001 items.prop('checked', true);
1004 } else if ( target.hasClass('submit-add-to-menu') ) {
1005 api.registerChange();
1007 if ( e.target.id && 'submit-customlinkdiv' == e.target.id )
1008 api.addCustomLink( api.addMenuItemToBottom );
1009 else if ( e.target.id && -1 != e.target.id.indexOf('submit-') )
1010 $('#' + e.target.id.replace(/submit-/, '')).addSelectedToMenu( api.addMenuItemToBottom );
1012 } else if ( target.hasClass('page-numbers') ) {
1013 $.post( ajaxurl, e.target.href.replace(/.*\?/, '').replace(/action=([^&]*)/, '') + '&action=menu-get-metabox',
1015 if ( -1 == resp.indexOf('replace-id') )
1018 var metaBoxData = $.parseJSON(resp),
1019 toReplace = document.getElementById(metaBoxData['replace-id']),
1020 placeholder = document.createElement('div'),
1021 wrap = document.createElement('div');
1023 if ( ! metaBoxData.markup || ! toReplace )
1026 wrap.innerHTML = metaBoxData.markup ? metaBoxData.markup : '';
1028 toReplace.parentNode.insertBefore( placeholder, toReplace );
1029 placeholder.parentNode.removeChild( toReplace );
1031 placeholder.parentNode.insertBefore( wrap, placeholder );
1033 placeholder.parentNode.removeChild( placeholder );
1043 eventOnClickEditLink : function(clickedEl) {
1045 matchedSection = /#(.*)$/.exec(clickedEl.href);
1046 if ( matchedSection && matchedSection[1] ) {
1047 settings = $('#'+matchedSection[1]);
1048 item = settings.parent();
1049 if( 0 !== item.length ) {
1050 if( item.hasClass('menu-item-edit-inactive') ) {
1051 if( ! settings.data('menu-item-data') ) {
1052 settings.data( 'menu-item-data', settings.getItemData() );
1054 settings.slideDown('fast');
1055 item.removeClass('menu-item-edit-inactive')
1056 .addClass('menu-item-edit-active');
1058 settings.slideUp('fast');
1059 item.removeClass('menu-item-edit-active')
1060 .addClass('menu-item-edit-inactive');
1067 eventOnClickCancelLink : function(clickedEl) {
1068 var settings = $( clickedEl ).closest( '.menu-item-settings' ),
1069 thisMenuItem = $( clickedEl ).closest( '.menu-item' );
1070 thisMenuItem.removeClass('menu-item-edit-active').addClass('menu-item-edit-inactive');
1071 settings.setItemData( settings.data('menu-item-data') ).hide();
1075 eventOnClickMenuSave : function() {
1077 menuName = $('#menu-name'),
1078 menuNameVal = menuName.val();
1079 // Cancel and warn if invalid menu name
1080 if( !menuNameVal || menuNameVal == menuName.attr('title') || !menuNameVal.replace(/\s+/, '') ) {
1081 menuName.parent().addClass('form-invalid');
1084 // Copy menu theme locations
1085 $('#nav-menu-theme-locations select').each(function() {
1086 locs += '<input type="hidden" name="' + this.name + '" value="' + $(this).val() + '" />';
1088 $('#update-nav-menu').append( locs );
1089 // Update menu item position data
1090 api.menuList.find('.menu-item-data-position').val( function(index) { return index + 1; } );
1091 window.onbeforeunload = null;
1096 eventOnClickMenuDelete : function() {
1097 // Delete warning AYS
1098 if ( window.confirm( navMenuL10n.warnDeleteMenu ) ) {
1099 window.onbeforeunload = null;
1105 eventOnClickMenuItemDelete : function(clickedEl) {
1106 var itemID = parseInt(clickedEl.id.replace('delete-', ''), 10);
1107 api.removeMenuItem( $('#menu-item-' + itemID) );
1108 api.registerChange();
1113 * Process the quick search response into a search result
1115 * @param string resp The server response to the query.
1116 * @param object req The request arguments.
1117 * @param jQuery panel The tabs panel we're searching in.
1119 processQuickSearchQueryResponse : function(resp, req, panel) {
1122 form = document.getElementById('nav-menu-meta'),
1123 pattern = /menu-item[(\[^]\]*/,
1124 $items = $('<div>').html(resp).find('li'),
1127 if( ! $items.length ) {
1128 $('.categorychecklist', panel).html( '<li><p>' + navMenuL10n.noResultsFound + '</p></li>' );
1129 $('.spinner', panel).hide();
1133 $items.each(function(){
1136 // make a unique DB ID number
1137 matched = pattern.exec($item.html());
1139 if ( matched && matched[1] ) {
1141 while( form.elements['menu-item[' + newID + '][menu-item-type]'] || takenIDs[ newID ] ) {
1145 takenIDs[newID] = true;
1146 if ( newID != matched[1] ) {
1147 $item.html( $item.html().replace(new RegExp(
1148 'menu-item\\[' + matched[1] + '\\]', 'g'),
1149 'menu-item[' + newID + ']'
1155 $('.categorychecklist', panel).html( $items );
1156 $('.spinner', panel).hide();
1159 removeMenuItem : function(el) {
1160 var children = el.childMenuItems();
1162 el.addClass('deleting').animate({
1165 }, 350, function() {
1166 var ins = $('#menu-instructions');
1168 children.shiftDepthClass( -1 ).updateParentMenuItemDBId();
1169 if ( 0 === $( '#menu-to-edit li' ).length ) {
1170 $( '.drag-instructions' ).hide();
1171 ins.removeClass( 'menu-instructions-inactive' );
1176 depthToPx : function(depth) {
1177 return depth * api.options.menuItemDepthPerLevel;
1180 pxToDepth : function(px) {
1181 return Math.floor(px / api.options.menuItemDepthPerLevel);
1186 $(document).ready(function(){ wpNavMenu.init(); });