customize-nav-menus.js 96 KB

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