customize-nav-menus.js 105 KB

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