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