customize-nav-menus.js 106 KB

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