customize-nav-menus.js 106 KB

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