run:R W Run
DIR
2026-03-11 16:18:51
R W Run
2.86 KB
2026-03-11 16:18:51
R W Run
758 By
2026-03-11 16:18:51
R W Run
6.24 KB
2026-03-11 16:18:51
R W Run
2.95 KB
2026-03-11 16:18:51
R W Run
5.66 KB
2026-03-11 16:18:51
R W Run
2.04 KB
2026-03-11 16:18:51
R W Run
11.32 KB
2026-03-11 16:18:51
R W Run
3.01 KB
2026-03-11 16:18:51
R W Run
9.54 KB
2026-03-11 16:18:51
R W Run
3.4 KB
2026-03-11 16:18:51
R W Run
2.85 KB
2026-03-11 16:18:51
R W Run
1.28 KB
2026-03-11 16:18:51
R W Run
61.15 KB
2026-03-11 16:18:51
R W Run
23.12 KB
2026-03-11 16:18:51
R W Run
3.35 KB
2026-03-11 16:18:51
R W Run
1.18 KB
2026-03-11 16:18:51
R W Run
1.98 KB
2026-03-11 16:18:51
R W Run
288.41 KB
2026-03-11 16:18:51
R W Run
109.69 KB
2026-03-11 16:18:51
R W Run
111.46 KB
2026-03-11 16:18:51
R W Run
47.14 KB
2026-03-11 16:18:51
R W Run
70.05 KB
2026-03-11 16:18:51
R W Run
27.41 KB
2026-03-11 16:18:51
R W Run
27.02 KB
2026-03-11 16:18:51
R W Run
8.65 KB
2026-03-11 16:18:51
R W Run
37.12 KB
2026-03-11 16:18:51
R W Run
15.13 KB
2026-03-11 16:18:51
R W Run
41.61 KB
2026-03-11 16:18:51
R W Run
13.14 KB
2026-03-11 16:18:51
R W Run
44 KB
2026-03-11 16:18:51
R W Run
12.78 KB
2026-03-11 16:18:51
R W Run
7.67 KB
2026-03-11 16:18:51
R W Run
5.41 KB
2026-03-11 16:18:51
R W Run
3.65 KB
2026-03-11 16:18:51
R W Run
39.98 KB
2026-03-11 16:18:51
R W Run
15.15 KB
2026-03-11 16:18:51
R W Run
20.17 KB
2026-03-11 16:18:51
R W Run
9.41 KB
2026-03-11 16:18:51
R W Run
7.61 KB
2026-03-11 16:18:51
R W Run
2.93 KB
2026-03-11 16:18:51
R W Run
23.09 KB
2026-03-11 16:18:51
R W Run
890 By
2026-03-11 16:18:51
R W Run
423 By
2026-03-11 16:18:51
R W Run
3.89 KB
2026-03-11 16:18:51
R W Run
1.7 KB
2026-03-11 16:18:51
R W Run
1.27 KB
2026-03-11 16:18:51
R W Run
611 By
2026-03-11 16:18:51
R W Run
3.38 KB
2026-03-11 16:18:51
R W Run
1.13 KB
2026-03-11 16:18:51
R W Run
6.61 KB
2026-03-11 16:18:51
R W Run
2.38 KB
2026-03-11 16:18:51
R W Run
61.15 KB
2026-03-11 16:18:51
R W Run
30.06 KB
2026-03-11 16:18:51
R W Run
4.14 KB
2026-03-11 16:18:51
R W Run
1.1 KB
2026-03-11 16:18:51
R W Run
1.31 KB
2026-03-11 16:18:51
R W Run
847 By
2026-03-11 16:18:51
R W Run
6.92 KB
2026-03-11 16:18:51
R W Run
2.35 KB
2026-03-11 16:18:51
R W Run
38.68 KB
2026-03-11 16:18:51
R W Run
18.4 KB
2026-03-11 16:18:51
R W Run
18.49 KB
2026-03-11 16:18:51
R W Run
6.6 KB
2026-03-11 16:18:51
R W Run
10.67 KB
2026-03-11 16:18:51
R W Run
5.03 KB
2026-03-11 16:18:51
R W Run
33.92 KB
2026-03-11 16:18:51
R W Run
17.97 KB
2026-03-11 16:18:51
R W Run
876 By
2026-03-11 16:18:51
R W Run
620 By
2026-03-11 16:18:51
R W Run
13.15 KB
2026-03-11 16:18:51
R W Run
6.13 KB
2026-03-11 16:18:51
R W Run
6.1 KB
2026-03-11 16:18:51
R W Run
2.2 KB
2026-03-11 16:18:51
R W Run
3.2 KB
2026-03-11 16:18:51
R W Run
1.53 KB
2026-03-11 16:18:51
R W Run
10.88 KB
2026-03-11 16:18:51
R W Run
3 KB
2026-03-11 16:18:51
R W Run
5.64 KB
2026-03-11 16:18:51
R W Run
2.22 KB
2026-03-11 16:18:51
R W Run
5.96 KB
2026-03-11 16:18:51
R W Run
2.41 KB
2026-03-11 16:18:51
R W Run
24.77 KB
2026-03-11 16:18:51
R W Run
11.43 KB
2026-03-11 16:18:51
R W Run
54.94 KB
2026-03-11 16:18:51
R W Run
26.51 KB
2026-03-11 16:18:51
R W Run
109.37 KB
2026-03-11 16:18:51
R W Run
47.31 KB
2026-03-11 16:18:51
R W Run
17.91 KB
2026-03-11 16:18:51
R W Run
7.81 KB
2026-03-11 16:18:51
R W Run
2.25 KB
2026-03-11 16:18:51
R W Run
676 By
2026-03-11 16:18:51
R W Run
22.56 KB
2026-03-11 16:18:51
R W Run
12.31 KB
2026-03-11 16:18:51
R W Run
7.52 KB
2026-03-11 16:18:51
R W Run
1.49 KB
2026-03-11 16:18:51
R W Run
740 By
2026-03-11 16:18:51
R W Run
458 By
2026-03-11 16:18:51
R W Run
error_log
📄customize-nav-menus.js
1/**
2 * @output wp-admin/js/customize-nav-menus.js
3 */
4
5/* global menus, _wpCustomizeNavMenusSettings, wpNavMenu, console */
6( function( api, wp, $ ) {
7 'use strict';
8
9 /**
10 * Set up wpNavMenu for drag and drop.
11 */
12 wpNavMenu.originalInit = wpNavMenu.init;
13 wpNavMenu.options.menuItemDepthPerLevel = 20;
14 wpNavMenu.options.sortableItems = '> .customize-control-nav_menu_item';
15 wpNavMenu.options.targetTolerance = 10;
16 wpNavMenu.init = function() {
17 this.jQueryExtensions();
18 };
19
20 /**
21 * @namespace wp.customize.Menus
22 */
23 api.Menus = api.Menus || {};
24
25 // Link settings.
26 api.Menus.data = {
27 itemTypes: [],
28 l10n: {},
29 settingTransport: 'refresh',
30 phpIntMax: 0,
31 defaultSettingValues: {
32 nav_menu: {},
33 nav_menu_item: {}
34 },
35 locationSlugMappedToName: {}
36 };
37 if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) {
38 $.extend( api.Menus.data, _wpCustomizeNavMenusSettings );
39 }
40
41 /**
42 * Newly-created Nav Menus and Nav Menu Items have negative integer IDs which
43 * serve as placeholders until Save & Publish happens.
44 *
45 * @alias wp.customize.Menus.generatePlaceholderAutoIncrementId
46 *
47 * @return {number}
48 */
49 api.Menus.generatePlaceholderAutoIncrementId = function() {
50 return -Math.ceil( api.Menus.data.phpIntMax * Math.random() );
51 };
52
53 /**
54 * wp.customize.Menus.AvailableItemModel
55 *
56 * A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class.
57 *
58 * @class wp.customize.Menus.AvailableItemModel
59 * @augments Backbone.Model
60 */
61 api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend(
62 {
63 id: null // This is only used by Backbone.
64 },
65 api.Menus.data.defaultSettingValues.nav_menu_item
66 ) );
67
68 /**
69 * wp.customize.Menus.AvailableItemCollection
70 *
71 * Collection for available menu item models.
72 *
73 * @class wp.customize.Menus.AvailableItemCollection
74 * @augments Backbone.Collection
75 */
76 api.Menus.AvailableItemCollection = Backbone.Collection.extend(/** @lends wp.customize.Menus.AvailableItemCollection.prototype */{
77 model: api.Menus.AvailableItemModel,
78
79 sort_key: 'order',
80
81 comparator: function( item ) {
82 return -item.get( this.sort_key );
83 },
84
85 sortByField: function( fieldName ) {
86 this.sort_key = fieldName;
87 this.sort();
88 }
89 });
90 api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems );
91
92 /**
93 * Insert a new `auto-draft` post.
94 *
95 * @since 4.7.0
96 * @alias wp.customize.Menus.insertAutoDraftPost
97 *
98 * @param {Object} params - Parameters for the draft post to create.
99 * @param {string} params.post_type - Post type to add.
100 * @param {string} params.post_title - Post title to use.
101 * @return {jQuery.promise} Promise resolved with the added post.
102 */
103 api.Menus.insertAutoDraftPost = function insertAutoDraftPost( params ) {
104 var request, deferred = $.Deferred();
105
106 request = wp.ajax.post( 'customize-nav-menus-insert-auto-draft', {
107 'customize-menus-nonce': api.settings.nonce['customize-menus'],
108 'wp_customize': 'on',
109 'customize_changeset_uuid': api.settings.changeset.uuid,
110 'params': params
111 } );
112
113 request.done( function( response ) {
114 if ( response.post_id ) {
115 api( 'nav_menus_created_posts' ).set(
116 api( 'nav_menus_created_posts' ).get().concat( [ response.post_id ] )
117 );
118
119 if ( 'page' === params.post_type ) {
120
121 // Activate static front page controls as this could be the first page created.
122 if ( api.section.has( 'static_front_page' ) ) {
123 api.section( 'static_front_page' ).activate();
124 }
125
126 // Add new page to dropdown-pages controls.
127 api.control.each( function( control ) {
128 var select;
129 if ( 'dropdown-pages' === control.params.type ) {
130 select = control.container.find( 'select[name^="_customize-dropdown-pages-"]' );
131 select.append( new Option( params.post_title, response.post_id ) );
132 }
133 } );
134 }
135 deferred.resolve( response );
136 }
137 } );
138
139 request.fail( function( response ) {
140 var error = response || '';
141
142 if ( 'undefined' !== typeof response.message ) {
143 error = response.message;
144 }
145
146 console.error( error );
147 deferred.rejectWith( error );
148 } );
149
150 return deferred.promise();
151 };
152
153 api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Menus.AvailableMenuItemsPanelView.prototype */{
154
155 el: '#available-menu-items',
156
157 events: {
158 'input #menu-items-search': 'debounceSearch',
159 'focus .menu-item-tpl': 'focus',
160 'click .menu-item-tpl': '_submit',
161 'click #custom-menu-item-submit': '_submitLink',
162 'keypress #custom-menu-item-name': '_submitLink',
163 'click .new-content-item .add-content': '_submitNew',
164 'keypress .create-item-input': '_submitNew',
165 'keydown': 'keyboardAccessible'
166 },
167
168 // Cache current selected menu item.
169 selected: null,
170
171 // Cache menu control that opened the panel.
172 currentMenuControl: null,
173 debounceSearch: null,
174 $search: null,
175 $clearResults: null,
176 searchTerm: '',
177 rendered: false,
178 pages: {},
179 sectionContent: '',
180 loading: false,
181 addingNew: false,
182
183 /**
184 * wp.customize.Menus.AvailableMenuItemsPanelView
185 *
186 * View class for the available menu items panel.
187 *
188 * @constructs wp.customize.Menus.AvailableMenuItemsPanelView
189 * @augments wp.Backbone.View
190 */
191 initialize: function() {
192 var self = this;
193
194 if ( ! api.panel.has( 'nav_menus' ) ) {
195 return;
196 }
197
198 this.$search = $( '#menu-items-search' );
199 this.$clearResults = this.$el.find( '.clear-results' );
200 this.sectionContent = this.$el.find( '.available-menu-items-list' );
201
202 this.debounceSearch = _.debounce( self.search, 500 );
203
204 _.bindAll( this, 'close' );
205
206 /*
207 * If the available menu items panel is open and the customize controls
208 * are interacted with (other than an item being deleted), then close
209 * the available menu items panel. Also close on back button click.
210 */
211 $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) {
212 var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
213 isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
214 if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
215 self.close();
216 }
217 } );
218
219 // Clear the search results and trigger an `input` event to fire a new search.
220 this.$clearResults.on( 'click', function() {
221 self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' );
222 } );
223
224 this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() {
225 $( this ).removeClass( 'invalid' );
226 var errorMessageId = $( this ).attr( 'aria-describedby' );
227 $( '#' + errorMessageId ).hide();
228 $( this ).removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' );
229 });
230
231 // Load available items if it looks like we'll need them.
232 api.panel( 'nav_menus' ).container.on( 'expanded', function() {
233 if ( ! self.rendered ) {
234 self.initList();
235 self.rendered = true;
236 }
237 });
238
239 // Load more items.
240 this.sectionContent.on( 'scroll', function() {
241 var totalHeight = self.$el.find( '.accordion-section.open .available-menu-items-list' ).prop( 'scrollHeight' ),
242 visibleHeight = self.$el.find( '.accordion-section.open' ).height();
243
244 if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) {
245 var type = $( this ).data( 'type' ),
246 object = $( this ).data( 'object' );
247
248 if ( 'search' === type ) {
249 if ( self.searchTerm ) {
250 self.doSearch( self.pages.search );
251 }
252 } else {
253 self.loadItems( [
254 { type: type, object: object }
255 ] );
256 }
257 }
258 });
259
260 // Close the panel if the URL in the preview changes.
261 api.previewer.bind( 'url', this.close );
262
263 self.delegateEvents();
264 },
265
266 // Search input change handler.
267 search: function( event ) {
268 var $searchSection = $( '#available-menu-items-search' ),
269 $otherSections = $( '#available-menu-items .accordion-section' ).not( $searchSection );
270
271 if ( ! event ) {
272 return;
273 }
274
275 if ( this.searchTerm === event.target.value ) {
276 return;
277 }
278
279 if ( '' !== event.target.value && ! $searchSection.hasClass( 'open' ) ) {
280 $otherSections.fadeOut( 100 );
281 $searchSection.find( '.accordion-section-content' ).slideDown( 'fast' );
282 $searchSection.addClass( 'open' );
283 this.$clearResults.addClass( 'is-visible' );
284 } else if ( '' === event.target.value ) {
285 $searchSection.removeClass( 'open' );
286 $otherSections.show();
287 this.$clearResults.removeClass( 'is-visible' );
288 }
289
290 this.searchTerm = event.target.value;
291 this.pages.search = 1;
292 this.doSearch( 1 );
293 },
294
295 // Get search results.
296 doSearch: function( page ) {
297 var self = this, params,
298 $section = $( '#available-menu-items-search' ),
299 $content = $section.find( '.accordion-section-content' ),
300 itemTemplate = wp.template( 'available-menu-item' );
301
302 if ( self.currentRequest ) {
303 self.currentRequest.abort();
304 }
305
306 if ( page < 0 ) {
307 return;
308 } else if ( page > 1 ) {
309 $section.addClass( 'loading-more' );
310 $content.attr( 'aria-busy', 'true' );
311 wp.a11y.speak( api.Menus.data.l10n.itemsLoadingMore );
312 } else if ( '' === self.searchTerm ) {
313 $content.html( '' );
314 wp.a11y.speak( '' );
315 return;
316 }
317
318 $section.addClass( 'loading' );
319 self.loading = true;
320
321 params = api.previewer.query( { excludeCustomizedSaved: true } );
322 _.extend( params, {
323 'customize-menus-nonce': api.settings.nonce['customize-menus'],
324 'wp_customize': 'on',
325 'search': self.searchTerm,
326 'page': page
327 } );
328
329 self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params );
330
331 self.currentRequest.done(function( data ) {
332 var items;
333 if ( 1 === page ) {
334 // Clear previous results as it's a new search.
335 $content.empty();
336 }
337 $section.removeClass( 'loading loading-more' );
338 $content.attr( 'aria-busy', 'false' );
339 $section.addClass( 'open' );
340 self.loading = false;
341 items = new api.Menus.AvailableItemCollection( data.items );
342 self.collection.add( items.models );
343 items.each( function( menuItem ) {
344 $content.append( itemTemplate( menuItem.attributes ) );
345 } );
346 if ( 20 > items.length ) {
347 self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either.
348 } else {
349 self.pages.search = self.pages.search + 1;
350 }
351 if ( items && page > 1 ) {
352 wp.a11y.speak( api.Menus.data.l10n.itemsFoundMore.replace( '%d', items.length ) );
353 } else if ( items && page === 1 ) {
354 wp.a11y.speak( api.Menus.data.l10n.itemsFound.replace( '%d', items.length ) );
355 }
356 });
357
358 self.currentRequest.fail(function( data ) {
359 // data.message may be undefined, for example when typing slow and the request is aborted.
360 if ( data.message ) {
361 $content.empty().append( $( '<li class="nothing-found"></li>' ).text( data.message ) );
362 wp.a11y.speak( data.message );
363 }
364 self.pages.search = -1;
365 });
366
367 self.currentRequest.always(function() {
368 $section.removeClass( 'loading loading-more' );
369 $content.attr( 'aria-busy', 'false' );
370 self.loading = false;
371 self.currentRequest = null;
372 });
373 },
374
375 // Render the individual items.
376 initList: function() {
377 var self = this;
378
379 // Render the template for each item by type.
380 _.each( api.Menus.data.itemTypes, function( itemType ) {
381 self.pages[ itemType.type + ':' + itemType.object ] = 0;
382 } );
383 self.loadItems( api.Menus.data.itemTypes );
384 },
385
386 /**
387 * Load available nav menu items.
388 *
389 * @since 4.3.0
390 * @since 4.7.0 Changed function signature to take list of item types instead of single type/object.
391 * @access private
392 *
393 * @param {Array.<Object>} itemTypes List of objects containing type and key.
394 * @param {string} deprecated Formerly the object parameter.
395 * @return {void}
396 */
397 loadItems: function( itemTypes, deprecated ) {
398 var self = this, _itemTypes, requestItemTypes = [], params, request, itemTemplate, availableMenuItemContainers = {};
399 itemTemplate = wp.template( 'available-menu-item' );
400
401 if ( _.isString( itemTypes ) && _.isString( deprecated ) ) {
402 _itemTypes = [ { type: itemTypes, object: deprecated } ];
403 } else {
404 _itemTypes = itemTypes;
405 }
406
407 _.each( _itemTypes, function( itemType ) {
408 var container, name = itemType.type + ':' + itemType.object;
409 if ( -1 === self.pages[ name ] ) {
410 return; // Skip types for which there are no more results.
411 }
412 container = $( '#available-menu-items-' + itemType.type + '-' + itemType.object );
413 container.find( '.accordion-section-title' ).addClass( 'loading' );
414 availableMenuItemContainers[ name ] = container;
415
416 requestItemTypes.push( {
417 object: itemType.object,
418 type: itemType.type,
419 page: self.pages[ name ]
420 } );
421 } );
422
423 if ( 0 === requestItemTypes.length ) {
424 return;
425 }
426
427 self.loading = true;
428
429 params = api.previewer.query( { excludeCustomizedSaved: true } );
430 _.extend( params, {
431 'customize-menus-nonce': api.settings.nonce['customize-menus'],
432 'wp_customize': 'on',
433 'item_types': requestItemTypes
434 } );
435
436 request = wp.ajax.post( 'load-available-menu-items-customizer', params );
437
438 request.done(function( data ) {
439 var typeInner;
440 _.each( data.items, function( typeItems, name ) {
441 if ( 0 === typeItems.length ) {
442 if ( 0 === self.pages[ name ] ) {
443 availableMenuItemContainers[ name ].find( '.accordion-section-title' )
444 .addClass( 'cannot-expand' )
445 .removeClass( 'loading' )
446 .find( '.accordion-section-title > button' )
447 .prop( 'tabIndex', -1 );
448 }
449 self.pages[ name ] = -1;
450 return;
451 } else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) {
452 availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).trigger( 'click' );
453 }
454 typeItems = new api.Menus.AvailableItemCollection( typeItems ); // @todo Why is this collection created and then thrown away?
455 self.collection.add( typeItems.models );
456 typeInner = availableMenuItemContainers[ name ].find( '.available-menu-items-list' );
457 typeItems.each( function( menuItem ) {
458 typeInner.append( itemTemplate( menuItem.attributes ) );
459 } );
460 self.pages[ name ] += 1;
461 });
462 });
463 request.fail(function( data ) {
464 if ( typeof console !== 'undefined' && console.error ) {
465 console.error( data );
466 }
467 });
468 request.always(function() {
469 _.each( availableMenuItemContainers, function( container ) {
470 container.find( '.accordion-section-title' ).removeClass( 'loading' );
471 } );
472 self.loading = false;
473 });
474 },
475
476 // Adjust the height of each section of items to fit the screen.
477 itemSectionHeight: function() {
478 var sections, lists, totalHeight, accordionHeight, diff;
479 totalHeight = window.innerHeight;
480 sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' );
481 lists = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")' );
482 accordionHeight = 46 * ( 1 + sections.length ) + 14; // Magic numbers.
483 diff = totalHeight - accordionHeight;
484 if ( 120 < diff && 290 > diff ) {
485 sections.css( 'max-height', diff );
486 lists.css( 'max-height', ( diff - 60 ) );
487 }
488 },
489
490 // Highlights a menu item.
491 select: function( menuitemTpl ) {
492 this.selected = $( menuitemTpl );
493 this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' );
494 this.selected.addClass( 'selected' );
495 },
496
497 // Highlights a menu item on focus.
498 focus: function( event ) {
499 this.select( $( event.currentTarget ) );
500 },
501
502 // Submit handler for keypress and click on menu item.
503 _submit: function( event ) {
504 // Only proceed with keypress if it is Enter or Spacebar.
505 if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) {
506 return;
507 }
508
509 this.submit( $( event.currentTarget ) );
510 },
511
512 // Adds a selected menu item to the menu.
513 submit: function( menuitemTpl ) {
514 var menuitemId, menu_item;
515
516 if ( ! menuitemTpl ) {
517 menuitemTpl = this.selected;
518 }
519
520 if ( ! menuitemTpl || ! this.currentMenuControl ) {
521 return;
522 }
523
524 this.select( menuitemTpl );
525
526 menuitemId = $( this.selected ).data( 'menu-item-id' );
527 menu_item = this.collection.findWhere( { id: menuitemId } );
528 if ( ! menu_item ) {
529 return;
530 }
531
532 // Leave the title as empty to reuse the original title as a placeholder if set.
533 var nav_menu_item = Object.assign( {}, menu_item.attributes );
534 if ( nav_menu_item.title === nav_menu_item.original_title ) {
535 nav_menu_item.title = '';
536 }
537
538 this.currentMenuControl.addItemToMenu( nav_menu_item );
539
540 $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' );
541 },
542
543 // Submit handler for keypress and click on custom menu item.
544 _submitLink: function( event ) {
545 // Only proceed with keypress if it is Enter.
546 if ( 'keypress' === event.type && 13 !== event.which ) {
547 return;
548 }
549
550 this.submitLink();
551 },
552
553 // Adds the custom menu item to the menu.
554 submitLink: function() {
555 var menuItem,
556 itemName = $( '#custom-menu-item-name' ),
557 itemUrl = $( '#custom-menu-item-url' ),
558 urlErrorMessage = $( '#custom-url-error' ),
559 nameErrorMessage = $( '#custom-name-error' ),
560 url = itemUrl.val().trim(),
561 urlRegex,
562 errorText;
563
564 if ( ! this.currentMenuControl ) {
565 return;
566 }
567
568 /*
569 * Allow URLs including:
570 * - http://example.com/
571 * - //example.com
572 * - /directory/
573 * - ?query-param
574 * - #target
575 * - mailto:foo@example.com
576 *
577 * Any further validation will be handled on the server when the setting is attempted to be saved,
578 * so this pattern does not need to be complete.
579 */
580 urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/;
581 if ( ! urlRegex.test( url ) || '' === itemName.val() ) {
582 if ( ! urlRegex.test( url ) ) {
583 itemUrl.addClass( 'invalid' )
584 .attr( 'aria-invalid', 'true' )
585 .attr( 'aria-describedby', 'custom-url-error' );
586 urlErrorMessage.show();
587 errorText = urlErrorMessage.text();
588 // Announce error message via screen reader
589 wp.a11y.speak( errorText, 'assertive' );
590 }
591 if ( '' === itemName.val() ) {
592 itemName.addClass( 'invalid' )
593 .attr( 'aria-invalid', 'true' )
594 .attr( 'aria-describedby', 'custom-name-error' );
595 nameErrorMessage.show();
596 errorText = ( '' === errorText ) ? nameErrorMessage.text() : errorText + nameErrorMessage.text();
597 // Announce error message via screen reader
598 wp.a11y.speak( errorText, 'assertive' );
599 }
600 return;
601 }
602
603 urlErrorMessage.hide();
604 nameErrorMessage.hide();
605 itemName.removeClass( 'invalid' )
606 .removeAttr( 'aria-invalid', 'true' )
607 .removeAttr( 'aria-describedby', 'custom-name-error' );
608 itemUrl.removeClass( 'invalid' )
609 .removeAttr( 'aria-invalid', 'true' )
610 .removeAttr( 'aria-describedby', 'custom-name-error' );
611
612 menuItem = {
613 'title': itemName.val(),
614 'url': url,
615 'type': 'custom',
616 'type_label': api.Menus.data.l10n.custom_label,
617 'object': 'custom'
618 };
619
620 this.currentMenuControl.addItemToMenu( menuItem );
621
622 // Reset the custom link form.
623 itemUrl.val( '' ).attr( 'placeholder', 'https://' );
624 itemName.val( '' );
625 },
626
627 /**
628 * Submit handler for keypress (enter) on field and click on button.
629 *
630 * @since 4.7.0
631 * @private
632 *
633 * @param {jQuery.Event} event Event.
634 * @return {void}
635 */
636 _submitNew: function( event ) {
637 var container;
638
639 // Only proceed with keypress if it is Enter.
640 if ( 'keypress' === event.type && 13 !== event.which ) {
641 return;
642 }
643
644 if ( this.addingNew ) {
645 return;
646 }
647
648 container = $( event.target ).closest( '.accordion-section' );
649
650 this.submitNew( container );
651 },
652
653 /**
654 * Creates a new object and adds an associated menu item to the menu.
655 *
656 * @since 4.7.0
657 * @private
658 *
659 * @param {jQuery} container
660 * @return {void}
661 */
662 submitNew: function( container ) {
663 var panel = this,
664 itemName = container.find( '.create-item-input' ),
665 title = itemName.val(),
666 dataContainer = container.find( '.available-menu-items-list' ),
667 itemType = dataContainer.data( 'type' ),
668 itemObject = dataContainer.data( 'object' ),
669 itemTypeLabel = dataContainer.data( 'type_label' ),
670 inputError = container.find('.create-item-error'),
671 promise;
672
673 if ( ! this.currentMenuControl ) {
674 return;
675 }
676
677 // Only posts are supported currently.
678 if ( 'post_type' !== itemType ) {
679 return;
680 }
681 if ( '' === itemName.val().trim() ) {
682 container.addClass( 'form-invalid' );
683 itemName.attr('aria-invalid', 'true');
684 itemName.attr('aria-describedby', inputError.attr('id'));
685 inputError.slideDown( 'fast' );
686 wp.a11y.speak( inputError.text() );
687 return;
688 } else {
689 container.removeClass( 'form-invalid' );
690 itemName.attr('aria-invalid', 'false');
691 itemName.removeAttr('aria-describedby');
692 inputError.hide();
693 container.find( '.accordion-section-title' ).addClass( 'loading' );
694 }
695
696 panel.addingNew = true;
697 itemName.attr( 'disabled', 'disabled' );
698 promise = api.Menus.insertAutoDraftPost( {
699 post_title: title,
700 post_type: itemObject
701 } );
702 promise.done( function( data ) {
703 var availableItem, $content, itemElement;
704 availableItem = new api.Menus.AvailableItemModel( {
705 'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
706 'title': itemName.val(),
707 'type': itemType,
708 'type_label': itemTypeLabel,
709 'object': itemObject,
710 'object_id': data.post_id,
711 'url': data.url
712 } );
713
714 // Add new item to menu.
715 panel.currentMenuControl.addItemToMenu( availableItem.attributes );
716
717 // Add the new item to the list of available items.
718 api.Menus.availableMenuItemsPanel.collection.add( availableItem );
719 $content = container.find( '.available-menu-items-list' );
720 itemElement = $( wp.template( 'available-menu-item' )( availableItem.attributes ) );
721 itemElement.find( '.menu-item-handle:first' ).addClass( 'item-added' );
722 $content.prepend( itemElement );
723 $content.scrollTop();
724
725 // Reset the create content form.
726 itemName.val( '' ).removeAttr( 'disabled' );
727 panel.addingNew = false;
728 container.find( '.accordion-section-title' ).removeClass( 'loading' );
729 } );
730 },
731
732 // Opens the panel.
733 open: function( menuControl ) {
734 var panel = this, close;
735
736 this.currentMenuControl = menuControl;
737
738 this.itemSectionHeight();
739
740 if ( api.section.has( 'publish_settings' ) ) {
741 api.section( 'publish_settings' ).collapse();
742 }
743
744 $( 'body' ).addClass( 'adding-menu-items' );
745
746 close = function() {
747 panel.close();
748 $( this ).off( 'click', close );
749 };
750 $( '#customize-preview' ).on( 'click', close );
751
752 // Collapse all controls.
753 _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) {
754 control.collapseForm();
755 } );
756
757 this.$el.find( '.selected' ).removeClass( 'selected' );
758
759 this.$search.trigger( 'focus' );
760 },
761
762 // Closes the panel.
763 close: function( options ) {
764 options = options || {};
765
766 if ( options.returnFocus && this.currentMenuControl ) {
767 this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
768 }
769
770 this.currentMenuControl = null;
771 this.selected = null;
772
773 $( 'body' ).removeClass( 'adding-menu-items' );
774 $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' );
775
776 this.$search.val( '' ).trigger( 'input' );
777 },
778
779 // Add a few keyboard enhancements to the panel.
780 keyboardAccessible: function( event ) {
781 var isEnter = ( 13 === event.which ),
782 isEsc = ( 27 === event.which ),
783 isBackTab = ( 9 === event.which && event.shiftKey ),
784 isSearchFocused = $( event.target ).is( this.$search );
785
786 // If enter pressed but nothing entered, don't do anything.
787 if ( isEnter && ! this.$search.val() ) {
788 return;
789 }
790
791 if ( isSearchFocused && isBackTab ) {
792 this.currentMenuControl.container.find( '.add-new-menu-item' ).focus();
793 event.preventDefault(); // Avoid additional back-tab.
794 } else if ( isEsc ) {
795 this.close( { returnFocus: true } );
796 }
797 }
798 });
799
800 /**
801 * wp.customize.Menus.MenusPanel
802 *
803 * Customizer panel for menus. This is used only for screen options management.
804 * Note that 'menus' must match the WP_Customize_Menu_Panel::$type.
805 *
806 * @class wp.customize.Menus.MenusPanel
807 * @augments wp.customize.Panel
808 */
809 api.Menus.MenusPanel = api.Panel.extend(/** @lends wp.customize.Menus.MenusPanel.prototype */{
810
811 attachEvents: function() {
812 api.Panel.prototype.attachEvents.call( this );
813
814 var panel = this,
815 panelMeta = panel.container.find( '.panel-meta' ),
816 help = panelMeta.find( '.customize-help-toggle' ),
817 content = panelMeta.find( '.customize-panel-description' ),
818 options = $( '#screen-options-wrap' ),
819 button = panelMeta.find( '.customize-screen-options-toggle' );
820 button.on( 'click keydown', function( event ) {
821 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
822 return;
823 }
824 event.preventDefault();
825
826 // Hide description.
827 if ( content.not( ':hidden' ) ) {
828 content.slideUp( 'fast' );
829 help.attr( 'aria-expanded', 'false' );
830 }
831
832 if ( 'true' === button.attr( 'aria-expanded' ) ) {
833 button.attr( 'aria-expanded', 'false' );
834 panelMeta.removeClass( 'open' );
835 panelMeta.removeClass( 'active-menu-screen-options' );
836 options.slideUp( 'fast' );
837 } else {
838 button.attr( 'aria-expanded', 'true' );
839 panelMeta.addClass( 'open' );
840 panelMeta.addClass( 'active-menu-screen-options' );
841 options.slideDown( 'fast' );
842 }
843
844 return false;
845 } );
846
847 // Help toggle.
848 help.on( 'click keydown', function( event ) {
849 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
850 return;
851 }
852 event.preventDefault();
853
854 if ( 'true' === button.attr( 'aria-expanded' ) ) {
855 button.attr( 'aria-expanded', 'false' );
856 help.attr( 'aria-expanded', 'true' );
857 panelMeta.addClass( 'open' );
858 panelMeta.removeClass( 'active-menu-screen-options' );
859 options.slideUp( 'fast' );
860 content.slideDown( 'fast' );
861 }
862 } );
863 },
864
865 /**
866 * Update field visibility when clicking on the field toggles.
867 */
868 ready: function() {
869 var panel = this;
870 panel.container.find( '.hide-column-tog' ).on( 'click', function() {
871 panel.saveManageColumnsState();
872 });
873
874 // Inject additional heading into the menu locations section's head container.
875 api.section( 'menu_locations', function( section ) {
876 section.headContainer.prepend(
877 wp.template( 'nav-menu-locations-header' )( api.Menus.data )
878 );
879 } );
880 },
881
882 /**
883 * Save hidden column states.
884 *
885 * @since 4.3.0
886 * @private
887 *
888 * @return {void}
889 */
890 saveManageColumnsState: _.debounce( function() {
891 var panel = this;
892 if ( panel._updateHiddenColumnsRequest ) {
893 panel._updateHiddenColumnsRequest.abort();
894 }
895
896 panel._updateHiddenColumnsRequest = wp.ajax.post( 'hidden-columns', {
897 hidden: panel.hidden(),
898 screenoptionnonce: $( '#screenoptionnonce' ).val(),
899 page: 'nav-menus'
900 } );
901 panel._updateHiddenColumnsRequest.always( function() {
902 panel._updateHiddenColumnsRequest = null;
903 } );
904 }, 2000 ),
905
906 /**
907 * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
908 */
909 checked: function() {},
910
911 /**
912 * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers.
913 */
914 unchecked: function() {},
915
916 /**
917 * Get hidden fields.
918 *
919 * @since 4.3.0
920 * @private
921 *
922 * @return {Array} Fields (columns) that are hidden.
923 */
924 hidden: function() {
925 return $( '.hide-column-tog' ).not( ':checked' ).map( function() {
926 var id = this.id;
927 return id.substring( 0, id.length - 5 );
928 }).get().join( ',' );
929 }
930 } );
931
932 /**
933 * wp.customize.Menus.MenuSection
934 *
935 * Customizer section for menus. This is used only for lazy-loading child controls.
936 * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type.
937 *
938 * @class wp.customize.Menus.MenuSection
939 * @augments wp.customize.Section
940 */
941 api.Menus.MenuSection = api.Section.extend(/** @lends wp.customize.Menus.MenuSection.prototype */{
942
943 /**
944 * Initialize.
945 *
946 * @since 4.3.0
947 *
948 * @param {string} id
949 * @param {Object} options
950 */
951 initialize: function( id, options ) {
952 var section = this;
953 api.Section.prototype.initialize.call( section, id, options );
954 section.deferred.initSortables = $.Deferred();
955 },
956
957 /**
958 * Ready.
959 */
960 ready: function() {
961 var section = this, fieldActiveToggles, handleFieldActiveToggle;
962
963 if ( 'undefined' === typeof section.params.menu_id ) {
964 throw new Error( 'params.menu_id was not defined' );
965 }
966
967 /*
968 * Since newly created sections won't be registered in PHP, we need to prevent the
969 * preview's sending of the activeSections to result in this control
970 * being deactivated when the preview refreshes. So we can hook onto
971 * the setting that has the same ID and its presence can dictate
972 * whether the section is active.
973 */
974 section.active.validate = function() {
975 if ( ! api.has( section.id ) ) {
976 return false;
977 }
978 return !! api( section.id ).get();
979 };
980
981 section.populateControls();
982
983 section.navMenuLocationSettings = {};
984 section.assignedLocations = new api.Value( [] );
985
986 api.each(function( setting, id ) {
987 var matches = id.match( /^nav_menu_locations\[(.+?)]/ );
988 if ( matches ) {
989 section.navMenuLocationSettings[ matches[1] ] = setting;
990 setting.bind( function() {
991 section.refreshAssignedLocations();
992 });
993 }
994 });
995
996 section.assignedLocations.bind(function( to ) {
997 section.updateAssignedLocationsInSectionTitle( to );
998 });
999
1000 section.refreshAssignedLocations();
1001
1002 api.bind( 'pane-contents-reflowed', function() {
1003 // Skip menus that have been removed.
1004 if ( ! section.contentContainer.parent().length ) {
1005 return;
1006 }
1007 section.container.find( '.menu-item .menu-item-reorder-nav button' ).attr({ 'tabindex': '0', 'aria-hidden': 'false' });
1008 section.container.find( '.menu-item.move-up-disabled .menus-move-up' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
1009 section.container.find( '.menu-item.move-down-disabled .menus-move-down' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
1010 section.container.find( '.menu-item.move-left-disabled .menus-move-left' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
1011 section.container.find( '.menu-item.move-right-disabled .menus-move-right' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
1012 } );
1013
1014 /**
1015 * Update the active field class for the content container for a given checkbox toggle.
1016 *
1017 * @this {jQuery}
1018 * @return {void}
1019 */
1020 handleFieldActiveToggle = function() {
1021 var className = 'field-' + $( this ).val() + '-active';
1022 section.contentContainer.toggleClass( className, $( this ).prop( 'checked' ) );
1023 };
1024 fieldActiveToggles = api.panel( 'nav_menus' ).contentContainer.find( '.metabox-prefs:first' ).find( '.hide-column-tog' );
1025 fieldActiveToggles.each( handleFieldActiveToggle );
1026 fieldActiveToggles.on( 'click', handleFieldActiveToggle );
1027 },
1028
1029 populateControls: function() {
1030 var section = this,
1031 menuNameControlId,
1032 menuLocationsControlId,
1033 menuAutoAddControlId,
1034 menuDeleteControlId,
1035 menuControl,
1036 menuNameControl,
1037 menuLocationsControl,
1038 menuAutoAddControl,
1039 menuDeleteControl;
1040
1041 // Add the control for managing the menu name.
1042 menuNameControlId = section.id + '[name]';
1043 menuNameControl = api.control( menuNameControlId );
1044 if ( ! menuNameControl ) {
1045 menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
1046 type: 'nav_menu_name',
1047 label: api.Menus.data.l10n.menuNameLabel,
1048 section: section.id,
1049 priority: 0,
1050 settings: {
1051 'default': section.id
1052 }
1053 } );
1054 api.control.add( menuNameControl );
1055 menuNameControl.active.set( true );
1056 }
1057
1058 // Add the menu control.
1059 menuControl = api.control( section.id );
1060 if ( ! menuControl ) {
1061 menuControl = new api.controlConstructor.nav_menu( section.id, {
1062 type: 'nav_menu',
1063 section: section.id,
1064 priority: 998,
1065 settings: {
1066 'default': section.id
1067 },
1068 menu_id: section.params.menu_id
1069 } );
1070 api.control.add( menuControl );
1071 menuControl.active.set( true );
1072 }
1073
1074 // Add the menu locations control.
1075 menuLocationsControlId = section.id + '[locations]';
1076 menuLocationsControl = api.control( menuLocationsControlId );
1077 if ( ! menuLocationsControl ) {
1078 menuLocationsControl = new api.controlConstructor.nav_menu_locations( menuLocationsControlId, {
1079 section: section.id,
1080 priority: 999,
1081 settings: {
1082 'default': section.id
1083 },
1084 menu_id: section.params.menu_id
1085 } );
1086 api.control.add( menuLocationsControl.id, menuLocationsControl );
1087 menuControl.active.set( true );
1088 }
1089
1090 // Add the control for managing the menu auto_add.
1091 menuAutoAddControlId = section.id + '[auto_add]';
1092 menuAutoAddControl = api.control( menuAutoAddControlId );
1093 if ( ! menuAutoAddControl ) {
1094 menuAutoAddControl = new api.controlConstructor.nav_menu_auto_add( menuAutoAddControlId, {
1095 type: 'nav_menu_auto_add',
1096 label: '',
1097 section: section.id,
1098 priority: 1000,
1099 settings: {
1100 'default': section.id
1101 }
1102 } );
1103 api.control.add( menuAutoAddControl );
1104 menuAutoAddControl.active.set( true );
1105 }
1106
1107 // Add the control for deleting the menu.
1108 menuDeleteControlId = section.id + '[delete]';
1109 menuDeleteControl = api.control( menuDeleteControlId );
1110 if ( ! menuDeleteControl ) {
1111 menuDeleteControl = new api.Control( menuDeleteControlId, {
1112 section: section.id,
1113 priority: 1001,
1114 templateId: 'nav-menu-delete-button'
1115 } );
1116 api.control.add( menuDeleteControl.id, menuDeleteControl );
1117 menuDeleteControl.active.set( true );
1118 menuDeleteControl.deferred.embedded.done( function () {
1119 menuDeleteControl.container.find( 'button' ).on( 'click', function() {
1120 var menuId = section.params.menu_id;
1121 var menuControl = api.Menus.getMenuControl( menuId );
1122 menuControl.setting.set( false );
1123 });
1124 } );
1125 }
1126 },
1127
1128 /**
1129 *
1130 */
1131 refreshAssignedLocations: function() {
1132 var section = this,
1133 menuTermId = section.params.menu_id,
1134 currentAssignedLocations = [];
1135 _.each( section.navMenuLocationSettings, function( setting, themeLocation ) {
1136 if ( setting() === menuTermId ) {
1137 currentAssignedLocations.push( themeLocation );
1138 }
1139 });
1140 section.assignedLocations.set( currentAssignedLocations );
1141 },
1142
1143 /**
1144 * @param {Array} themeLocationSlugs Theme location slugs.
1145 */
1146 updateAssignedLocationsInSectionTitle: function( themeLocationSlugs ) {
1147 var section = this,
1148 $title;
1149
1150 $title = section.container.find( '.accordion-section-title button:first' );
1151 $title.find( '.menu-in-location' ).remove();
1152 _.each( themeLocationSlugs, function( themeLocationSlug ) {
1153 var $label, locationName;
1154 $label = $( '<span class="menu-in-location"></span>' );
1155 locationName = api.Menus.data.locationSlugMappedToName[ themeLocationSlug ];
1156 $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', locationName ) );
1157 $title.append( $label );
1158 });
1159
1160 section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocationSlugs.length );
1161
1162 },
1163
1164 onChangeExpanded: function( expanded, args ) {
1165 var section = this, completeCallback;
1166
1167 if ( expanded ) {
1168 wpNavMenu.menuList = section.contentContainer;
1169 wpNavMenu.targetList = wpNavMenu.menuList;
1170
1171 // Add attributes needed by wpNavMenu.
1172 $( '#menu-to-edit' ).removeAttr( 'id' );
1173 wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' );
1174
1175 api.Menus.MenuItemControl.prototype.initAccessibility();
1176
1177 _.each( api.section( section.id ).controls(), function( control ) {
1178 if ( 'nav_menu_item' === control.params.type ) {
1179 control.actuallyEmbed();
1180 }
1181 } );
1182
1183 // Make sure Sortables is initialized after the section has been expanded to prevent `offset` issues.
1184 if ( args.completeCallback ) {
1185 completeCallback = args.completeCallback;
1186 }
1187 args.completeCallback = function() {
1188 if ( 'resolved' !== section.deferred.initSortables.state() ) {
1189 wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above.
1190 section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable.
1191
1192 // @todo Note that wp.customize.reflowPaneContents() is debounced,
1193 // so this immediate change will show a slight flicker while priorities get updated.
1194 api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems();
1195 }
1196 if ( _.isFunction( completeCallback ) ) {
1197 completeCallback();
1198 }
1199 };
1200 }
1201 api.Section.prototype.onChangeExpanded.call( section, expanded, args );
1202 },
1203
1204 /**
1205 * Highlight how a user may create new menu items.
1206 *
1207 * This method reminds the user to create new menu items and how.
1208 * It's exposed this way because this class knows best which UI needs
1209 * highlighted but those expanding this section know more about why and
1210 * when the affordance should be highlighted.
1211 *
1212 * @since 4.9.0
1213 *
1214 * @return {void}
1215 */
1216 highlightNewItemButton: function() {
1217 api.utils.highlightButton( this.contentContainer.find( '.add-new-menu-item' ), { delay: 2000 } );
1218 }
1219 });
1220
1221 /**
1222 * Create a nav menu setting and section.
1223 *
1224 * @since 4.9.0
1225 *
1226 * @param {string} [name=''] Nav menu name.
1227 * @return {wp.customize.Menus.MenuSection} Added nav menu.
1228 */
1229 api.Menus.createNavMenu = function createNavMenu( name ) {
1230 var customizeId, placeholderId, setting;
1231 placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
1232
1233 customizeId = 'nav_menu[' + String( placeholderId ) + ']';
1234
1235 // Register the menu control setting.
1236 setting = api.create( customizeId, customizeId, {}, {
1237 type: 'nav_menu',
1238 transport: api.Menus.data.settingTransport,
1239 previewer: api.previewer
1240 } );
1241 setting.set( $.extend(
1242 {},
1243 api.Menus.data.defaultSettingValues.nav_menu,
1244 {
1245 name: name || ''
1246 }
1247 ) );
1248
1249 /*
1250 * Add the menu section (and its controls).
1251 * Note that this will automatically create the required controls
1252 * inside via the Section's ready method.
1253 */
1254 return api.section.add( new api.Menus.MenuSection( customizeId, {
1255 panel: 'nav_menus',
1256 title: displayNavMenuName( name ),
1257 customizeAction: api.Menus.data.l10n.customizingMenus,
1258 priority: 10,
1259 menu_id: placeholderId
1260 } ) );
1261 };
1262
1263 /**
1264 * wp.customize.Menus.NewMenuSection
1265 *
1266 * Customizer section for new menus.
1267 *
1268 * @class wp.customize.Menus.NewMenuSection
1269 * @augments wp.customize.Section
1270 */
1271 api.Menus.NewMenuSection = api.Section.extend(/** @lends wp.customize.Menus.NewMenuSection.prototype */{
1272
1273 /**
1274 * Add behaviors for the accordion section.
1275 *
1276 * @since 4.3.0
1277 */
1278 attachEvents: function() {
1279 var section = this,
1280 container = section.container,
1281 contentContainer = section.contentContainer,
1282 navMenuSettingPattern = /^nav_menu\[/;
1283
1284 section.headContainer.find( '.accordion-section-title' ).replaceWith(
1285 wp.template( 'nav-menu-create-menu-section-title' )
1286 );
1287
1288 /*
1289 * We have to manually handle section expanded because we do not
1290 * apply the `accordion-section-title` class to this button-driven section.
1291 */
1292 container.on( 'click', '.customize-add-menu-button', function() {
1293 section.expand();
1294 });
1295
1296 contentContainer.on( 'keydown', '.menu-name-field', function( event ) {
1297 if ( 13 === event.which ) { // Enter.
1298 section.submit();
1299 }
1300 } );
1301 contentContainer.on( 'click', '#customize-new-menu-submit', function( event ) {
1302 section.submit();
1303 event.stopPropagation();
1304 event.preventDefault();
1305 } );
1306
1307 /**
1308 * Get number of non-deleted nav menus.
1309 *
1310 * @since 4.9.0
1311 * @return {number} Count.
1312 */
1313 function getNavMenuCount() {
1314 var count = 0;
1315 api.each( function( setting ) {
1316 if ( navMenuSettingPattern.test( setting.id ) && false !== setting.get() ) {
1317 count += 1;
1318 }
1319 } );
1320 return count;
1321 }
1322
1323 /**
1324 * Update visibility of notice to prompt users to create menus.
1325 *
1326 * @since 4.9.0
1327 * @return {void}
1328 */
1329 function updateNoticeVisibility() {
1330 container.find( '.add-new-menu-notice' ).prop( 'hidden', getNavMenuCount() > 0 );
1331 }
1332
1333 /**
1334 * Handle setting addition.
1335 *
1336 * @since 4.9.0
1337 * @param {wp.customize.Setting} setting - Added setting.
1338 * @return {void}
1339 */
1340 function addChangeEventListener( setting ) {
1341 if ( navMenuSettingPattern.test( setting.id ) ) {
1342 setting.bind( updateNoticeVisibility );
1343 updateNoticeVisibility();
1344 }
1345 }
1346
1347 /**
1348 * Handle setting removal.
1349 *
1350 * @since 4.9.0
1351 * @param {wp.customize.Setting} setting - Removed setting.
1352 * @return {void}
1353 */
1354 function removeChangeEventListener( setting ) {
1355 if ( navMenuSettingPattern.test( setting.id ) ) {
1356 setting.unbind( updateNoticeVisibility );
1357 updateNoticeVisibility();
1358 }
1359 }
1360
1361 api.each( addChangeEventListener );
1362 api.bind( 'add', addChangeEventListener );
1363 api.bind( 'removed', removeChangeEventListener );
1364 updateNoticeVisibility();
1365
1366 api.Section.prototype.attachEvents.apply( section, arguments );
1367 },
1368
1369 /**
1370 * Set up the control.
1371 *
1372 * @since 4.9.0
1373 */
1374 ready: function() {
1375 this.populateControls();
1376 },
1377
1378 /**
1379 * Create the controls for this section.
1380 *
1381 * @since 4.9.0
1382 */
1383 populateControls: function() {
1384 var section = this,
1385 menuNameControlId,
1386 menuLocationsControlId,
1387 newMenuSubmitControlId,
1388 menuNameControl,
1389 menuLocationsControl,
1390 newMenuSubmitControl;
1391
1392 menuNameControlId = section.id + '[name]';
1393 menuNameControl = api.control( menuNameControlId );
1394 if ( ! menuNameControl ) {
1395 menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, {
1396 label: api.Menus.data.l10n.menuNameLabel,
1397 description: api.Menus.data.l10n.newMenuNameDescription,
1398 section: section.id,
1399 priority: 0
1400 } );
1401 api.control.add( menuNameControl.id, menuNameControl );
1402 menuNameControl.active.set( true );
1403 }
1404
1405 menuLocationsControlId = section.id + '[locations]';
1406 menuLocationsControl = api.control( menuLocationsControlId );
1407 if ( ! menuLocationsControl ) {
1408 menuLocationsControl = new api.controlConstructor.nav_menu_locations( menuLocationsControlId, {
1409 section: section.id,
1410 priority: 1,
1411 menu_id: '',
1412 isCreating: true
1413 } );
1414 api.control.add( menuLocationsControlId, menuLocationsControl );
1415 menuLocationsControl.active.set( true );
1416 }
1417
1418 newMenuSubmitControlId = section.id + '[submit]';
1419 newMenuSubmitControl = api.control( newMenuSubmitControlId );
1420 if ( !newMenuSubmitControl ) {
1421 newMenuSubmitControl = new api.Control( newMenuSubmitControlId, {
1422 section: section.id,
1423 priority: 1,
1424 templateId: 'nav-menu-submit-new-button'
1425 } );
1426 api.control.add( newMenuSubmitControlId, newMenuSubmitControl );
1427 newMenuSubmitControl.active.set( true );
1428 }
1429 },
1430
1431 /**
1432 * Create the new menu with name and location supplied by the user.
1433 *
1434 * @since 4.9.0
1435 */
1436 submit: function() {
1437 var section = this,
1438 contentContainer = section.contentContainer,
1439 nameInput = contentContainer.find( '.menu-name-field' ).first(),
1440 name = nameInput.val(),
1441 menuSection;
1442
1443 if ( ! name ) {
1444 nameInput.addClass( 'invalid' );
1445 nameInput.focus();
1446 return;
1447 }
1448
1449 menuSection = api.Menus.createNavMenu( name );
1450
1451 // Clear name field.
1452 nameInput.val( '' );
1453 nameInput.removeClass( 'invalid' );
1454
1455 contentContainer.find( '.assigned-menu-location input[type=checkbox]' ).each( function() {
1456 var checkbox = $( this ),
1457 navMenuLocationSetting;
1458
1459 if ( checkbox.prop( 'checked' ) ) {
1460 navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' );
1461 navMenuLocationSetting.set( menuSection.params.menu_id );
1462
1463 // Reset state for next new menu.
1464 checkbox.prop( 'checked', false );
1465 }
1466 } );
1467
1468 wp.a11y.speak( api.Menus.data.l10n.menuAdded );
1469
1470 // Focus on the new menu section.
1471 menuSection.focus( {
1472 completeCallback: function() {
1473 menuSection.highlightNewItemButton();
1474 }
1475 } );
1476 },
1477
1478 /**
1479 * Select a default location.
1480 *
1481 * This method selects a single location by default so we can support
1482 * creating a menu for a specific menu location.
1483 *
1484 * @since 4.9.0
1485 *
1486 * @param {string|null} locationId - The ID of the location to select. `null` clears all selections.
1487 * @return {void}
1488 */
1489 selectDefaultLocation: function( locationId ) {
1490 var locationControl = api.control( this.id + '[locations]' ),
1491 locationSelections = {};
1492
1493 if ( locationId !== null ) {
1494 locationSelections[ locationId ] = true;
1495 }
1496
1497 locationControl.setSelections( locationSelections );
1498 }
1499 });
1500
1501 /**
1502 * wp.customize.Menus.MenuLocationControl
1503 *
1504 * Customizer control for menu locations (rendered as a <select>).
1505 * Note that 'nav_menu_location' must match the WP_Customize_Nav_Menu_Location_Control::$type.
1506 *
1507 * @class wp.customize.Menus.MenuLocationControl
1508 * @augments wp.customize.Control
1509 */
1510 api.Menus.MenuLocationControl = api.Control.extend(/** @lends wp.customize.Menus.MenuLocationControl.prototype */{
1511 initialize: function( id, options ) {
1512 var control = this,
1513 matches = id.match( /^nav_menu_locations\[(.+?)]/ );
1514 control.themeLocation = matches[1];
1515 api.Control.prototype.initialize.call( control, id, options );
1516 },
1517
1518 ready: function() {
1519 var control = this, navMenuIdRegex = /^nav_menu\[(-?\d+)]/;
1520
1521 // @todo It would be better if this was added directly on the setting itself, as opposed to the control.
1522 control.setting.validate = function( value ) {
1523 if ( '' === value ) {
1524 return 0;
1525 } else {
1526 return parseInt( value, 10 );
1527 }
1528 };
1529
1530 // Create and Edit menu buttons.
1531 control.container.find( '.create-menu' ).on( 'click', function() {
1532 var addMenuSection = api.section( 'add_menu' );
1533 addMenuSection.selectDefaultLocation( this.dataset.locationId );
1534 addMenuSection.focus();
1535 } );
1536 control.container.find( '.edit-menu' ).on( 'click', function() {
1537 var menuId = control.setting();
1538 api.section( 'nav_menu[' + menuId + ']' ).focus();
1539 });
1540 control.setting.bind( 'change', function() {
1541 var menuIsSelected = 0 !== control.setting();
1542 control.container.find( '.create-menu' ).toggleClass( 'hidden', menuIsSelected );
1543 control.container.find( '.edit-menu' ).toggleClass( 'hidden', ! menuIsSelected );
1544 });
1545
1546 // Add/remove menus from the available options when they are added and removed.
1547 api.bind( 'add', function( setting ) {
1548 var option, menuId, matches = setting.id.match( navMenuIdRegex );
1549 if ( ! matches || false === setting() ) {
1550 return;
1551 }
1552 menuId = matches[1];
1553 option = new Option( displayNavMenuName( setting().name ), menuId );
1554 control.container.find( 'select' ).append( option );
1555 });
1556 api.bind( 'remove', function( setting ) {
1557 var menuId, matches = setting.id.match( navMenuIdRegex );
1558 if ( ! matches ) {
1559 return;
1560 }
1561 menuId = parseInt( matches[1], 10 );
1562 if ( control.setting() === menuId ) {
1563 control.setting.set( '' );
1564 }
1565 control.container.find( 'option[value=' + menuId + ']' ).remove();
1566 });
1567 api.bind( 'change', function( setting ) {
1568 var menuId, matches = setting.id.match( navMenuIdRegex );
1569 if ( ! matches ) {
1570 return;
1571 }
1572 menuId = parseInt( matches[1], 10 );
1573 if ( false === setting() ) {
1574 if ( control.setting() === menuId ) {
1575 control.setting.set( '' );
1576 }
1577 control.container.find( 'option[value=' + menuId + ']' ).remove();
1578 } else {
1579 control.container.find( 'option[value=' + menuId + ']' ).text( displayNavMenuName( setting().name ) );
1580 }
1581 });
1582 }
1583 });
1584
1585 api.Menus.MenuItemControl = api.Control.extend(/** @lends wp.customize.Menus.MenuItemControl.prototype */{
1586
1587 /**
1588 * wp.customize.Menus.MenuItemControl
1589 *
1590 * Customizer control for menu items.
1591 * Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type.
1592 *
1593 * @constructs wp.customize.Menus.MenuItemControl
1594 * @augments wp.customize.Control
1595 *
1596 * @inheritDoc
1597 */
1598 initialize: function( id, options ) {
1599 var control = this;
1600 control.expanded = new api.Value( false );
1601 control.expandedArgumentsQueue = [];
1602 control.expanded.bind( function( expanded ) {
1603 var args = control.expandedArgumentsQueue.shift();
1604 args = $.extend( {}, control.defaultExpandedArguments, args );
1605 control.onChangeExpanded( expanded, args );
1606 });
1607 api.Control.prototype.initialize.call( control, id, options );
1608 control.active.validate = function() {
1609 var value, section = api.section( control.section() );
1610 if ( section ) {
1611 value = section.active();
1612 } else {
1613 value = false;
1614 }
1615 return value;
1616 };
1617 },
1618
1619 /**
1620 * Set up the initial state of the screen reader accessibility information for menu items.
1621 *
1622 * @since 6.6.0
1623 */
1624 initAccessibility: function() {
1625 var control = this,
1626 menu = $( '#menu-to-edit' );
1627
1628 // Refresh the accessibility when the user comes close to the item in any way.
1629 menu.on( 'mouseenter.refreshAccessibility focus.refreshAccessibility touchstart.refreshAccessibility', '.menu-item', function(){
1630 control.refreshAdvancedAccessibilityOfItem( $( this ).find( 'button.item-edit' ) );
1631 } );
1632
1633 // We have to update on click as well because we might hover first, change the item, and then click.
1634 menu.on( 'click', 'button.item-edit', function() {
1635 control.refreshAdvancedAccessibilityOfItem( $( this ) );
1636 } );
1637 },
1638
1639 /**
1640 * refreshAdvancedAccessibilityOfItem( [itemToRefresh] )
1641 *
1642 * Refreshes advanced accessibility buttons for one menu item.
1643 * Shows or hides buttons based on the location of the menu item.
1644 *
1645 * @param {Object} itemToRefresh The menu item that might need its advanced accessibility buttons refreshed
1646 *
1647 * @since 6.6.0
1648 */
1649 refreshAdvancedAccessibilityOfItem: function( itemToRefresh ) {
1650 // Only refresh accessibility when necessary.
1651 if ( true !== $( itemToRefresh ).data( 'needs_accessibility_refresh' ) ) {
1652 return;
1653 }
1654
1655 var primaryItems, itemPosition, title,
1656 parentItem, parentItemId, parentItemName, subItems, totalSubItems,
1657 $this = $( itemToRefresh ),
1658 menuItem = $this.closest( 'li.menu-item' ).first(),
1659 depth = menuItem.menuItemDepth(),
1660 isPrimaryMenuItem = ( 0 === depth ),
1661 itemName = $this.closest( '.menu-item-handle' ).find( '.menu-item-title' ).text(),
1662 menuItemType = $this.closest( '.menu-item-handle' ).find( '.item-type' ).text(),
1663 totalMenuItems = $( '#menu-to-edit li' ).length;
1664
1665 if ( isPrimaryMenuItem ) {
1666 primaryItems = $( '.menu-item-depth-0' ),
1667 itemPosition = primaryItems.index( menuItem ) + 1,
1668 totalMenuItems = primaryItems.length,
1669 // String together help text for primary menu items.
1670 title = menus.menuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalMenuItems );
1671 } else {
1672 parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(),
1673 parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(),
1674 parentItemName = parentItem.find( '.menu-item-title' ).text(),
1675 subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ),
1676 totalSubItems = subItems.length,
1677 itemPosition = $( subItems.parents( '.menu-item' ).get().reverse() ).index( menuItem ) + 1;
1678
1679 // String together help text for sub menu items.
1680 if ( depth < 2 ) {
1681 title = menus.subMenuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName );
1682 } else {
1683 title = menus.subMenuMoreDepthFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName ).replace( '%6$d', depth );
1684 }
1685 }
1686
1687 $this.find( '.screen-reader-text' ).text( title );
1688
1689 // Mark this item's accessibility as refreshed.
1690 $this.data( 'needs_accessibility_refresh', false );
1691 },
1692
1693 /**
1694 * Override the embed() method to do nothing,
1695 * so that the control isn't embedded on load,
1696 * unless the containing section is already expanded.
1697 *
1698 * @since 4.3.0
1699 */
1700 embed: function() {
1701 var control = this,
1702 sectionId = control.section(),
1703 section;
1704 if ( ! sectionId ) {
1705 return;
1706 }
1707 section = api.section( sectionId );
1708 if ( ( section && section.expanded() ) || api.settings.autofocus.control === control.id ) {
1709 control.actuallyEmbed();
1710 }
1711 },
1712
1713 /**
1714 * This function is called in Section.onChangeExpanded() so the control
1715 * will only get embedded when the Section is first expanded.
1716 *
1717 * @since 4.3.0
1718 */
1719 actuallyEmbed: function() {
1720 var control = this;
1721 if ( 'resolved' === control.deferred.embedded.state() ) {
1722 return;
1723 }
1724 control.renderContent();
1725 control.deferred.embedded.resolve(); // This triggers control.ready().
1726
1727 // Mark all menu items as unprocessed.
1728 $( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
1729 },
1730
1731 /**
1732 * Set up the control.
1733 */
1734 ready: function() {
1735 if ( 'undefined' === typeof this.params.menu_item_id ) {
1736 throw new Error( 'params.menu_item_id was not defined' );
1737 }
1738
1739 this._setupControlToggle();
1740 this._setupReorderUI();
1741 this._setupUpdateUI();
1742 this._setupRemoveUI();
1743 this._setupLinksUI();
1744 this._setupTitleUI();
1745 },
1746
1747 /**
1748 * Show/hide the settings when clicking on the menu item handle.
1749 */
1750 _setupControlToggle: function() {
1751 var control = this;
1752
1753 this.container.find( '.menu-item-handle' ).on( 'click', function( e ) {
1754 e.preventDefault();
1755 e.stopPropagation();
1756 var menuControl = control.getMenuControl(),
1757 isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
1758 isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
1759
1760 if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
1761 api.Menus.availableMenuItemsPanel.close();
1762 }
1763
1764 if ( menuControl.isReordering || menuControl.isSorting ) {
1765 return;
1766 }
1767 control.toggleForm();
1768 } );
1769 },
1770
1771 /**
1772 * Set up the menu-item-reorder-nav
1773 */
1774 _setupReorderUI: function() {
1775 var control = this, template, $reorderNav;
1776
1777 template = wp.template( 'menu-item-reorder-nav' );
1778
1779 // Add the menu item reordering elements to the menu item control.
1780 control.container.find( '.item-controls' ).after( template );
1781
1782 // Handle clicks for up/down/left-right on the reorder nav.
1783 $reorderNav = control.container.find( '.menu-item-reorder-nav' );
1784 $reorderNav.find( '.menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right' ).on( 'click', function() {
1785 var moveBtn = $( this );
1786 control.params.depth = control.getDepth();
1787
1788 moveBtn.focus();
1789
1790 var isMoveUp = moveBtn.is( '.menus-move-up' ),
1791 isMoveDown = moveBtn.is( '.menus-move-down' ),
1792 isMoveLeft = moveBtn.is( '.menus-move-left' ),
1793 isMoveRight = moveBtn.is( '.menus-move-right' );
1794
1795 if ( isMoveUp ) {
1796 control.moveUp();
1797 } else if ( isMoveDown ) {
1798 control.moveDown();
1799 } else if ( isMoveLeft ) {
1800 control.moveLeft();
1801 } else if ( isMoveRight ) {
1802 control.moveRight();
1803 control.params.depth += 1;
1804 }
1805
1806 moveBtn.focus(); // Re-focus after the container was moved.
1807
1808 // Mark all menu items as unprocessed.
1809 $( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
1810 } );
1811 },
1812
1813 /**
1814 * Set up event handlers for menu item updating.
1815 */
1816 _setupUpdateUI: function() {
1817 var control = this,
1818 settingValue = control.setting(),
1819 updateNotifications;
1820
1821 control.elements = {};
1822 control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) );
1823 control.elements.title = new api.Element( control.container.find( '.edit-menu-item-title' ) );
1824 control.elements.attr_title = new api.Element( control.container.find( '.edit-menu-item-attr-title' ) );
1825 control.elements.target = new api.Element( control.container.find( '.edit-menu-item-target' ) );
1826 control.elements.classes = new api.Element( control.container.find( '.edit-menu-item-classes' ) );
1827 control.elements.xfn = new api.Element( control.container.find( '.edit-menu-item-xfn' ) );
1828 control.elements.description = new api.Element( control.container.find( '.edit-menu-item-description' ) );
1829 // @todo Allow other elements, added by plugins, to be automatically picked up here;
1830 // allow additional values to be added to setting array.
1831
1832 _.each( control.elements, function( element, property ) {
1833 element.bind(function( value ) {
1834 if ( element.element.is( 'input[type=checkbox]' ) ) {
1835 value = ( value ) ? element.element.val() : '';
1836 }
1837
1838 var settingValue = control.setting();
1839 if ( settingValue && settingValue[ property ] !== value ) {
1840 settingValue = _.clone( settingValue );
1841 settingValue[ property ] = value;
1842 control.setting.set( settingValue );
1843 }
1844 });
1845 if ( settingValue ) {
1846 if ( ( property === 'classes' || property === 'xfn' ) && _.isArray( settingValue[ property ] ) ) {
1847 element.set( settingValue[ property ].join( ' ' ) );
1848 } else {
1849 element.set( settingValue[ property ] );
1850 }
1851 }
1852 });
1853
1854 control.setting.bind(function( to, from ) {
1855 var itemId = control.params.menu_item_id,
1856 followingSiblingItemControls = [],
1857 childrenItemControls = [],
1858 menuControl;
1859
1860 if ( false === to ) {
1861 menuControl = api.control( 'nav_menu[' + String( from.nav_menu_term_id ) + ']' );
1862 control.container.remove();
1863
1864 _.each( menuControl.getMenuItemControls(), function( otherControl ) {
1865 if ( from.menu_item_parent === otherControl.setting().menu_item_parent && otherControl.setting().position > from.position ) {
1866 followingSiblingItemControls.push( otherControl );
1867 } else if ( otherControl.setting().menu_item_parent === itemId ) {
1868 childrenItemControls.push( otherControl );
1869 }
1870 });
1871
1872 // Shift all following siblings by the number of children this item has.
1873 _.each( followingSiblingItemControls, function( followingSiblingItemControl ) {
1874 var value = _.clone( followingSiblingItemControl.setting() );
1875 value.position += childrenItemControls.length;
1876 followingSiblingItemControl.setting.set( value );
1877 });
1878
1879 // Now move the children up to be the new subsequent siblings.
1880 _.each( childrenItemControls, function( childrenItemControl, i ) {
1881 var value = _.clone( childrenItemControl.setting() );
1882 value.position = from.position + i;
1883 value.menu_item_parent = from.menu_item_parent;
1884 childrenItemControl.setting.set( value );
1885 });
1886
1887 menuControl.debouncedReflowMenuItems();
1888 } else {
1889 // Update the elements' values to match the new setting properties.
1890 _.each( to, function( value, key ) {
1891 if ( control.elements[ key] ) {
1892 control.elements[ key ].set( to[ key ] );
1893 }
1894 } );
1895 control.container.find( '.menu-item-data-parent-id' ).val( to.menu_item_parent );
1896
1897 // Handle UI updates when the position or depth (parent) change.
1898 if ( to.position !== from.position || to.menu_item_parent !== from.menu_item_parent ) {
1899 control.getMenuControl().debouncedReflowMenuItems();
1900 }
1901 }
1902 });
1903
1904 // Style the URL field as invalid when there is an invalid_url notification.
1905 updateNotifications = function() {
1906 control.elements.url.element.toggleClass( 'invalid', control.setting.notifications.has( 'invalid_url' ) );
1907 };
1908 control.setting.notifications.bind( 'add', updateNotifications );
1909 control.setting.notifications.bind( 'removed', updateNotifications );
1910 },
1911
1912 /**
1913 * Set up event handlers for menu item deletion.
1914 */
1915 _setupRemoveUI: function() {
1916 var control = this, $removeBtn;
1917
1918 // Configure delete button.
1919 $removeBtn = control.container.find( '.item-delete' );
1920
1921 $removeBtn.on( 'click', function() {
1922 // Find an adjacent element to add focus to when this menu item goes away.
1923 var addingItems = true, $adjacentFocusTarget, $next, $prev,
1924 instanceCounter = 0, // Instance count of the menu item deleted.
1925 deleteItemOriginalItemId = control.params.original_item_id,
1926 addedItems = control.getMenuControl().$sectionContent.find( '.menu-item' ),
1927 availableMenuItem;
1928
1929 if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
1930 addingItems = false;
1931 }
1932
1933 $next = control.container.nextAll( '.customize-control-nav_menu_item:visible' ).first();
1934 $prev = control.container.prevAll( '.customize-control-nav_menu_item:visible' ).first();
1935
1936 if ( $next.length ) {
1937 $adjacentFocusTarget = $next.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
1938 } else if ( $prev.length ) {
1939 $adjacentFocusTarget = $prev.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
1940 } else {
1941 $adjacentFocusTarget = control.container.nextAll( '.customize-control-nav_menu' ).find( '.add-new-menu-item' ).first();
1942 }
1943
1944 /*
1945 * If the menu item deleted is the only of its instance left,
1946 * remove the check icon of this menu item in the right panel.
1947 */
1948 _.each( addedItems, function( addedItem ) {
1949 var menuItemId, menuItemControl, matches;
1950
1951 // This is because menu item that's deleted is just hidden.
1952 if ( ! $( addedItem ).is( ':visible' ) ) {
1953 return;
1954 }
1955
1956 matches = addedItem.getAttribute( 'id' ).match( /^customize-control-nav_menu_item-(-?\d+)$/, '' );
1957 if ( ! matches ) {
1958 return;
1959 }
1960
1961 menuItemId = parseInt( matches[1], 10 );
1962 menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' );
1963
1964 // Check for duplicate menu items.
1965 if ( menuItemControl && deleteItemOriginalItemId == menuItemControl.params.original_item_id ) {
1966 instanceCounter++;
1967 }
1968 } );
1969
1970 if ( instanceCounter <= 1 ) {
1971 // Revert the check icon to add icon.
1972 availableMenuItem = $( '#menu-item-tpl-' + control.params.original_item_id );
1973 availableMenuItem.removeClass( 'selected' );
1974 availableMenuItem.find( '.menu-item-handle' ).removeClass( 'item-added' );
1975 }
1976
1977 control.container.slideUp( function() {
1978 control.setting.set( false );
1979 wp.a11y.speak( api.Menus.data.l10n.itemDeleted );
1980 $adjacentFocusTarget.focus(); // Keyboard accessibility.
1981 } );
1982
1983 control.setting.set( false );
1984 } );
1985 },
1986
1987 _setupLinksUI: function() {
1988 var $origBtn;
1989
1990 // Configure original link.
1991 $origBtn = this.container.find( 'a.original-link' );
1992
1993 $origBtn.on( 'click', function( e ) {
1994 e.preventDefault();
1995 api.previewer.previewUrl( e.target.toString() );
1996 } );
1997 },
1998
1999 /**
2000 * Update item handle title when changed.
2001 */
2002 _setupTitleUI: function() {
2003 var control = this, titleEl;
2004
2005 // Ensure that whitespace is trimmed on blur so placeholder can be shown.
2006 control.container.find( '.edit-menu-item-title' ).on( 'blur', function() {
2007 $( this ).val( $( this ).val().trim() );
2008 } );
2009
2010 titleEl = control.container.find( '.menu-item-title' );
2011 control.setting.bind( function( item ) {
2012 var trimmedTitle, titleText;
2013 if ( ! item ) {
2014 return;
2015 }
2016 item.title = item.title || '';
2017 trimmedTitle = item.title.trim();
2018
2019 titleText = trimmedTitle || item.original_title || api.Menus.data.l10n.untitled;
2020
2021 if ( item._invalid ) {
2022 titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText );
2023 }
2024
2025 // Don't update to an empty title.
2026 if ( trimmedTitle || item.original_title ) {
2027 titleEl
2028 .text( titleText )
2029 .removeClass( 'no-title' );
2030 } else {
2031 titleEl
2032 .text( titleText )
2033 .addClass( 'no-title' );
2034 }
2035 } );
2036 },
2037
2038 /**
2039 *
2040 * @return {number}
2041 */
2042 getDepth: function() {
2043 var control = this, setting = control.setting(), depth = 0;
2044 if ( ! setting ) {
2045 return 0;
2046 }
2047 while ( setting && setting.menu_item_parent ) {
2048 depth += 1;
2049 control = api.control( 'nav_menu_item[' + setting.menu_item_parent + ']' );
2050 if ( ! control ) {
2051 break;
2052 }
2053 setting = control.setting();
2054 }
2055 return depth;
2056 },
2057
2058 /**
2059 * Amend the control's params with the data necessary for the JS template just in time.
2060 */
2061 renderContent: function() {
2062 var control = this,
2063 settingValue = control.setting(),
2064 containerClasses;
2065
2066 control.params.title = settingValue.title || '';
2067 control.params.depth = control.getDepth();
2068 control.container.data( 'item-depth', control.params.depth );
2069 containerClasses = [
2070 'menu-item',
2071 'menu-item-depth-' + String( control.params.depth ),
2072 'menu-item-' + settingValue.object,
2073 'menu-item-edit-inactive'
2074 ];
2075
2076 if ( settingValue._invalid ) {
2077 containerClasses.push( 'menu-item-invalid' );
2078 control.params.title = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', control.params.title );
2079 } else if ( 'draft' === settingValue.status ) {
2080 containerClasses.push( 'pending' );
2081 control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title );
2082 }
2083
2084 control.params.el_classes = containerClasses.join( ' ' );
2085 control.params.item_type_label = settingValue.type_label;
2086 control.params.item_type = settingValue.type;
2087 control.params.url = settingValue.url;
2088 control.params.target = settingValue.target;
2089 control.params.attr_title = settingValue.attr_title;
2090 control.params.classes = _.isArray( settingValue.classes ) ? settingValue.classes.join( ' ' ) : settingValue.classes;
2091 control.params.xfn = settingValue.xfn;
2092 control.params.description = settingValue.description;
2093 control.params.parent = settingValue.menu_item_parent;
2094 control.params.original_title = settingValue.original_title || '';
2095
2096 control.container.addClass( control.params.el_classes );
2097
2098 api.Control.prototype.renderContent.call( control );
2099 },
2100
2101 /***********************************************************************
2102 * Begin public API methods
2103 **********************************************************************/
2104
2105 /**
2106 * @return {wp.customize.controlConstructor.nav_menu|null}
2107 */
2108 getMenuControl: function() {
2109 var control = this, settingValue = control.setting();
2110 if ( settingValue && settingValue.nav_menu_term_id ) {
2111 return api.control( 'nav_menu[' + settingValue.nav_menu_term_id + ']' );
2112 } else {
2113 return null;
2114 }
2115 },
2116
2117 /**
2118 * Expand the accordion section containing a control
2119 */
2120 expandControlSection: function() {
2121 var $section = this.container.closest( '.accordion-section' );
2122 if ( ! $section.hasClass( 'open' ) ) {
2123 $section.find( '.accordion-section-title:first' ).trigger( 'click' );
2124 }
2125 },
2126
2127 /**
2128 * @since 4.6.0
2129 *
2130 * @param {Boolean} expanded
2131 * @param {Object} [params]
2132 * @return {Boolean} False if state already applied.
2133 */
2134 _toggleExpanded: api.Section.prototype._toggleExpanded,
2135
2136 /**
2137 * @since 4.6.0
2138 *
2139 * @param {Object} [params]
2140 * @return {Boolean} False if already expanded.
2141 */
2142 expand: api.Section.prototype.expand,
2143
2144 /**
2145 * Expand the menu item form control.
2146 *
2147 * @since 4.5.0 Added params.completeCallback.
2148 *
2149 * @param {Object} [params] - Optional params.
2150 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
2151 */
2152 expandForm: function( params ) {
2153 this.expand( params );
2154 },
2155
2156 /**
2157 * @since 4.6.0
2158 *
2159 * @param {Object} [params]
2160 * @return {Boolean} False if already collapsed.
2161 */
2162 collapse: api.Section.prototype.collapse,
2163
2164 /**
2165 * Collapse the menu item form control.
2166 *
2167 * @since 4.5.0 Added params.completeCallback.
2168 *
2169 * @param {Object} [params] - Optional params.
2170 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
2171 */
2172 collapseForm: function( params ) {
2173 this.collapse( params );
2174 },
2175
2176 /**
2177 * Expand or collapse the menu item control.
2178 *
2179 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
2180 * @since 4.5.0 Added params.completeCallback.
2181 *
2182 * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility
2183 * @param {Object} [params] - Optional params.
2184 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
2185 */
2186 toggleForm: function( showOrHide, params ) {
2187 if ( typeof showOrHide === 'undefined' ) {
2188 showOrHide = ! this.expanded();
2189 }
2190 if ( showOrHide ) {
2191 this.expand( params );
2192 } else {
2193 this.collapse( params );
2194 }
2195 },
2196
2197 /**
2198 * Expand or collapse the menu item control.
2199 *
2200 * @since 4.6.0
2201 * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility
2202 * @param {Object} [params] - Optional params.
2203 * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating.
2204 */
2205 onChangeExpanded: function( showOrHide, params ) {
2206 var self = this, $menuitem, $inside, complete;
2207
2208 $menuitem = this.container;
2209 $inside = $menuitem.find( '.menu-item-settings:first' );
2210 if ( 'undefined' === typeof showOrHide ) {
2211 showOrHide = ! $inside.is( ':visible' );
2212 }
2213
2214 // Already expanded or collapsed.
2215 if ( $inside.is( ':visible' ) === showOrHide ) {
2216 if ( params && params.completeCallback ) {
2217 params.completeCallback();
2218 }
2219 return;
2220 }
2221
2222 if ( showOrHide ) {
2223 // Close all other menu item controls before expanding this one.
2224 api.control.each( function( otherControl ) {
2225 if ( self.params.type === otherControl.params.type && self !== otherControl ) {
2226 otherControl.collapseForm();
2227 }
2228 } );
2229
2230 complete = function() {
2231 $menuitem
2232 .removeClass( 'menu-item-edit-inactive' )
2233 .addClass( 'menu-item-edit-active' );
2234 self.container.trigger( 'expanded' );
2235
2236 if ( params && params.completeCallback ) {
2237 params.completeCallback();
2238 }
2239 };
2240
2241 $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'true' );
2242 $inside.slideDown( 'fast', complete );
2243
2244 self.container.trigger( 'expand' );
2245 } else {
2246 complete = function() {
2247 $menuitem
2248 .addClass( 'menu-item-edit-inactive' )
2249 .removeClass( 'menu-item-edit-active' );
2250 self.container.trigger( 'collapsed' );
2251
2252 if ( params && params.completeCallback ) {
2253 params.completeCallback();
2254 }
2255 };
2256
2257 self.container.trigger( 'collapse' );
2258
2259 $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'false' );
2260 $inside.slideUp( 'fast', complete );
2261 }
2262 },
2263
2264 /**
2265 * Expand the containing menu section, expand the form, and focus on
2266 * the first input in the control.
2267 *
2268 * @since 4.5.0 Added params.completeCallback.
2269 *
2270 * @param {Object} [params] - Params object.
2271 * @param {Function} [params.completeCallback] - Optional callback function when focus has completed.
2272 */
2273 focus: function( params ) {
2274 params = params || {};
2275 var control = this, originalCompleteCallback = params.completeCallback, focusControl;
2276
2277 focusControl = function() {
2278 control.expandControlSection();
2279
2280 params.completeCallback = function() {
2281 var focusable;
2282
2283 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
2284 focusable = control.container.find( '.menu-item-settings' ).find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' );
2285 focusable.first().focus();
2286
2287 if ( originalCompleteCallback ) {
2288 originalCompleteCallback();
2289 }
2290 };
2291
2292 control.expandForm( params );
2293 };
2294
2295 if ( api.section.has( control.section() ) ) {
2296 api.section( control.section() ).expand( {
2297 completeCallback: focusControl
2298 } );
2299 } else {
2300 focusControl();
2301 }
2302 },
2303
2304 /**
2305 * Move menu item up one in the menu.
2306 */
2307 moveUp: function() {
2308 this._changePosition( -1 );
2309 wp.a11y.speak( api.Menus.data.l10n.movedUp );
2310 },
2311
2312 /**
2313 * Move menu item up one in the menu.
2314 */
2315 moveDown: function() {
2316 this._changePosition( 1 );
2317 wp.a11y.speak( api.Menus.data.l10n.movedDown );
2318 },
2319 /**
2320 * Move menu item and all children up one level of depth.
2321 */
2322 moveLeft: function() {
2323 this._changeDepth( -1 );
2324 wp.a11y.speak( api.Menus.data.l10n.movedLeft );
2325 },
2326
2327 /**
2328 * Move menu item and children one level deeper, as a submenu of the previous item.
2329 */
2330 moveRight: function() {
2331 this._changeDepth( 1 );
2332 wp.a11y.speak( api.Menus.data.l10n.movedRight );
2333 },
2334
2335 /**
2336 * Note that this will trigger a UI update, causing child items to
2337 * move as well and cardinal order class names to be updated.
2338 *
2339 * @private
2340 *
2341 * @param {number} offset 1|-1
2342 */
2343 _changePosition: function( offset ) {
2344 var control = this,
2345 adjacentSetting,
2346 settingValue = _.clone( control.setting() ),
2347 siblingSettings = [],
2348 realPosition;
2349
2350 if ( 1 !== offset && -1 !== offset ) {
2351 throw new Error( 'Offset changes by 1 are only supported.' );
2352 }
2353
2354 // Skip moving deleted items.
2355 if ( ! control.setting() ) {
2356 return;
2357 }
2358
2359 // Locate the other items under the same parent (siblings).
2360 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
2361 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
2362 siblingSettings.push( otherControl.setting );
2363 }
2364 });
2365 siblingSettings.sort(function( a, b ) {
2366 return a().position - b().position;
2367 });
2368
2369 realPosition = _.indexOf( siblingSettings, control.setting );
2370 if ( -1 === realPosition ) {
2371 throw new Error( 'Expected setting to be among siblings.' );
2372 }
2373
2374 // Skip doing anything if the item is already at the edge in the desired direction.
2375 if ( ( realPosition === 0 && offset < 0 ) || ( realPosition === siblingSettings.length - 1 && offset > 0 ) ) {
2376 // @todo Should we allow a menu item to be moved up to break it out of a parent? Adopt with previous or following parent?
2377 return;
2378 }
2379
2380 // Update any adjacent menu item setting to take on this item's position.
2381 adjacentSetting = siblingSettings[ realPosition + offset ];
2382 if ( adjacentSetting ) {
2383 adjacentSetting.set( $.extend(
2384 _.clone( adjacentSetting() ),
2385 {
2386 position: settingValue.position
2387 }
2388 ) );
2389 }
2390
2391 settingValue.position += offset;
2392 control.setting.set( settingValue );
2393 },
2394
2395 /**
2396 * Note that this will trigger a UI update, causing child items to
2397 * move as well and cardinal order class names to be updated.
2398 *
2399 * @private
2400 *
2401 * @param {number} offset 1|-1
2402 */
2403 _changeDepth: function( offset ) {
2404 if ( 1 !== offset && -1 !== offset ) {
2405 throw new Error( 'Offset changes by 1 are only supported.' );
2406 }
2407 var control = this,
2408 settingValue = _.clone( control.setting() ),
2409 siblingControls = [],
2410 realPosition,
2411 siblingControl,
2412 parentControl;
2413
2414 // Locate the other items under the same parent (siblings).
2415 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
2416 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
2417 siblingControls.push( otherControl );
2418 }
2419 });
2420 siblingControls.sort(function( a, b ) {
2421 return a.setting().position - b.setting().position;
2422 });
2423
2424 realPosition = _.indexOf( siblingControls, control );
2425 if ( -1 === realPosition ) {
2426 throw new Error( 'Expected control to be among siblings.' );
2427 }
2428
2429 if ( -1 === offset ) {
2430 // Skip moving left an item that is already at the top level.
2431 if ( ! settingValue.menu_item_parent ) {
2432 return;
2433 }
2434
2435 parentControl = api.control( 'nav_menu_item[' + settingValue.menu_item_parent + ']' );
2436
2437 // Make this control the parent of all the following siblings.
2438 _( siblingControls ).chain().slice( realPosition ).each(function( siblingControl, i ) {
2439 siblingControl.setting.set(
2440 $.extend(
2441 {},
2442 siblingControl.setting(),
2443 {
2444 menu_item_parent: control.params.menu_item_id,
2445 position: i
2446 }
2447 )
2448 );
2449 });
2450
2451 // Increase the positions of the parent item's subsequent children to make room for this one.
2452 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
2453 var otherControlSettingValue, isControlToBeShifted;
2454 isControlToBeShifted = (
2455 otherControl.setting().menu_item_parent === parentControl.setting().menu_item_parent &&
2456 otherControl.setting().position > parentControl.setting().position
2457 );
2458 if ( isControlToBeShifted ) {
2459 otherControlSettingValue = _.clone( otherControl.setting() );
2460 otherControl.setting.set(
2461 $.extend(
2462 otherControlSettingValue,
2463 { position: otherControlSettingValue.position + 1 }
2464 )
2465 );
2466 }
2467 });
2468
2469 // Make this control the following sibling of its parent item.
2470 settingValue.position = parentControl.setting().position + 1;
2471 settingValue.menu_item_parent = parentControl.setting().menu_item_parent;
2472 control.setting.set( settingValue );
2473
2474 } else if ( 1 === offset ) {
2475 // Skip moving right an item that doesn't have a previous sibling.
2476 if ( realPosition === 0 ) {
2477 return;
2478 }
2479
2480 // Make the control the last child of the previous sibling.
2481 siblingControl = siblingControls[ realPosition - 1 ];
2482 settingValue.menu_item_parent = siblingControl.params.menu_item_id;
2483 settingValue.position = 0;
2484 _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) {
2485 if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) {
2486 settingValue.position = Math.max( settingValue.position, otherControl.setting().position );
2487 }
2488 });
2489 settingValue.position += 1;
2490 control.setting.set( settingValue );
2491 }
2492 }
2493 } );
2494
2495 /**
2496 * wp.customize.Menus.MenuNameControl
2497 *
2498 * Customizer control for a nav menu's name.
2499 *
2500 * @class wp.customize.Menus.MenuNameControl
2501 * @augments wp.customize.Control
2502 */
2503 api.Menus.MenuNameControl = api.Control.extend(/** @lends wp.customize.Menus.MenuNameControl.prototype */{
2504
2505 ready: function() {
2506 var control = this;
2507
2508 if ( control.setting ) {
2509 var settingValue = control.setting();
2510
2511 control.nameElement = new api.Element( control.container.find( '.menu-name-field' ) );
2512
2513 control.nameElement.bind(function( value ) {
2514 var settingValue = control.setting();
2515 if ( settingValue && settingValue.name !== value ) {
2516 settingValue = _.clone( settingValue );
2517 settingValue.name = value;
2518 control.setting.set( settingValue );
2519 }
2520 });
2521 if ( settingValue ) {
2522 control.nameElement.set( settingValue.name );
2523 }
2524
2525 control.setting.bind(function( object ) {
2526 if ( object ) {
2527 control.nameElement.set( object.name );
2528 }
2529 });
2530 }
2531 }
2532 });
2533
2534 /**
2535 * wp.customize.Menus.MenuLocationsControl
2536 *
2537 * Customizer control for a nav menu's locations.
2538 *
2539 * @since 4.9.0
2540 * @class wp.customize.Menus.MenuLocationsControl
2541 * @augments wp.customize.Control
2542 */
2543 api.Menus.MenuLocationsControl = api.Control.extend(/** @lends wp.customize.Menus.MenuLocationsControl.prototype */{
2544
2545 /**
2546 * Set up the control.
2547 *
2548 * @since 4.9.0
2549 */
2550 ready: function () {
2551 var control = this;
2552
2553 control.container.find( '.assigned-menu-location' ).each(function() {
2554 var container = $( this ),
2555 checkbox = container.find( 'input[type=checkbox]' ),
2556 element = new api.Element( checkbox ),
2557 navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' ),
2558 isNewMenu = control.params.menu_id === '',
2559 updateCheckbox = isNewMenu ? _.noop : function( checked ) {
2560 element.set( checked );
2561 },
2562 updateSetting = isNewMenu ? _.noop : function( checked ) {
2563 navMenuLocationSetting.set( checked ? control.params.menu_id : 0 );
2564 },
2565 updateSelectedMenuLabel = function( selectedMenuId ) {
2566 var menuSetting = api( 'nav_menu[' + String( selectedMenuId ) + ']' );
2567 if ( ! selectedMenuId || ! menuSetting || ! menuSetting() ) {
2568 container.find( '.theme-location-set' ).hide();
2569 } else {
2570 container.find( '.theme-location-set' ).show().find( 'span' ).text( displayNavMenuName( menuSetting().name ) );
2571 }
2572 };
2573
2574 updateCheckbox( navMenuLocationSetting.get() === control.params.menu_id );
2575
2576 checkbox.on( 'change', function() {
2577 // Note: We can't use element.bind( function( checked ){ ... } ) here because it will trigger a change as well.
2578 updateSetting( this.checked );
2579 } );
2580
2581 navMenuLocationSetting.bind( function( selectedMenuId ) {
2582 updateCheckbox( selectedMenuId === control.params.menu_id );
2583 updateSelectedMenuLabel( selectedMenuId );
2584 } );
2585 updateSelectedMenuLabel( navMenuLocationSetting.get() );
2586 });
2587 },
2588
2589 /**
2590 * Set the selected locations.
2591 *
2592 * This method sets the selected locations and allows us to do things like
2593 * set the default location for a new menu.
2594 *
2595 * @since 4.9.0
2596 *
2597 * @param {Object.<string,boolean>} selections - A map of location selections.
2598 * @return {void}
2599 */
2600 setSelections: function( selections ) {
2601 this.container.find( '.menu-location' ).each( function( i, checkboxNode ) {
2602 var locationId = checkboxNode.dataset.locationId;
2603 checkboxNode.checked = locationId in selections ? selections[ locationId ] : false;
2604 } );
2605 }
2606 });
2607
2608 /**
2609 * wp.customize.Menus.MenuAutoAddControl
2610 *
2611 * Customizer control for a nav menu's auto add.
2612 *
2613 * @class wp.customize.Menus.MenuAutoAddControl
2614 * @augments wp.customize.Control
2615 */
2616 api.Menus.MenuAutoAddControl = api.Control.extend(/** @lends wp.customize.Menus.MenuAutoAddControl.prototype */{
2617
2618 ready: function() {
2619 var control = this,
2620 settingValue = control.setting();
2621
2622 /*
2623 * Since the control is not registered in PHP, we need to prevent the
2624 * preview's sending of the activeControls to result in this control
2625 * being deactivated.
2626 */
2627 control.active.validate = function() {
2628 var value, section = api.section( control.section() );
2629 if ( section ) {
2630 value = section.active();
2631 } else {
2632 value = false;
2633 }
2634 return value;
2635 };
2636
2637 control.autoAddElement = new api.Element( control.container.find( 'input[type=checkbox].auto_add' ) );
2638
2639 control.autoAddElement.bind(function( value ) {
2640 var settingValue = control.setting();
2641 if ( settingValue && settingValue.name !== value ) {
2642 settingValue = _.clone( settingValue );
2643 settingValue.auto_add = value;
2644 control.setting.set( settingValue );
2645 }
2646 });
2647 if ( settingValue ) {
2648 control.autoAddElement.set( settingValue.auto_add );
2649 }
2650
2651 control.setting.bind(function( object ) {
2652 if ( object ) {
2653 control.autoAddElement.set( object.auto_add );
2654 }
2655 });
2656 }
2657
2658 });
2659
2660 /**
2661 * wp.customize.Menus.MenuControl
2662 *
2663 * Customizer control for menus.
2664 * Note that 'nav_menu' must match the WP_Menu_Customize_Control::$type
2665 *
2666 * @class wp.customize.Menus.MenuControl
2667 * @augments wp.customize.Control
2668 */
2669 api.Menus.MenuControl = api.Control.extend(/** @lends wp.customize.Menus.MenuControl.prototype */{
2670 /**
2671 * Set up the control.
2672 */
2673 ready: function() {
2674 var control = this,
2675 section = api.section( control.section() ),
2676 menuId = control.params.menu_id,
2677 menu = control.setting(),
2678 name,
2679 widgetTemplate,
2680 select;
2681
2682 if ( 'undefined' === typeof this.params.menu_id ) {
2683 throw new Error( 'params.menu_id was not defined' );
2684 }
2685
2686 /*
2687 * Since the control is not registered in PHP, we need to prevent the
2688 * preview's sending of the activeControls to result in this control
2689 * being deactivated.
2690 */
2691 control.active.validate = function() {
2692 var value;
2693 if ( section ) {
2694 value = section.active();
2695 } else {
2696 value = false;
2697 }
2698 return value;
2699 };
2700
2701 control.$controlSection = section.headContainer;
2702 control.$sectionContent = control.container.closest( '.accordion-section-content' );
2703
2704 this._setupModel();
2705
2706 api.section( control.section(), function( section ) {
2707 section.deferred.initSortables.done(function( menuList ) {
2708 control._setupSortable( menuList );
2709 });
2710 } );
2711
2712 this._setupAddition();
2713 this._setupTitle();
2714
2715 // Add menu to Navigation Menu widgets.
2716 if ( menu ) {
2717 name = displayNavMenuName( menu.name );
2718
2719 // Add the menu to the existing controls.
2720 api.control.each( function( widgetControl ) {
2721 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
2722 return;
2723 }
2724 widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).show();
2725 widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).hide();
2726
2727 select = widgetControl.container.find( 'select' );
2728 if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) {
2729 select.append( new Option( name, menuId ) );
2730 }
2731 } );
2732
2733 // Add the menu to the widget template.
2734 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
2735 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).show();
2736 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).hide();
2737 select = widgetTemplate.find( '.widget-inside select:first' );
2738 if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) {
2739 select.append( new Option( name, menuId ) );
2740 }
2741 }
2742
2743 /*
2744 * Wait for menu items to be added.
2745 * Ideally, we'd bind to an event indicating construction is complete,
2746 * but deferring appears to be the best option today.
2747 */
2748 _.defer( function () {
2749 control.updateInvitationVisibility();
2750 } );
2751 },
2752
2753 /**
2754 * Update ordering of menu item controls when the setting is updated.
2755 */
2756 _setupModel: function() {
2757 var control = this,
2758 menuId = control.params.menu_id;
2759
2760 control.setting.bind( function( to ) {
2761 var name;
2762 if ( false === to ) {
2763 control._handleDeletion();
2764 } else {
2765 // Update names in the Navigation Menu widgets.
2766 name = displayNavMenuName( to.name );
2767 api.control.each( function( widgetControl ) {
2768 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
2769 return;
2770 }
2771 var select = widgetControl.container.find( 'select' );
2772 select.find( 'option[value=' + String( menuId ) + ']' ).text( name );
2773 });
2774 }
2775 } );
2776 },
2777
2778 /**
2779 * Allow items in each menu to be re-ordered, and for the order to be previewed.
2780 *
2781 * Notice that the UI aspects here are handled by wpNavMenu.initSortables()
2782 * which is called in MenuSection.onChangeExpanded()
2783 *
2784 * @param {Object} menuList - The element that has sortable().
2785 */
2786 _setupSortable: function( menuList ) {
2787 var control = this;
2788
2789 if ( ! menuList.is( control.$sectionContent ) ) {
2790 throw new Error( 'Unexpected menuList.' );
2791 }
2792
2793 menuList.on( 'sortstart', function() {
2794 control.isSorting = true;
2795 });
2796
2797 menuList.on( 'sortstop', function() {
2798 setTimeout( function() { // Next tick.
2799 var menuItemContainerIds = control.$sectionContent.sortable( 'toArray' ),
2800 menuItemControls = [],
2801 position = 0,
2802 priority = 10;
2803
2804 control.isSorting = false;
2805
2806 // Reset horizontal scroll position when done dragging.
2807 control.$sectionContent.scrollLeft( 0 );
2808
2809 _.each( menuItemContainerIds, function( menuItemContainerId ) {
2810 var menuItemId, menuItemControl, matches;
2811 matches = menuItemContainerId.match( /^customize-control-nav_menu_item-(-?\d+)$/, '' );
2812 if ( ! matches ) {
2813 return;
2814 }
2815 menuItemId = parseInt( matches[1], 10 );
2816 menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' );
2817 if ( menuItemControl ) {
2818 menuItemControls.push( menuItemControl );
2819 }
2820 } );
2821
2822 _.each( menuItemControls, function( menuItemControl ) {
2823 if ( false === menuItemControl.setting() ) {
2824 // Skip deleted items.
2825 return;
2826 }
2827 var setting = _.clone( menuItemControl.setting() );
2828 position += 1;
2829 priority += 1;
2830 setting.position = position;
2831 menuItemControl.priority( priority );
2832
2833 // Note that wpNavMenu will be setting this .menu-item-data-parent-id input's value.
2834 setting.menu_item_parent = parseInt( menuItemControl.container.find( '.menu-item-data-parent-id' ).val(), 10 );
2835 if ( ! setting.menu_item_parent ) {
2836 setting.menu_item_parent = 0;
2837 }
2838
2839 menuItemControl.setting.set( setting );
2840 });
2841
2842 // Mark all menu items as unprocessed.
2843 $( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
2844 });
2845
2846 });
2847 control.isReordering = false;
2848
2849 /**
2850 * Keyboard-accessible reordering.
2851 */
2852 this.container.find( '.reorder-toggle' ).on( 'click', function() {
2853 control.toggleReordering( ! control.isReordering );
2854 } );
2855 },
2856
2857 /**
2858 * Set up UI for adding a new menu item.
2859 */
2860 _setupAddition: function() {
2861 var self = this;
2862
2863 this.container.find( '.add-new-menu-item' ).on( 'click', function( event ) {
2864 if ( self.$sectionContent.hasClass( 'reordering' ) ) {
2865 return;
2866 }
2867
2868 if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
2869 $( this ).attr( 'aria-expanded', 'true' );
2870 api.Menus.availableMenuItemsPanel.open( self );
2871 } else {
2872 $( this ).attr( 'aria-expanded', 'false' );
2873 api.Menus.availableMenuItemsPanel.close();
2874 event.stopPropagation();
2875 }
2876 } );
2877 },
2878
2879 _handleDeletion: function() {
2880 var control = this,
2881 section,
2882 menuId = control.params.menu_id,
2883 removeSection,
2884 widgetTemplate,
2885 navMenuCount = 0;
2886 section = api.section( control.section() );
2887 removeSection = function() {
2888 section.container.remove();
2889 api.section.remove( section.id );
2890 };
2891
2892 if ( section && section.expanded() ) {
2893 section.collapse({
2894 completeCallback: function() {
2895 removeSection();
2896 wp.a11y.speak( api.Menus.data.l10n.menuDeleted );
2897 api.panel( 'nav_menus' ).focus();
2898 }
2899 });
2900 } else {
2901 removeSection();
2902 }
2903
2904 api.each(function( setting ) {
2905 if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) {
2906 navMenuCount += 1;
2907 }
2908 });
2909
2910 // Remove the menu from any Navigation Menu widgets.
2911 api.control.each(function( widgetControl ) {
2912 if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) {
2913 return;
2914 }
2915 var select = widgetControl.container.find( 'select' );
2916 if ( select.val() === String( menuId ) ) {
2917 select.prop( 'selectedIndex', 0 ).trigger( 'change' );
2918 }
2919
2920 widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
2921 widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
2922 widgetControl.container.find( 'option[value=' + String( menuId ) + ']' ).remove();
2923 });
2924
2925 // Remove the menu to the nav menu widget template.
2926 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
2927 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
2928 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
2929 widgetTemplate.find( 'option[value=' + String( menuId ) + ']' ).remove();
2930 },
2931
2932 /**
2933 * Update Section Title as menu name is changed.
2934 */
2935 _setupTitle: function() {
2936 var control = this;
2937
2938 control.setting.bind( function( menu ) {
2939 if ( ! menu ) {
2940 return;
2941 }
2942
2943 var section = api.section( control.section() ),
2944 menuId = control.params.menu_id,
2945 controlTitle = section.headContainer.find( '.accordion-section-title' ),
2946 sectionTitle = section.contentContainer.find( '.customize-section-title h3' ),
2947 location = section.headContainer.find( '.menu-in-location' ),
2948 action = sectionTitle.find( '.customize-action' ),
2949 name = displayNavMenuName( menu.name );
2950
2951 // Update the control title.
2952 controlTitle.text( name );
2953 if ( location.length ) {
2954 location.appendTo( controlTitle );
2955 }
2956
2957 // Update the section title.
2958 sectionTitle.text( name );
2959 if ( action.length ) {
2960 action.prependTo( sectionTitle );
2961 }
2962
2963 // Update the nav menu name in location selects.
2964 api.control.each( function( control ) {
2965 if ( /^nav_menu_locations\[/.test( control.id ) ) {
2966 control.container.find( 'option[value=' + menuId + ']' ).text( name );
2967 }
2968 } );
2969
2970 // Update the nav menu name in all location checkboxes.
2971 section.contentContainer.find( '.customize-control-checkbox input' ).each( function() {
2972 if ( $( this ).prop( 'checked' ) ) {
2973 $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( name );
2974 }
2975 } );
2976 } );
2977 },
2978
2979 /***********************************************************************
2980 * Begin public API methods
2981 **********************************************************************/
2982
2983 /**
2984 * Enable/disable the reordering UI
2985 *
2986 * @param {boolean} showOrHide to enable/disable reordering
2987 */
2988 toggleReordering: function( showOrHide ) {
2989 var addNewItemBtn = this.container.find( '.add-new-menu-item' ),
2990 reorderBtn = this.container.find( '.reorder-toggle' ),
2991 itemsTitle = this.$sectionContent.find( '.item-title' );
2992
2993 showOrHide = Boolean( showOrHide );
2994
2995 if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
2996 return;
2997 }
2998
2999 this.isReordering = showOrHide;
3000 this.$sectionContent.toggleClass( 'reordering', showOrHide );
3001 this.$sectionContent.sortable( this.isReordering ? 'disable' : 'enable' );
3002 if ( this.isReordering ) {
3003 addNewItemBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
3004 reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOff );
3005 wp.a11y.speak( api.Menus.data.l10n.reorderModeOn );
3006 itemsTitle.attr( 'aria-hidden', 'false' );
3007 } else {
3008 addNewItemBtn.removeAttr( 'tabindex aria-hidden' );
3009 reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOn );
3010 wp.a11y.speak( api.Menus.data.l10n.reorderModeOff );
3011 itemsTitle.attr( 'aria-hidden', 'true' );
3012 }
3013
3014 if ( showOrHide ) {
3015 _( this.getMenuItemControls() ).each( function( formControl ) {
3016 formControl.collapseForm();
3017 } );
3018 }
3019 },
3020
3021 /**
3022 * @return {wp.customize.controlConstructor.nav_menu_item[]}
3023 */
3024 getMenuItemControls: function() {
3025 var menuControl = this,
3026 menuItemControls = [],
3027 menuTermId = menuControl.params.menu_id;
3028
3029 api.control.each(function( control ) {
3030 if ( 'nav_menu_item' === control.params.type && control.setting() && menuTermId === control.setting().nav_menu_term_id ) {
3031 menuItemControls.push( control );
3032 }
3033 });
3034
3035 return menuItemControls;
3036 },
3037
3038 /**
3039 * Make sure that each menu item control has the proper depth.
3040 */
3041 reflowMenuItems: function() {
3042 var menuControl = this,
3043 menuItemControls = menuControl.getMenuItemControls(),
3044 reflowRecursively;
3045
3046 reflowRecursively = function( context ) {
3047 var currentMenuItemControls = [],
3048 thisParent = context.currentParent;
3049 _.each( context.menuItemControls, function( menuItemControl ) {
3050 if ( thisParent === menuItemControl.setting().menu_item_parent ) {
3051 currentMenuItemControls.push( menuItemControl );
3052 // @todo We could remove this item from menuItemControls now, for efficiency.
3053 }
3054 });
3055 currentMenuItemControls.sort( function( a, b ) {
3056 return a.setting().position - b.setting().position;
3057 });
3058
3059 _.each( currentMenuItemControls, function( menuItemControl ) {
3060 // Update position.
3061 context.currentAbsolutePosition += 1;
3062 menuItemControl.priority.set( context.currentAbsolutePosition ); // This will change the sort order.
3063
3064 // Update depth.
3065 if ( ! menuItemControl.container.hasClass( 'menu-item-depth-' + String( context.currentDepth ) ) ) {
3066 _.each( menuItemControl.container.prop( 'className' ).match( /menu-item-depth-\d+/g ), function( className ) {
3067 menuItemControl.container.removeClass( className );
3068 });
3069 menuItemControl.container.addClass( 'menu-item-depth-' + String( context.currentDepth ) );
3070 }
3071 menuItemControl.container.data( 'item-depth', context.currentDepth );
3072
3073 // Process any children items.
3074 context.currentDepth += 1;
3075 context.currentParent = menuItemControl.params.menu_item_id;
3076 reflowRecursively( context );
3077 context.currentDepth -= 1;
3078 context.currentParent = thisParent;
3079 });
3080
3081 // Update class names for reordering controls.
3082 if ( currentMenuItemControls.length ) {
3083 _( currentMenuItemControls ).each(function( menuItemControl ) {
3084 menuItemControl.container.removeClass( 'move-up-disabled move-down-disabled move-left-disabled move-right-disabled' );
3085 if ( 0 === context.currentDepth ) {
3086 menuItemControl.container.addClass( 'move-left-disabled' );
3087 } else if ( 10 === context.currentDepth ) {
3088 menuItemControl.container.addClass( 'move-right-disabled' );
3089 }
3090 });
3091
3092 currentMenuItemControls[0].container
3093 .addClass( 'move-up-disabled' )
3094 .addClass( 'move-right-disabled' )
3095 .toggleClass( 'move-down-disabled', 1 === currentMenuItemControls.length );
3096 currentMenuItemControls[ currentMenuItemControls.length - 1 ].container
3097 .addClass( 'move-down-disabled' )
3098 .toggleClass( 'move-up-disabled', 1 === currentMenuItemControls.length );
3099 }
3100 };
3101
3102 reflowRecursively( {
3103 menuItemControls: menuItemControls,
3104 currentParent: 0,
3105 currentDepth: 0,
3106 currentAbsolutePosition: 0
3107 } );
3108
3109 menuControl.updateInvitationVisibility( menuItemControls );
3110 menuControl.container.find( '.reorder-toggle' ).toggle( menuItemControls.length > 1 );
3111 },
3112
3113 /**
3114 * Note that this function gets debounced so that when a lot of setting
3115 * changes are made at once, for instance when moving a menu item that
3116 * has child items, this function will only be called once all of the
3117 * settings have been updated.
3118 */
3119 debouncedReflowMenuItems: _.debounce( function() {
3120 this.reflowMenuItems.apply( this, arguments );
3121 }, 0 ),
3122
3123 /**
3124 * Add a new item to this menu.
3125 *
3126 * @param {Object} item - Value for the nav_menu_item setting to be created.
3127 * @return {wp.customize.Menus.controlConstructor.nav_menu_item} The newly-created nav_menu_item control instance.
3128 */
3129 addItemToMenu: function( item ) {
3130 var menuControl = this, customizeId, settingArgs, setting, menuItemControl, placeholderId, position = 0, priority = 10,
3131 originalItemId = item.id || '';
3132
3133 _.each( menuControl.getMenuItemControls(), function( control ) {
3134 if ( false === control.setting() ) {
3135 return;
3136 }
3137 priority = Math.max( priority, control.priority() );
3138 if ( 0 === control.setting().menu_item_parent ) {
3139 position = Math.max( position, control.setting().position );
3140 }
3141 });
3142 position += 1;
3143 priority += 1;
3144
3145 item = $.extend(
3146 {},
3147 api.Menus.data.defaultSettingValues.nav_menu_item,
3148 item,
3149 {
3150 nav_menu_term_id: menuControl.params.menu_id,
3151 position: position
3152 }
3153 );
3154 delete item.id; // Only used by Backbone.
3155
3156 placeholderId = api.Menus.generatePlaceholderAutoIncrementId();
3157 customizeId = 'nav_menu_item[' + String( placeholderId ) + ']';
3158 settingArgs = {
3159 type: 'nav_menu_item',
3160 transport: api.Menus.data.settingTransport,
3161 previewer: api.previewer
3162 };
3163 setting = api.create( customizeId, customizeId, {}, settingArgs );
3164 setting.set( item ); // Change from initial empty object to actual item to mark as dirty.
3165
3166 // Add the menu item control.
3167 menuItemControl = new api.controlConstructor.nav_menu_item( customizeId, {
3168 type: 'nav_menu_item',
3169 section: menuControl.id,
3170 priority: priority,
3171 settings: {
3172 'default': customizeId
3173 },
3174 menu_item_id: placeholderId,
3175 original_item_id: originalItemId
3176 } );
3177
3178 api.control.add( menuItemControl );
3179 setting.preview();
3180 menuControl.debouncedReflowMenuItems();
3181
3182 wp.a11y.speak( api.Menus.data.l10n.itemAdded );
3183
3184 return menuItemControl;
3185 },
3186
3187 /**
3188 * Show an invitation to add new menu items when there are no menu items.
3189 *
3190 * @since 4.9.0
3191 *
3192 * @param {wp.customize.controlConstructor.nav_menu_item[]} optionalMenuItemControls
3193 */
3194 updateInvitationVisibility: function ( optionalMenuItemControls ) {
3195 var menuItemControls = optionalMenuItemControls || this.getMenuItemControls();
3196
3197 this.container.find( '.new-menu-item-invitation' ).toggle( menuItemControls.length === 0 );
3198 }
3199 } );
3200
3201 /**
3202 * Extends wp.customize.controlConstructor with control constructor for
3203 * menu_location, menu_item, nav_menu, and new_menu.
3204 */
3205 $.extend( api.controlConstructor, {
3206 nav_menu_location: api.Menus.MenuLocationControl,
3207 nav_menu_item: api.Menus.MenuItemControl,
3208 nav_menu: api.Menus.MenuControl,
3209 nav_menu_name: api.Menus.MenuNameControl,
3210 nav_menu_locations: api.Menus.MenuLocationsControl,
3211 nav_menu_auto_add: api.Menus.MenuAutoAddControl
3212 });
3213
3214 /**
3215 * Extends wp.customize.panelConstructor with section constructor for menus.
3216 */
3217 $.extend( api.panelConstructor, {
3218 nav_menus: api.Menus.MenusPanel
3219 });
3220
3221 /**
3222 * Extends wp.customize.sectionConstructor with section constructor for menu.
3223 */
3224 $.extend( api.sectionConstructor, {
3225 nav_menu: api.Menus.MenuSection,
3226 new_menu: api.Menus.NewMenuSection
3227 });
3228
3229 /**
3230 * Init Customizer for menus.
3231 */
3232 api.bind( 'ready', function() {
3233
3234 // Set up the menu items panel.
3235 api.Menus.availableMenuItemsPanel = new api.Menus.AvailableMenuItemsPanelView({
3236 collection: api.Menus.availableMenuItems
3237 });
3238
3239 api.bind( 'saved', function( data ) {
3240 if ( data.nav_menu_updates || data.nav_menu_item_updates ) {
3241 api.Menus.applySavedData( data );
3242 }
3243 } );
3244
3245 /*
3246 * Reset the list of posts created in the customizer once published.
3247 * The setting is updated quietly (bypassing events being triggered)
3248 * so that the customized state doesn't become immediately dirty.
3249 */
3250 api.state( 'changesetStatus' ).bind( function( status ) {
3251 if ( 'publish' === status ) {
3252 api( 'nav_menus_created_posts' )._value = [];
3253 }
3254 } );
3255
3256 // Open and focus menu control.
3257 api.previewer.bind( 'focus-nav-menu-item-control', api.Menus.focusMenuItemControl );
3258 } );
3259
3260 /**
3261 * When customize_save comes back with a success, make sure any inserted
3262 * nav menus and items are properly re-added with their newly-assigned IDs.
3263 *
3264 * @alias wp.customize.Menus.applySavedData
3265 *
3266 * @param {Object} data
3267 * @param {Array} data.nav_menu_updates
3268 * @param {Array} data.nav_menu_item_updates
3269 */
3270 api.Menus.applySavedData = function( data ) {
3271
3272 var insertedMenuIdMapping = {}, insertedMenuItemIdMapping = {};
3273
3274 _( data.nav_menu_updates ).each(function( update ) {
3275 var oldCustomizeId, newCustomizeId, customizeId, oldSetting, newSetting, setting, settingValue, oldSection, newSection, wasSaved, widgetTemplate, navMenuCount, shouldExpandNewSection;
3276 if ( 'inserted' === update.status ) {
3277 if ( ! update.previous_term_id ) {
3278 throw new Error( 'Expected previous_term_id' );
3279 }
3280 if ( ! update.term_id ) {
3281 throw new Error( 'Expected term_id' );
3282 }
3283 oldCustomizeId = 'nav_menu[' + String( update.previous_term_id ) + ']';
3284 if ( ! api.has( oldCustomizeId ) ) {
3285 throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
3286 }
3287 oldSetting = api( oldCustomizeId );
3288 if ( ! api.section.has( oldCustomizeId ) ) {
3289 throw new Error( 'Expected control to exist: ' + oldCustomizeId );
3290 }
3291 oldSection = api.section( oldCustomizeId );
3292
3293 settingValue = oldSetting.get();
3294 if ( ! settingValue ) {
3295 throw new Error( 'Did not expect setting to be empty (deleted).' );
3296 }
3297 settingValue = $.extend( _.clone( settingValue ), update.saved_value );
3298
3299 insertedMenuIdMapping[ update.previous_term_id ] = update.term_id;
3300 newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']';
3301 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
3302 type: 'nav_menu',
3303 transport: api.Menus.data.settingTransport,
3304 previewer: api.previewer
3305 } );
3306
3307 shouldExpandNewSection = oldSection.expanded();
3308 if ( shouldExpandNewSection ) {
3309 oldSection.collapse();
3310 }
3311
3312 // Add the menu section.
3313 newSection = new api.Menus.MenuSection( newCustomizeId, {
3314 panel: 'nav_menus',
3315 title: settingValue.name,
3316 customizeAction: api.Menus.data.l10n.customizingMenus,
3317 type: 'nav_menu',
3318 priority: oldSection.priority.get(),
3319 menu_id: update.term_id
3320 } );
3321
3322 // Add new control for the new menu.
3323 api.section.add( newSection );
3324
3325 // Update the values for nav menus in Navigation Menu controls.
3326 api.control.each( function( setting ) {
3327 if ( ! setting.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== setting.params.widget_id_base ) {
3328 return;
3329 }
3330 var select, oldMenuOption, newMenuOption;
3331 select = setting.container.find( 'select' );
3332 oldMenuOption = select.find( 'option[value=' + String( update.previous_term_id ) + ']' );
3333 newMenuOption = select.find( 'option[value=' + String( update.term_id ) + ']' );
3334 newMenuOption.prop( 'selected', oldMenuOption.prop( 'selected' ) );
3335 oldMenuOption.remove();
3336 } );
3337
3338 // Delete the old placeholder nav_menu.
3339 oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
3340 oldSetting.set( false );
3341 oldSetting.preview();
3342 newSetting.preview();
3343 oldSetting._dirty = false;
3344
3345 // Remove nav_menu section.
3346 oldSection.container.remove();
3347 api.section.remove( oldCustomizeId );
3348
3349 // Update the nav_menu widget to reflect removed placeholder menu.
3350 navMenuCount = 0;
3351 api.each(function( setting ) {
3352 if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) {
3353 navMenuCount += 1;
3354 }
3355 });
3356 widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' );
3357 widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount );
3358 widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount );
3359 widgetTemplate.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove();
3360
3361 // Update the nav_menu_locations[...] controls to remove the placeholder menus from the dropdown options.
3362 wp.customize.control.each(function( control ){
3363 if ( /^nav_menu_locations\[/.test( control.id ) ) {
3364 control.container.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove();
3365 }
3366 });
3367
3368 // Update nav_menu_locations to reference the new ID.
3369 api.each( function( setting ) {
3370 var wasSaved = api.state( 'saved' ).get();
3371 if ( /^nav_menu_locations\[/.test( setting.id ) && setting.get() === update.previous_term_id ) {
3372 setting.set( update.term_id );
3373 setting._dirty = false; // Not dirty because this is has also just been done on server in WP_Customize_Nav_Menu_Setting::update().
3374 api.state( 'saved' ).set( wasSaved );
3375 setting.preview();
3376 }
3377 } );
3378
3379 if ( shouldExpandNewSection ) {
3380 newSection.expand();
3381 }
3382 } else if ( 'updated' === update.status ) {
3383 customizeId = 'nav_menu[' + String( update.term_id ) + ']';
3384 if ( ! api.has( customizeId ) ) {
3385 throw new Error( 'Expected setting to exist: ' + customizeId );
3386 }
3387
3388 // Make sure the setting gets updated with its sanitized server value (specifically the conflict-resolved name).
3389 setting = api( customizeId );
3390 if ( ! _.isEqual( update.saved_value, setting.get() ) ) {
3391 wasSaved = api.state( 'saved' ).get();
3392 setting.set( update.saved_value );
3393 setting._dirty = false;
3394 api.state( 'saved' ).set( wasSaved );
3395 }
3396 }
3397 } );
3398
3399 // Build up mapping of nav_menu_item placeholder IDs to inserted IDs.
3400 _( data.nav_menu_item_updates ).each(function( update ) {
3401 if ( update.previous_post_id ) {
3402 insertedMenuItemIdMapping[ update.previous_post_id ] = update.post_id;
3403 }
3404 });
3405
3406 _( data.nav_menu_item_updates ).each(function( update ) {
3407 var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldControl, newControl;
3408 if ( 'inserted' === update.status ) {
3409 if ( ! update.previous_post_id ) {
3410 throw new Error( 'Expected previous_post_id' );
3411 }
3412 if ( ! update.post_id ) {
3413 throw new Error( 'Expected post_id' );
3414 }
3415 oldCustomizeId = 'nav_menu_item[' + String( update.previous_post_id ) + ']';
3416 if ( ! api.has( oldCustomizeId ) ) {
3417 throw new Error( 'Expected setting to exist: ' + oldCustomizeId );
3418 }
3419 oldSetting = api( oldCustomizeId );
3420 if ( ! api.control.has( oldCustomizeId ) ) {
3421 throw new Error( 'Expected control to exist: ' + oldCustomizeId );
3422 }
3423 oldControl = api.control( oldCustomizeId );
3424
3425 settingValue = oldSetting.get();
3426 if ( ! settingValue ) {
3427 throw new Error( 'Did not expect setting to be empty (deleted).' );
3428 }
3429 settingValue = _.clone( settingValue );
3430
3431 // If the parent menu item was also inserted, update the menu_item_parent to the new ID.
3432 if ( settingValue.menu_item_parent < 0 ) {
3433 if ( ! insertedMenuItemIdMapping[ settingValue.menu_item_parent ] ) {
3434 throw new Error( 'inserted ID for menu_item_parent not available' );
3435 }
3436 settingValue.menu_item_parent = insertedMenuItemIdMapping[ settingValue.menu_item_parent ];
3437 }
3438
3439 // If the menu was also inserted, then make sure it uses the new menu ID for nav_menu_term_id.
3440 if ( insertedMenuIdMapping[ settingValue.nav_menu_term_id ] ) {
3441 settingValue.nav_menu_term_id = insertedMenuIdMapping[ settingValue.nav_menu_term_id ];
3442 }
3443
3444 newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']';
3445 newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, {
3446 type: 'nav_menu_item',
3447 transport: api.Menus.data.settingTransport,
3448 previewer: api.previewer
3449 } );
3450
3451 // Add the menu control.
3452 newControl = new api.controlConstructor.nav_menu_item( newCustomizeId, {
3453 type: 'nav_menu_item',
3454 menu_id: update.post_id,
3455 section: 'nav_menu[' + String( settingValue.nav_menu_term_id ) + ']',
3456 priority: oldControl.priority.get(),
3457 settings: {
3458 'default': newCustomizeId
3459 },
3460 menu_item_id: update.post_id
3461 } );
3462
3463 // Remove old control.
3464 oldControl.container.remove();
3465 api.control.remove( oldCustomizeId );
3466
3467 // Add new control to take its place.
3468 api.control.add( newControl );
3469
3470 // Delete the placeholder and preview the new setting.
3471 oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set.
3472 oldSetting.set( false );
3473 oldSetting.preview();
3474 newSetting.preview();
3475 oldSetting._dirty = false;
3476
3477 newControl.container.toggleClass( 'menu-item-edit-inactive', oldControl.container.hasClass( 'menu-item-edit-inactive' ) );
3478 }
3479 });
3480
3481 /*
3482 * Update the settings for any nav_menu widgets that had selected a placeholder ID.
3483 */
3484 _.each( data.widget_nav_menu_updates, function( widgetSettingValue, widgetSettingId ) {
3485 var setting = api( widgetSettingId );
3486 if ( setting ) {
3487 setting._value = widgetSettingValue;
3488 setting.preview(); // Send to the preview now so that menu refresh will use the inserted menu.
3489 }
3490 });
3491 };
3492
3493 /**
3494 * Focus a menu item control.
3495 *
3496 * @alias wp.customize.Menus.focusMenuItemControl
3497 *
3498 * @param {string} menuItemId
3499 */
3500 api.Menus.focusMenuItemControl = function( menuItemId ) {
3501 var control = api.Menus.getMenuItemControl( menuItemId );
3502 if ( control ) {
3503 control.focus();
3504 }
3505 };
3506
3507 /**
3508 * Get the control for a given menu.
3509 *
3510 * @alias wp.customize.Menus.getMenuControl
3511 *
3512 * @param menuId
3513 * @return {wp.customize.controlConstructor.menus[]}
3514 */
3515 api.Menus.getMenuControl = function( menuId ) {
3516 return api.control( 'nav_menu[' + menuId + ']' );
3517 };
3518
3519 /**
3520 * Given a menu item ID, get the control associated with it.
3521 *
3522 * @alias wp.customize.Menus.getMenuItemControl
3523 *
3524 * @param {string} menuItemId
3525 * @return {Object|null}
3526 */
3527 api.Menus.getMenuItemControl = function( menuItemId ) {
3528 return api.control( menuItemIdToSettingId( menuItemId ) );
3529 };
3530
3531 /**
3532 * @alias wp.customize.Menus~menuItemIdToSettingId
3533 *
3534 * @param {string} menuItemId
3535 */
3536 function menuItemIdToSettingId( menuItemId ) {
3537 return 'nav_menu_item[' + menuItemId + ']';
3538 }
3539
3540 /**
3541 * Apply sanitize_text_field()-like logic to the supplied name, returning a
3542 * "unnammed" fallback string if the name is then empty.
3543 *
3544 * @alias wp.customize.Menus~displayNavMenuName
3545 *
3546 * @param {string} name
3547 * @return {string}
3548 */
3549 function displayNavMenuName( name ) {
3550 name = name || '';
3551 name = wp.sanitize.stripTagsAndEncodeText( name ); // Remove any potential tags from name.
3552 name = name.toString().trim();
3553 return name || api.Menus.data.l10n.unnamed;
3554 }
3555
3556})( wp.customize, wp, jQuery );
3557
Ui Ux Design – Teachers Night Out https://cardgames4educators.com Wed, 16 Oct 2024 22:24:18 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://cardgames4educators.com/wp-content/uploads/2024/06/cropped-Card-4-Educators-logo-32x32.png Ui Ux Design – Teachers Night Out https://cardgames4educators.com 32 32 Masters In English How English Speaker https://cardgames4educators.com/masters-in-english-how-english-speaker/ https://cardgames4educators.com/masters-in-english-how-english-speaker/#comments Mon, 27 May 2024 08:54:45 +0000 https://themexriver.com/wp/kadu/?p=1

Erat himenaeos neque id sagittis massa. Hac suscipit pulvinar dignissim platea magnis eu. Don tellus a pharetra inceptos efficitur dui pulvinar. Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent pulvinar odio volutpat parturient. Quisque risus finibus suspendisse mus purus magnis facilisi condimentum consectetur dui. Curae elit suspendisse cursus vehicula.

Turpis taciti class non vel pretium quis pulvinar tempor lobortis nunc. Libero phasellus parturient sapien volutpat malesuada ornare. Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae. Porta est tempor ex eget feugiat vulputate ipsum. Justo nec iaculis habitant diam arcu fermentum.

We offer comprehen sive emplo ment services such as assistance wit employer compliance.Our company is your strategic HR partner as instead of HR. john smithson

Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae.

Exploring Learning Landscapes in Academic

Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent.

]]>
https://cardgames4educators.com/masters-in-english-how-english-speaker/feed/ 1