Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 

2282 rader
67 KiB

  1. /* global _wpCustomizeWidgetsSettings */
  2. (function( wp, $ ){
  3. if ( ! wp || ! wp.customize ) { return; }
  4. // Set up our namespace...
  5. var api = wp.customize,
  6. l10n;
  7. api.Widgets = api.Widgets || {};
  8. api.Widgets.savedWidgetIds = {};
  9. // Link settings
  10. api.Widgets.data = _wpCustomizeWidgetsSettings || {};
  11. l10n = api.Widgets.data.l10n;
  12. delete api.Widgets.data.l10n;
  13. /**
  14. * wp.customize.Widgets.WidgetModel
  15. *
  16. * A single widget model.
  17. *
  18. * @constructor
  19. * @augments Backbone.Model
  20. */
  21. api.Widgets.WidgetModel = Backbone.Model.extend({
  22. id: null,
  23. temp_id: null,
  24. classname: null,
  25. control_tpl: null,
  26. description: null,
  27. is_disabled: null,
  28. is_multi: null,
  29. multi_number: null,
  30. name: null,
  31. id_base: null,
  32. transport: null,
  33. params: [],
  34. width: null,
  35. height: null,
  36. search_matched: true
  37. });
  38. /**
  39. * wp.customize.Widgets.WidgetCollection
  40. *
  41. * Collection for widget models.
  42. *
  43. * @constructor
  44. * @augments Backbone.Model
  45. */
  46. api.Widgets.WidgetCollection = Backbone.Collection.extend({
  47. model: api.Widgets.WidgetModel,
  48. // Controls searching on the current widget collection
  49. // and triggers an update event
  50. doSearch: function( value ) {
  51. // Don't do anything if we've already done this search
  52. // Useful because the search handler fires multiple times per keystroke
  53. if ( this.terms === value ) {
  54. return;
  55. }
  56. // Updates terms with the value passed
  57. this.terms = value;
  58. // If we have terms, run a search...
  59. if ( this.terms.length > 0 ) {
  60. this.search( this.terms );
  61. }
  62. // If search is blank, set all the widgets as they matched the search to reset the views.
  63. if ( this.terms === '' ) {
  64. this.each( function ( widget ) {
  65. widget.set( 'search_matched', true );
  66. } );
  67. }
  68. },
  69. // Performs a search within the collection
  70. // @uses RegExp
  71. search: function( term ) {
  72. var match, haystack;
  73. // Escape the term string for RegExp meta characters
  74. term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
  75. // Consider spaces as word delimiters and match the whole string
  76. // so matching terms can be combined
  77. term = term.replace( / /g, ')(?=.*' );
  78. match = new RegExp( '^(?=.*' + term + ').+', 'i' );
  79. this.each( function ( data ) {
  80. haystack = [ data.get( 'name' ), data.get( 'id' ), data.get( 'description' ) ].join( ' ' );
  81. data.set( 'search_matched', match.test( haystack ) );
  82. } );
  83. }
  84. });
  85. api.Widgets.availableWidgets = new api.Widgets.WidgetCollection( api.Widgets.data.availableWidgets );
  86. /**
  87. * wp.customize.Widgets.SidebarModel
  88. *
  89. * A single sidebar model.
  90. *
  91. * @constructor
  92. * @augments Backbone.Model
  93. */
  94. api.Widgets.SidebarModel = Backbone.Model.extend({
  95. after_title: null,
  96. after_widget: null,
  97. before_title: null,
  98. before_widget: null,
  99. 'class': null,
  100. description: null,
  101. id: null,
  102. name: null,
  103. is_rendered: false
  104. });
  105. /**
  106. * wp.customize.Widgets.SidebarCollection
  107. *
  108. * Collection for sidebar models.
  109. *
  110. * @constructor
  111. * @augments Backbone.Collection
  112. */
  113. api.Widgets.SidebarCollection = Backbone.Collection.extend({
  114. model: api.Widgets.SidebarModel
  115. });
  116. api.Widgets.registeredSidebars = new api.Widgets.SidebarCollection( api.Widgets.data.registeredSidebars );
  117. /**
  118. * wp.customize.Widgets.AvailableWidgetsPanelView
  119. *
  120. * View class for the available widgets panel.
  121. *
  122. * @constructor
  123. * @augments wp.Backbone.View
  124. * @augments Backbone.View
  125. */
  126. api.Widgets.AvailableWidgetsPanelView = wp.Backbone.View.extend({
  127. el: '#available-widgets',
  128. events: {
  129. 'input #widgets-search': 'search',
  130. 'keyup #widgets-search': 'search',
  131. 'focus .widget-tpl' : 'focus',
  132. 'click .widget-tpl' : '_submit',
  133. 'keypress .widget-tpl' : '_submit',
  134. 'keydown' : 'keyboardAccessible'
  135. },
  136. // Cache current selected widget
  137. selected: null,
  138. // Cache sidebar control which has opened panel
  139. currentSidebarControl: null,
  140. $search: null,
  141. $clearResults: null,
  142. searchMatchesCount: null,
  143. initialize: function() {
  144. var self = this;
  145. this.$search = $( '#widgets-search' );
  146. this.$clearResults = this.$el.find( '.clear-results' );
  147. _.bindAll( this, 'close' );
  148. this.listenTo( this.collection, 'change', this.updateList );
  149. this.updateList();
  150. // Set the initial search count to the number of available widgets.
  151. this.searchMatchesCount = this.collection.length;
  152. // If the available widgets panel is open and the customize controls are
  153. // interacted with (i.e. available widgets panel is blurred) then close the
  154. // available widgets panel. Also close on back button click.
  155. $( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) {
  156. var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
  157. if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
  158. self.close();
  159. }
  160. } );
  161. // Clear the search results and trigger a `keyup` event to fire a new search.
  162. this.$clearResults.on( 'click', function() {
  163. self.$search.val( '' ).focus().trigger( 'keyup' );
  164. } );
  165. // Close the panel if the URL in the preview changes
  166. api.previewer.bind( 'url', this.close );
  167. },
  168. // Performs a search and handles selected widget
  169. search: function( event ) {
  170. var firstVisible;
  171. this.collection.doSearch( event.target.value );
  172. // Update the search matches count.
  173. this.updateSearchMatchesCount();
  174. // Announce how many search results.
  175. this.announceSearchMatches();
  176. // Remove a widget from being selected if it is no longer visible
  177. if ( this.selected && ! this.selected.is( ':visible' ) ) {
  178. this.selected.removeClass( 'selected' );
  179. this.selected = null;
  180. }
  181. // If a widget was selected but the filter value has been cleared out, clear selection
  182. if ( this.selected && ! event.target.value ) {
  183. this.selected.removeClass( 'selected' );
  184. this.selected = null;
  185. }
  186. // If a filter has been entered and a widget hasn't been selected, select the first one shown
  187. if ( ! this.selected && event.target.value ) {
  188. firstVisible = this.$el.find( '> .widget-tpl:visible:first' );
  189. if ( firstVisible.length ) {
  190. this.select( firstVisible );
  191. }
  192. }
  193. // Toggle the clear search results button.
  194. if ( '' !== event.target.value ) {
  195. this.$clearResults.addClass( 'is-visible' );
  196. } else if ( '' === event.target.value ) {
  197. this.$clearResults.removeClass( 'is-visible' );
  198. }
  199. // Set a CSS class on the search container when there are no search results.
  200. if ( ! this.searchMatchesCount ) {
  201. this.$el.addClass( 'no-widgets-found' );
  202. } else {
  203. this.$el.removeClass( 'no-widgets-found' );
  204. }
  205. },
  206. // Update the count of the available widgets that have the `search_matched` attribute.
  207. updateSearchMatchesCount: function() {
  208. this.searchMatchesCount = this.collection.where({ search_matched: true }).length;
  209. },
  210. // Send a message to the aria-live region to announce how many search results.
  211. announceSearchMatches: _.debounce( function() {
  212. var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ;
  213. if ( ! this.searchMatchesCount ) {
  214. message = l10n.noWidgetsFound;
  215. }
  216. wp.a11y.speak( message );
  217. }, 500 ),
  218. // Changes visibility of available widgets
  219. updateList: function() {
  220. this.collection.each( function( widget ) {
  221. var widgetTpl = $( '#widget-tpl-' + widget.id );
  222. widgetTpl.toggle( widget.get( 'search_matched' ) && ! widget.get( 'is_disabled' ) );
  223. if ( widget.get( 'is_disabled' ) && widgetTpl.is( this.selected ) ) {
  224. this.selected = null;
  225. }
  226. } );
  227. },
  228. // Highlights a widget
  229. select: function( widgetTpl ) {
  230. this.selected = $( widgetTpl );
  231. this.selected.siblings( '.widget-tpl' ).removeClass( 'selected' );
  232. this.selected.addClass( 'selected' );
  233. },
  234. // Highlights a widget on focus
  235. focus: function( event ) {
  236. this.select( $( event.currentTarget ) );
  237. },
  238. // Submit handler for keypress and click on widget
  239. _submit: function( event ) {
  240. // Only proceed with keypress if it is Enter or Spacebar
  241. if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
  242. return;
  243. }
  244. this.submit( $( event.currentTarget ) );
  245. },
  246. // Adds a selected widget to the sidebar
  247. submit: function( widgetTpl ) {
  248. var widgetId, widget, widgetFormControl;
  249. if ( ! widgetTpl ) {
  250. widgetTpl = this.selected;
  251. }
  252. if ( ! widgetTpl || ! this.currentSidebarControl ) {
  253. return;
  254. }
  255. this.select( widgetTpl );
  256. widgetId = $( this.selected ).data( 'widget-id' );
  257. widget = this.collection.findWhere( { id: widgetId } );
  258. if ( ! widget ) {
  259. return;
  260. }
  261. widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) );
  262. if ( widgetFormControl ) {
  263. widgetFormControl.focus();
  264. }
  265. this.close();
  266. },
  267. // Opens the panel
  268. open: function( sidebarControl ) {
  269. this.currentSidebarControl = sidebarControl;
  270. // Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens
  271. _( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
  272. if ( control.params.is_wide ) {
  273. control.collapseForm();
  274. }
  275. } );
  276. $( 'body' ).addClass( 'adding-widget' );
  277. this.$el.find( '.selected' ).removeClass( 'selected' );
  278. // Reset search
  279. this.collection.doSearch( '' );
  280. if ( ! api.settings.browser.mobile ) {
  281. this.$search.focus();
  282. }
  283. },
  284. // Closes the panel
  285. close: function( options ) {
  286. options = options || {};
  287. if ( options.returnFocus && this.currentSidebarControl ) {
  288. this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
  289. }
  290. this.currentSidebarControl = null;
  291. this.selected = null;
  292. $( 'body' ).removeClass( 'adding-widget' );
  293. this.$search.val( '' );
  294. },
  295. // Add keyboard accessiblity to the panel
  296. keyboardAccessible: function( event ) {
  297. var isEnter = ( event.which === 13 ),
  298. isEsc = ( event.which === 27 ),
  299. isDown = ( event.which === 40 ),
  300. isUp = ( event.which === 38 ),
  301. isTab = ( event.which === 9 ),
  302. isShift = ( event.shiftKey ),
  303. selected = null,
  304. firstVisible = this.$el.find( '> .widget-tpl:visible:first' ),
  305. lastVisible = this.$el.find( '> .widget-tpl:visible:last' ),
  306. isSearchFocused = $( event.target ).is( this.$search ),
  307. isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' );
  308. if ( isDown || isUp ) {
  309. if ( isDown ) {
  310. if ( isSearchFocused ) {
  311. selected = firstVisible;
  312. } else if ( this.selected && this.selected.nextAll( '.widget-tpl:visible' ).length !== 0 ) {
  313. selected = this.selected.nextAll( '.widget-tpl:visible:first' );
  314. }
  315. } else if ( isUp ) {
  316. if ( isSearchFocused ) {
  317. selected = lastVisible;
  318. } else if ( this.selected && this.selected.prevAll( '.widget-tpl:visible' ).length !== 0 ) {
  319. selected = this.selected.prevAll( '.widget-tpl:visible:first' );
  320. }
  321. }
  322. this.select( selected );
  323. if ( selected ) {
  324. selected.focus();
  325. } else {
  326. this.$search.focus();
  327. }
  328. return;
  329. }
  330. // If enter pressed but nothing entered, don't do anything
  331. if ( isEnter && ! this.$search.val() ) {
  332. return;
  333. }
  334. if ( isEnter ) {
  335. this.submit();
  336. } else if ( isEsc ) {
  337. this.close( { returnFocus: true } );
  338. }
  339. if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
  340. this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
  341. event.preventDefault();
  342. }
  343. }
  344. });
  345. /**
  346. * Handlers for the widget-synced event, organized by widget ID base.
  347. * Other widgets may provide their own update handlers by adding
  348. * listeners for the widget-synced event.
  349. */
  350. api.Widgets.formSyncHandlers = {
  351. /**
  352. * @param {jQuery.Event} e
  353. * @param {jQuery} widget
  354. * @param {String} newForm
  355. */
  356. rss: function( e, widget, newForm ) {
  357. var oldWidgetError = widget.find( '.widget-error:first' ),
  358. newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' );
  359. if ( oldWidgetError.length && newWidgetError.length ) {
  360. oldWidgetError.replaceWith( newWidgetError );
  361. } else if ( oldWidgetError.length ) {
  362. oldWidgetError.remove();
  363. } else if ( newWidgetError.length ) {
  364. widget.find( '.widget-content:first' ).prepend( newWidgetError );
  365. }
  366. }
  367. };
  368. /**
  369. * wp.customize.Widgets.WidgetControl
  370. *
  371. * Customizer control for widgets.
  372. * Note that 'widget_form' must match the WP_Widget_Form_Customize_Control::$type
  373. *
  374. * @constructor
  375. * @augments wp.customize.Control
  376. */
  377. api.Widgets.WidgetControl = api.Control.extend({
  378. defaultExpandedArguments: {
  379. duration: 'fast',
  380. completeCallback: $.noop
  381. },
  382. /**
  383. * @since 4.1.0
  384. */
  385. initialize: function( id, options ) {
  386. var control = this;
  387. control.widgetControlEmbedded = false;
  388. control.widgetContentEmbedded = false;
  389. control.expanded = new api.Value( false );
  390. control.expandedArgumentsQueue = [];
  391. control.expanded.bind( function( expanded ) {
  392. var args = control.expandedArgumentsQueue.shift();
  393. args = $.extend( {}, control.defaultExpandedArguments, args );
  394. control.onChangeExpanded( expanded, args );
  395. });
  396. control.altNotice = true;
  397. api.Control.prototype.initialize.call( control, id, options );
  398. },
  399. /**
  400. * Set up the control.
  401. *
  402. * @since 3.9.0
  403. */
  404. ready: function() {
  405. var control = this;
  406. /*
  407. * Embed a placeholder once the section is expanded. The full widget
  408. * form content will be embedded once the control itself is expanded,
  409. * and at this point the widget-added event will be triggered.
  410. */
  411. if ( ! control.section() ) {
  412. control.embedWidgetControl();
  413. } else {
  414. api.section( control.section(), function( section ) {
  415. var onExpanded = function( isExpanded ) {
  416. if ( isExpanded ) {
  417. control.embedWidgetControl();
  418. section.expanded.unbind( onExpanded );
  419. }
  420. };
  421. if ( section.expanded() ) {
  422. onExpanded( true );
  423. } else {
  424. section.expanded.bind( onExpanded );
  425. }
  426. } );
  427. }
  428. },
  429. /**
  430. * Embed the .widget element inside the li container.
  431. *
  432. * @since 4.4.0
  433. */
  434. embedWidgetControl: function() {
  435. var control = this, widgetControl;
  436. if ( control.widgetControlEmbedded ) {
  437. return;
  438. }
  439. control.widgetControlEmbedded = true;
  440. widgetControl = $( control.params.widget_control );
  441. control.container.append( widgetControl );
  442. control._setupModel();
  443. control._setupWideWidget();
  444. control._setupControlToggle();
  445. control._setupWidgetTitle();
  446. control._setupReorderUI();
  447. control._setupHighlightEffects();
  448. control._setupUpdateUI();
  449. control._setupRemoveUI();
  450. },
  451. /**
  452. * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event.
  453. *
  454. * @since 4.4.0
  455. */
  456. embedWidgetContent: function() {
  457. var control = this, widgetContent;
  458. control.embedWidgetControl();
  459. if ( control.widgetContentEmbedded ) {
  460. return;
  461. }
  462. control.widgetContentEmbedded = true;
  463. widgetContent = $( control.params.widget_content );
  464. control.container.find( '.widget-content:first' ).append( widgetContent );
  465. /*
  466. * Trigger widget-added event so that plugins can attach any event
  467. * listeners and dynamic UI elements.
  468. */
  469. $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] );
  470. },
  471. /**
  472. * Handle changes to the setting
  473. */
  474. _setupModel: function() {
  475. var self = this, rememberSavedWidgetId;
  476. // Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
  477. rememberSavedWidgetId = function() {
  478. api.Widgets.savedWidgetIds[self.params.widget_id] = true;
  479. };
  480. api.bind( 'ready', rememberSavedWidgetId );
  481. api.bind( 'saved', rememberSavedWidgetId );
  482. this._updateCount = 0;
  483. this.isWidgetUpdating = false;
  484. this.liveUpdateMode = true;
  485. // Update widget whenever model changes
  486. this.setting.bind( function( to, from ) {
  487. if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) {
  488. self.updateWidget( { instance: to } );
  489. }
  490. } );
  491. },
  492. /**
  493. * Add special behaviors for wide widget controls
  494. */
  495. _setupWideWidget: function() {
  496. var self = this, $widgetInside, $widgetForm, $customizeSidebar,
  497. $themeControlsContainer, positionWidget;
  498. if ( ! this.params.is_wide ) {
  499. return;
  500. }
  501. $widgetInside = this.container.find( '.widget-inside' );
  502. $widgetForm = $widgetInside.find( '> .form' );
  503. $customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
  504. this.container.addClass( 'wide-widget-control' );
  505. this.container.find( '.widget-content:first' ).css( {
  506. 'max-width': this.params.width,
  507. 'min-height': this.params.height
  508. } );
  509. /**
  510. * Keep the widget-inside positioned so the top of fixed-positioned
  511. * element is at the same top position as the widget-top. When the
  512. * widget-top is scrolled out of view, keep the widget-top in view;
  513. * likewise, don't allow the widget to drop off the bottom of the window.
  514. * If a widget is too tall to fit in the window, don't let the height
  515. * exceed the window height so that the contents of the widget control
  516. * will become scrollable (overflow:auto).
  517. */
  518. positionWidget = function() {
  519. var offsetTop = self.container.offset().top,
  520. windowHeight = $( window ).height(),
  521. formHeight = $widgetForm.outerHeight(),
  522. top;
  523. $widgetInside.css( 'max-height', windowHeight );
  524. top = Math.max(
  525. 0, // prevent top from going off screen
  526. Math.min(
  527. Math.max( offsetTop, 0 ), // distance widget in panel is from top of screen
  528. windowHeight - formHeight // flush up against bottom of screen
  529. )
  530. );
  531. $widgetInside.css( 'top', top );
  532. };
  533. $themeControlsContainer = $( '#customize-theme-controls' );
  534. this.container.on( 'expand', function() {
  535. positionWidget();
  536. $customizeSidebar.on( 'scroll', positionWidget );
  537. $( window ).on( 'resize', positionWidget );
  538. $themeControlsContainer.on( 'expanded collapsed', positionWidget );
  539. } );
  540. this.container.on( 'collapsed', function() {
  541. $customizeSidebar.off( 'scroll', positionWidget );
  542. $( window ).off( 'resize', positionWidget );
  543. $themeControlsContainer.off( 'expanded collapsed', positionWidget );
  544. } );
  545. // Reposition whenever a sidebar's widgets are changed
  546. api.each( function( setting ) {
  547. if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
  548. setting.bind( function() {
  549. if ( self.container.hasClass( 'expanded' ) ) {
  550. positionWidget();
  551. }
  552. } );
  553. }
  554. } );
  555. },
  556. /**
  557. * Show/hide the control when clicking on the form title, when clicking
  558. * the close button
  559. */
  560. _setupControlToggle: function() {
  561. var self = this, $closeBtn;
  562. this.container.find( '.widget-top' ).on( 'click', function( e ) {
  563. e.preventDefault();
  564. var sidebarWidgetsControl = self.getSidebarWidgetsControl();
  565. if ( sidebarWidgetsControl.isReordering ) {
  566. return;
  567. }
  568. self.expanded( ! self.expanded() );
  569. } );
  570. $closeBtn = this.container.find( '.widget-control-close' );
  571. $closeBtn.on( 'click', function( e ) {
  572. e.preventDefault();
  573. self.collapse();
  574. self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
  575. } );
  576. },
  577. /**
  578. * Update the title of the form if a title field is entered
  579. */
  580. _setupWidgetTitle: function() {
  581. var self = this, updateTitle;
  582. updateTitle = function() {
  583. var title = self.setting().title,
  584. inWidgetTitle = self.container.find( '.in-widget-title' );
  585. if ( title ) {
  586. inWidgetTitle.text( ': ' + title );
  587. } else {
  588. inWidgetTitle.text( '' );
  589. }
  590. };
  591. this.setting.bind( updateTitle );
  592. updateTitle();
  593. },
  594. /**
  595. * Set up the widget-reorder-nav
  596. */
  597. _setupReorderUI: function() {
  598. var self = this, selectSidebarItem, $moveWidgetArea,
  599. $reorderNav, updateAvailableSidebars, template;
  600. /**
  601. * select the provided sidebar list item in the move widget area
  602. *
  603. * @param {jQuery} li
  604. */
  605. selectSidebarItem = function( li ) {
  606. li.siblings( '.selected' ).removeClass( 'selected' );
  607. li.addClass( 'selected' );
  608. var isSelfSidebar = ( li.data( 'id' ) === self.params.sidebar_id );
  609. self.container.find( '.move-widget-btn' ).prop( 'disabled', isSelfSidebar );
  610. };
  611. /**
  612. * Add the widget reordering elements to the widget control
  613. */
  614. this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
  615. template = _.template( api.Widgets.data.tpl.moveWidgetArea );
  616. $moveWidgetArea = $( template( {
  617. sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
  618. } )
  619. );
  620. this.container.find( '.widget-top' ).after( $moveWidgetArea );
  621. /**
  622. * Update available sidebars when their rendered state changes
  623. */
  624. updateAvailableSidebars = function() {
  625. var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem,
  626. renderedSidebarCount = 0;
  627. selfSidebarItem = $sidebarItems.filter( function(){
  628. return $( this ).data( 'id' ) === self.params.sidebar_id;
  629. } );
  630. $sidebarItems.each( function() {
  631. var li = $( this ),
  632. sidebarId, sidebar, sidebarIsRendered;
  633. sidebarId = li.data( 'id' );
  634. sidebar = api.Widgets.registeredSidebars.get( sidebarId );
  635. sidebarIsRendered = sidebar.get( 'is_rendered' );
  636. li.toggle( sidebarIsRendered );
  637. if ( sidebarIsRendered ) {
  638. renderedSidebarCount += 1;
  639. }
  640. if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) {
  641. selectSidebarItem( selfSidebarItem );
  642. }
  643. } );
  644. if ( renderedSidebarCount > 1 ) {
  645. self.container.find( '.move-widget' ).show();
  646. } else {
  647. self.container.find( '.move-widget' ).hide();
  648. }
  649. };
  650. updateAvailableSidebars();
  651. api.Widgets.registeredSidebars.on( 'change:is_rendered', updateAvailableSidebars );
  652. /**
  653. * Handle clicks for up/down/move on the reorder nav
  654. */
  655. $reorderNav = this.container.find( '.widget-reorder-nav' );
  656. $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() {
  657. $( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' );
  658. } ).on( 'click keypress', function( event ) {
  659. if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
  660. return;
  661. }
  662. $( this ).focus();
  663. if ( $( this ).is( '.move-widget' ) ) {
  664. self.toggleWidgetMoveArea();
  665. } else {
  666. var isMoveDown = $( this ).is( '.move-widget-down' ),
  667. isMoveUp = $( this ).is( '.move-widget-up' ),
  668. i = self.getWidgetSidebarPosition();
  669. if ( ( isMoveUp && i === 0 ) || ( isMoveDown && i === self.getSidebarWidgetsControl().setting().length - 1 ) ) {
  670. return;
  671. }
  672. if ( isMoveUp ) {
  673. self.moveUp();
  674. wp.a11y.speak( l10n.widgetMovedUp );
  675. } else {
  676. self.moveDown();
  677. wp.a11y.speak( l10n.widgetMovedDown );
  678. }
  679. $( this ).focus(); // re-focus after the container was moved
  680. }
  681. } );
  682. /**
  683. * Handle selecting a sidebar to move to
  684. */
  685. this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) {
  686. if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
  687. return;
  688. }
  689. event.preventDefault();
  690. selectSidebarItem( $( this ) );
  691. } );
  692. /**
  693. * Move widget to another sidebar
  694. */
  695. this.container.find( '.move-widget-btn' ).click( function() {
  696. self.getSidebarWidgetsControl().toggleReordering( false );
  697. var oldSidebarId = self.params.sidebar_id,
  698. newSidebarId = self.container.find( '.widget-area-select li.selected' ).data( 'id' ),
  699. oldSidebarWidgetsSetting, newSidebarWidgetsSetting,
  700. oldSidebarWidgetIds, newSidebarWidgetIds, i;
  701. oldSidebarWidgetsSetting = api( 'sidebars_widgets[' + oldSidebarId + ']' );
  702. newSidebarWidgetsSetting = api( 'sidebars_widgets[' + newSidebarId + ']' );
  703. oldSidebarWidgetIds = Array.prototype.slice.call( oldSidebarWidgetsSetting() );
  704. newSidebarWidgetIds = Array.prototype.slice.call( newSidebarWidgetsSetting() );
  705. i = self.getWidgetSidebarPosition();
  706. oldSidebarWidgetIds.splice( i, 1 );
  707. newSidebarWidgetIds.push( self.params.widget_id );
  708. oldSidebarWidgetsSetting( oldSidebarWidgetIds );
  709. newSidebarWidgetsSetting( newSidebarWidgetIds );
  710. self.focus();
  711. } );
  712. },
  713. /**
  714. * Highlight widgets in preview when interacted with in the Customizer
  715. */
  716. _setupHighlightEffects: function() {
  717. var self = this;
  718. // Highlight whenever hovering or clicking over the form
  719. this.container.on( 'mouseenter click', function() {
  720. self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
  721. } );
  722. // Highlight when the setting is updated
  723. this.setting.bind( function() {
  724. self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
  725. } );
  726. },
  727. /**
  728. * Set up event handlers for widget updating
  729. */
  730. _setupUpdateUI: function() {
  731. var self = this, $widgetRoot, $widgetContent,
  732. $saveBtn, updateWidgetDebounced, formSyncHandler;
  733. $widgetRoot = this.container.find( '.widget:first' );
  734. $widgetContent = $widgetRoot.find( '.widget-content:first' );
  735. // Configure update button
  736. $saveBtn = this.container.find( '.widget-control-save' );
  737. $saveBtn.val( l10n.saveBtnLabel );
  738. $saveBtn.attr( 'title', l10n.saveBtnTooltip );
  739. $saveBtn.removeClass( 'button-primary' );
  740. $saveBtn.on( 'click', function( e ) {
  741. e.preventDefault();
  742. self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
  743. } );
  744. updateWidgetDebounced = _.debounce( function() {
  745. self.updateWidget();
  746. }, 250 );
  747. // Trigger widget form update when hitting Enter within an input
  748. $widgetContent.on( 'keydown', 'input', function( e ) {
  749. if ( 13 === e.which ) { // Enter
  750. e.preventDefault();
  751. self.updateWidget( { ignoreActiveElement: true } );
  752. }
  753. } );
  754. // Handle widgets that support live previews
  755. $widgetContent.on( 'change input propertychange', ':input', function( e ) {
  756. if ( ! self.liveUpdateMode ) {
  757. return;
  758. }
  759. if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
  760. updateWidgetDebounced();
  761. }
  762. } );
  763. // Remove loading indicators when the setting is saved and the preview updates
  764. this.setting.previewer.channel.bind( 'synced', function() {
  765. self.container.removeClass( 'previewer-loading' );
  766. } );
  767. api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
  768. if ( updatedWidgetId === self.params.widget_id ) {
  769. self.container.removeClass( 'previewer-loading' );
  770. }
  771. } );
  772. formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ];
  773. if ( formSyncHandler ) {
  774. $( document ).on( 'widget-synced', function( e, widget ) {
  775. if ( $widgetRoot.is( widget ) ) {
  776. formSyncHandler.apply( document, arguments );
  777. }
  778. } );
  779. }
  780. },
  781. /**
  782. * Update widget control to indicate whether it is currently rendered.
  783. *
  784. * Overrides api.Control.toggle()
  785. *
  786. * @since 4.1.0
  787. *
  788. * @param {Boolean} active
  789. * @param {Object} args
  790. * @param {Callback} args.completeCallback
  791. */
  792. onChangeActive: function ( active, args ) {
  793. // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments
  794. this.container.toggleClass( 'widget-rendered', active );
  795. if ( args.completeCallback ) {
  796. args.completeCallback();
  797. }
  798. },
  799. /**
  800. * Set up event handlers for widget removal
  801. */
  802. _setupRemoveUI: function() {
  803. var self = this, $removeBtn, replaceDeleteWithRemove;
  804. // Configure remove button
  805. $removeBtn = this.container.find( 'a.widget-control-remove' );
  806. $removeBtn.on( 'click', function( e ) {
  807. e.preventDefault();
  808. // Find an adjacent element to add focus to when this widget goes away
  809. var $adjacentFocusTarget;
  810. if ( self.container.next().is( '.customize-control-widget_form' ) ) {
  811. $adjacentFocusTarget = self.container.next().find( '.widget-action:first' );
  812. } else if ( self.container.prev().is( '.customize-control-widget_form' ) ) {
  813. $adjacentFocusTarget = self.container.prev().find( '.widget-action:first' );
  814. } else {
  815. $adjacentFocusTarget = self.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
  816. }
  817. self.container.slideUp( function() {
  818. var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ),
  819. sidebarWidgetIds, i;
  820. if ( ! sidebarsWidgetsControl ) {
  821. return;
  822. }
  823. sidebarWidgetIds = sidebarsWidgetsControl.setting().slice();
  824. i = _.indexOf( sidebarWidgetIds, self.params.widget_id );
  825. if ( -1 === i ) {
  826. return;
  827. }
  828. sidebarWidgetIds.splice( i, 1 );
  829. sidebarsWidgetsControl.setting( sidebarWidgetIds );
  830. $adjacentFocusTarget.focus(); // keyboard accessibility
  831. } );
  832. } );
  833. replaceDeleteWithRemove = function() {
  834. $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Delete"
  835. $removeBtn.attr( 'title', l10n.removeBtnTooltip );
  836. };
  837. if ( this.params.is_new ) {
  838. api.bind( 'saved', replaceDeleteWithRemove );
  839. } else {
  840. replaceDeleteWithRemove();
  841. }
  842. },
  843. /**
  844. * Find all inputs in a widget container that should be considered when
  845. * comparing the loaded form with the sanitized form, whose fields will
  846. * be aligned to copy the sanitized over. The elements returned by this
  847. * are passed into this._getInputsSignature(), and they are iterated
  848. * over when copying sanitized values over to the form loaded.
  849. *
  850. * @param {jQuery} container element in which to look for inputs
  851. * @returns {jQuery} inputs
  852. * @private
  853. */
  854. _getInputs: function( container ) {
  855. return $( container ).find( ':input[name]' );
  856. },
  857. /**
  858. * Iterate over supplied inputs and create a signature string for all of them together.
  859. * This string can be used to compare whether or not the form has all of the same fields.
  860. *
  861. * @param {jQuery} inputs
  862. * @returns {string}
  863. * @private
  864. */
  865. _getInputsSignature: function( inputs ) {
  866. var inputsSignatures = _( inputs ).map( function( input ) {
  867. var $input = $( input ), signatureParts;
  868. if ( $input.is( ':checkbox, :radio' ) ) {
  869. signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ), $input.prop( 'value' ) ];
  870. } else {
  871. signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ) ];
  872. }
  873. return signatureParts.join( ',' );
  874. } );
  875. return inputsSignatures.join( ';' );
  876. },
  877. /**
  878. * Get the state for an input depending on its type.
  879. *
  880. * @param {jQuery|Element} input
  881. * @returns {string|boolean|array|*}
  882. * @private
  883. */
  884. _getInputState: function( input ) {
  885. input = $( input );
  886. if ( input.is( ':radio, :checkbox' ) ) {
  887. return input.prop( 'checked' );
  888. } else if ( input.is( 'select[multiple]' ) ) {
  889. return input.find( 'option:selected' ).map( function () {
  890. return $( this ).val();
  891. } ).get();
  892. } else {
  893. return input.val();
  894. }
  895. },
  896. /**
  897. * Update an input's state based on its type.
  898. *
  899. * @param {jQuery|Element} input
  900. * @param {string|boolean|array|*} state
  901. * @private
  902. */
  903. _setInputState: function ( input, state ) {
  904. input = $( input );
  905. if ( input.is( ':radio, :checkbox' ) ) {
  906. input.prop( 'checked', state );
  907. } else if ( input.is( 'select[multiple]' ) ) {
  908. if ( ! $.isArray( state ) ) {
  909. state = [];
  910. } else {
  911. // Make sure all state items are strings since the DOM value is a string
  912. state = _.map( state, function ( value ) {
  913. return String( value );
  914. } );
  915. }
  916. input.find( 'option' ).each( function () {
  917. $( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) );
  918. } );
  919. } else {
  920. input.val( state );
  921. }
  922. },
  923. /***********************************************************************
  924. * Begin public API methods
  925. **********************************************************************/
  926. /**
  927. * @return {wp.customize.controlConstructor.sidebar_widgets[]}
  928. */
  929. getSidebarWidgetsControl: function() {
  930. var settingId, sidebarWidgetsControl;
  931. settingId = 'sidebars_widgets[' + this.params.sidebar_id + ']';
  932. sidebarWidgetsControl = api.control( settingId );
  933. if ( ! sidebarWidgetsControl ) {
  934. return;
  935. }
  936. return sidebarWidgetsControl;
  937. },
  938. /**
  939. * Submit the widget form via Ajax and get back the updated instance,
  940. * along with the new widget control form to render.
  941. *
  942. * @param {object} [args]
  943. * @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used
  944. * @param {Function|null} [args.complete=null] Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success.
  945. * @param {Boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element.
  946. */
  947. updateWidget: function( args ) {
  948. var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
  949. updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
  950. // The updateWidget logic requires that the form fields to be fully present.
  951. self.embedWidgetContent();
  952. args = $.extend( {
  953. instance: null,
  954. complete: null,
  955. ignoreActiveElement: false
  956. }, args );
  957. instanceOverride = args.instance;
  958. completeCallback = args.complete;
  959. this._updateCount += 1;
  960. updateNumber = this._updateCount;
  961. $widgetRoot = this.container.find( '.widget:first' );
  962. $widgetContent = $widgetRoot.find( '.widget-content:first' );
  963. // Remove a previous error message
  964. $widgetContent.find( '.widget-error' ).remove();
  965. this.container.addClass( 'widget-form-loading' );
  966. this.container.addClass( 'previewer-loading' );
  967. processing = api.state( 'processing' );
  968. processing( processing() + 1 );
  969. if ( ! this.liveUpdateMode ) {
  970. this.container.addClass( 'widget-form-disabled' );
  971. }
  972. params = {};
  973. params.action = 'update-widget';
  974. params.wp_customize = 'on';
  975. params.nonce = api.settings.nonce['update-widget'];
  976. params.customize_theme = api.settings.theme.stylesheet;
  977. params.customized = wp.customize.previewer.query().customized;
  978. data = $.param( params );
  979. $inputs = this._getInputs( $widgetContent );
  980. // Store the value we're submitting in data so that when the response comes back,
  981. // we know if it got sanitized; if there is no difference in the sanitized value,
  982. // then we do not need to touch the UI and mess up the user's ongoing editing.
  983. $inputs.each( function() {
  984. $( this ).data( 'state' + updateNumber, self._getInputState( this ) );
  985. } );
  986. if ( instanceOverride ) {
  987. data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } );
  988. } else {
  989. data += '&' + $inputs.serialize();
  990. }
  991. data += '&' + $widgetContent.find( '~ :input' ).serialize();
  992. if ( this._previousUpdateRequest ) {
  993. this._previousUpdateRequest.abort();
  994. }
  995. jqxhr = $.post( wp.ajax.settings.url, data );
  996. this._previousUpdateRequest = jqxhr;
  997. jqxhr.done( function( r ) {
  998. var message, sanitizedForm, $sanitizedInputs, hasSameInputsInResponse,
  999. isLiveUpdateAborted = false;
  1000. // Check if the user is logged out.
  1001. if ( '0' === r ) {
  1002. api.previewer.preview.iframe.hide();
  1003. api.previewer.login().done( function() {
  1004. self.updateWidget( args );
  1005. api.previewer.preview.iframe.show();
  1006. } );
  1007. return;
  1008. }
  1009. // Check for cheaters.
  1010. if ( '-1' === r ) {
  1011. api.previewer.cheatin();
  1012. return;
  1013. }
  1014. if ( r.success ) {
  1015. sanitizedForm = $( '<div>' + r.data.form + '</div>' );
  1016. $sanitizedInputs = self._getInputs( sanitizedForm );
  1017. hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs );
  1018. // Restore live update mode if sanitized fields are now aligned with the existing fields
  1019. if ( hasSameInputsInResponse && ! self.liveUpdateMode ) {
  1020. self.liveUpdateMode = true;
  1021. self.container.removeClass( 'widget-form-disabled' );
  1022. self.container.find( 'input[name="savewidget"]' ).hide();
  1023. }
  1024. // Sync sanitized field states to existing fields if they are aligned
  1025. if ( hasSameInputsInResponse && self.liveUpdateMode ) {
  1026. $inputs.each( function( i ) {
  1027. var $input = $( this ),
  1028. $sanitizedInput = $( $sanitizedInputs[i] ),
  1029. submittedState, sanitizedState, canUpdateState;
  1030. submittedState = $input.data( 'state' + updateNumber );
  1031. sanitizedState = self._getInputState( $sanitizedInput );
  1032. $input.data( 'sanitized', sanitizedState );
  1033. canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) );
  1034. if ( canUpdateState ) {
  1035. self._setInputState( $input, sanitizedState );
  1036. }
  1037. } );
  1038. $( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] );
  1039. // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled
  1040. } else if ( self.liveUpdateMode ) {
  1041. self.liveUpdateMode = false;
  1042. self.container.find( 'input[name="savewidget"]' ).show();
  1043. isLiveUpdateAborted = true;
  1044. // Otherwise, replace existing form with the sanitized form
  1045. } else {
  1046. $widgetContent.html( r.data.form );
  1047. self.container.removeClass( 'widget-form-disabled' );
  1048. $( document ).trigger( 'widget-updated', [ $widgetRoot ] );
  1049. }
  1050. /**
  1051. * If the old instance is identical to the new one, there is nothing new
  1052. * needing to be rendered, and so we can preempt the event for the
  1053. * preview finishing loading.
  1054. */
  1055. isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
  1056. if ( isChanged ) {
  1057. self.isWidgetUpdating = true; // suppress triggering another updateWidget
  1058. self.setting( r.data.instance );
  1059. self.isWidgetUpdating = false;
  1060. } else {
  1061. // no change was made, so stop the spinner now instead of when the preview would updates
  1062. self.container.removeClass( 'previewer-loading' );
  1063. }
  1064. if ( completeCallback ) {
  1065. completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } );
  1066. }
  1067. } else {
  1068. // General error message
  1069. message = l10n.error;
  1070. if ( r.data && r.data.message ) {
  1071. message = r.data.message;
  1072. }
  1073. if ( completeCallback ) {
  1074. completeCallback.call( self, message );
  1075. } else {
  1076. $widgetContent.prepend( '<p class="widget-error"><strong>' + message + '</strong></p>' );
  1077. }
  1078. }
  1079. } );
  1080. jqxhr.fail( function( jqXHR, textStatus ) {
  1081. if ( completeCallback ) {
  1082. completeCallback.call( self, textStatus );
  1083. }
  1084. } );
  1085. jqxhr.always( function() {
  1086. self.container.removeClass( 'widget-form-loading' );
  1087. $inputs.each( function() {
  1088. $( this ).removeData( 'state' + updateNumber );
  1089. } );
  1090. processing( processing() - 1 );
  1091. } );
  1092. },
  1093. /**
  1094. * Expand the accordion section containing a control
  1095. */
  1096. expandControlSection: function() {
  1097. api.Control.prototype.expand.call( this );
  1098. },
  1099. /**
  1100. * @since 4.1.0
  1101. *
  1102. * @param {Boolean} expanded
  1103. * @param {Object} [params]
  1104. * @returns {Boolean} false if state already applied
  1105. */
  1106. _toggleExpanded: api.Section.prototype._toggleExpanded,
  1107. /**
  1108. * @since 4.1.0
  1109. *
  1110. * @param {Object} [params]
  1111. * @returns {Boolean} false if already expanded
  1112. */
  1113. expand: api.Section.prototype.expand,
  1114. /**
  1115. * Expand the widget form control
  1116. *
  1117. * @deprecated 4.1.0 Use this.expand() instead.
  1118. */
  1119. expandForm: function() {
  1120. this.expand();
  1121. },
  1122. /**
  1123. * @since 4.1.0
  1124. *
  1125. * @param {Object} [params]
  1126. * @returns {Boolean} false if already collapsed
  1127. */
  1128. collapse: api.Section.prototype.collapse,
  1129. /**
  1130. * Collapse the widget form control
  1131. *
  1132. * @deprecated 4.1.0 Use this.collapse() instead.
  1133. */
  1134. collapseForm: function() {
  1135. this.collapse();
  1136. },
  1137. /**
  1138. * Expand or collapse the widget control
  1139. *
  1140. * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
  1141. *
  1142. * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
  1143. */
  1144. toggleForm: function( showOrHide ) {
  1145. if ( typeof showOrHide === 'undefined' ) {
  1146. showOrHide = ! this.expanded();
  1147. }
  1148. this.expanded( showOrHide );
  1149. },
  1150. /**
  1151. * Respond to change in the expanded state.
  1152. *
  1153. * @param {Boolean} expanded
  1154. * @param {Object} args merged on top of this.defaultActiveArguments
  1155. */
  1156. onChangeExpanded: function ( expanded, args ) {
  1157. var self = this, $widget, $inside, complete, prevComplete, expandControl;
  1158. self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
  1159. if ( expanded ) {
  1160. self.embedWidgetContent();
  1161. }
  1162. // If the expanded state is unchanged only manipulate container expanded states
  1163. if ( args.unchanged ) {
  1164. if ( expanded ) {
  1165. api.Control.prototype.expand.call( self, {
  1166. completeCallback: args.completeCallback
  1167. });
  1168. }
  1169. return;
  1170. }
  1171. $widget = this.container.find( 'div.widget:first' );
  1172. $inside = $widget.find( '.widget-inside:first' );
  1173. expandControl = function() {
  1174. // Close all other widget controls before expanding this one
  1175. api.control.each( function( otherControl ) {
  1176. if ( self.params.type === otherControl.params.type && self !== otherControl ) {
  1177. otherControl.collapse();
  1178. }
  1179. } );
  1180. complete = function() {
  1181. self.container.removeClass( 'expanding' );
  1182. self.container.addClass( 'expanded' );
  1183. self.container.trigger( 'expanded' );
  1184. };
  1185. if ( args.completeCallback ) {
  1186. prevComplete = complete;
  1187. complete = function () {
  1188. prevComplete();
  1189. args.completeCallback();
  1190. };
  1191. }
  1192. if ( self.params.is_wide ) {
  1193. $inside.fadeIn( args.duration, complete );
  1194. } else {
  1195. $inside.slideDown( args.duration, complete );
  1196. }
  1197. self.container.trigger( 'expand' );
  1198. self.container.addClass( 'expanding' );
  1199. };
  1200. if ( expanded ) {
  1201. if ( api.section.has( self.section() ) ) {
  1202. api.section( self.section() ).expand( {
  1203. completeCallback: expandControl
  1204. } );
  1205. } else {
  1206. expandControl();
  1207. }
  1208. } else {
  1209. complete = function() {
  1210. self.container.removeClass( 'collapsing' );
  1211. self.container.removeClass( 'expanded' );
  1212. self.container.trigger( 'collapsed' );
  1213. };
  1214. if ( args.completeCallback ) {
  1215. prevComplete = complete;
  1216. complete = function () {
  1217. prevComplete();
  1218. args.completeCallback();
  1219. };
  1220. }
  1221. self.container.trigger( 'collapse' );
  1222. self.container.addClass( 'collapsing' );
  1223. if ( self.params.is_wide ) {
  1224. $inside.fadeOut( args.duration, complete );
  1225. } else {
  1226. $inside.slideUp( args.duration, function() {
  1227. $widget.css( { width:'', margin:'' } );
  1228. complete();
  1229. } );
  1230. }
  1231. }
  1232. },
  1233. /**
  1234. * Get the position (index) of the widget in the containing sidebar
  1235. *
  1236. * @returns {Number}
  1237. */
  1238. getWidgetSidebarPosition: function() {
  1239. var sidebarWidgetIds, position;
  1240. sidebarWidgetIds = this.getSidebarWidgetsControl().setting();
  1241. position = _.indexOf( sidebarWidgetIds, this.params.widget_id );
  1242. if ( position === -1 ) {
  1243. return;
  1244. }
  1245. return position;
  1246. },
  1247. /**
  1248. * Move widget up one in the sidebar
  1249. */
  1250. moveUp: function() {
  1251. this._moveWidgetByOne( -1 );
  1252. },
  1253. /**
  1254. * Move widget up one in the sidebar
  1255. */
  1256. moveDown: function() {
  1257. this._moveWidgetByOne( 1 );
  1258. },
  1259. /**
  1260. * @private
  1261. *
  1262. * @param {Number} offset 1|-1
  1263. */
  1264. _moveWidgetByOne: function( offset ) {
  1265. var i, sidebarWidgetsSetting, sidebarWidgetIds, adjacentWidgetId;
  1266. i = this.getWidgetSidebarPosition();
  1267. sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting;
  1268. sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // clone
  1269. adjacentWidgetId = sidebarWidgetIds[i + offset];
  1270. sidebarWidgetIds[i + offset] = this.params.widget_id;
  1271. sidebarWidgetIds[i] = adjacentWidgetId;
  1272. sidebarWidgetsSetting( sidebarWidgetIds );
  1273. },
  1274. /**
  1275. * Toggle visibility of the widget move area
  1276. *
  1277. * @param {Boolean} [showOrHide]
  1278. */
  1279. toggleWidgetMoveArea: function( showOrHide ) {
  1280. var self = this, $moveWidgetArea;
  1281. $moveWidgetArea = this.container.find( '.move-widget-area' );
  1282. if ( typeof showOrHide === 'undefined' ) {
  1283. showOrHide = ! $moveWidgetArea.hasClass( 'active' );
  1284. }
  1285. if ( showOrHide ) {
  1286. // reset the selected sidebar
  1287. $moveWidgetArea.find( '.selected' ).removeClass( 'selected' );
  1288. $moveWidgetArea.find( 'li' ).filter( function() {
  1289. return $( this ).data( 'id' ) === self.params.sidebar_id;
  1290. } ).addClass( 'selected' );
  1291. this.container.find( '.move-widget-btn' ).prop( 'disabled', true );
  1292. }
  1293. $moveWidgetArea.toggleClass( 'active', showOrHide );
  1294. },
  1295. /**
  1296. * Highlight the widget control and section
  1297. */
  1298. highlightSectionAndControl: function() {
  1299. var $target;
  1300. if ( this.container.is( ':hidden' ) ) {
  1301. $target = this.container.closest( '.control-section' );
  1302. } else {
  1303. $target = this.container;
  1304. }
  1305. $( '.highlighted' ).removeClass( 'highlighted' );
  1306. $target.addClass( 'highlighted' );
  1307. setTimeout( function() {
  1308. $target.removeClass( 'highlighted' );
  1309. }, 500 );
  1310. }
  1311. } );
  1312. /**
  1313. * wp.customize.Widgets.WidgetsPanel
  1314. *
  1315. * Customizer panel containing the widget area sections.
  1316. *
  1317. * @since 4.4.0
  1318. */
  1319. api.Widgets.WidgetsPanel = api.Panel.extend({
  1320. /**
  1321. * Add and manage the display of the no-rendered-areas notice.
  1322. *
  1323. * @since 4.4.0
  1324. */
  1325. ready: function () {
  1326. var panel = this;
  1327. api.Panel.prototype.ready.call( panel );
  1328. panel.deferred.embedded.done(function() {
  1329. var panelMetaContainer, noRenderedAreasNotice, shouldShowNotice;
  1330. panelMetaContainer = panel.container.find( '.panel-meta' );
  1331. noRenderedAreasNotice = $( '<div></div>', {
  1332. 'class': 'no-widget-areas-rendered-notice'
  1333. });
  1334. noRenderedAreasNotice.append( $( '<em></em>', {
  1335. text: l10n.noAreasRendered
  1336. } ) );
  1337. panelMetaContainer.append( noRenderedAreasNotice );
  1338. shouldShowNotice = function() {
  1339. return ( 0 === _.filter( panel.sections(), function( section ) {
  1340. return section.active();
  1341. } ).length );
  1342. };
  1343. /*
  1344. * Set the initial visibility state for rendered notice.
  1345. * Update the visibility of the notice whenever a reflow happens.
  1346. */
  1347. noRenderedAreasNotice.toggle( shouldShowNotice() );
  1348. api.previewer.deferred.active.done( function () {
  1349. noRenderedAreasNotice.toggle( shouldShowNotice() );
  1350. });
  1351. api.bind( 'pane-contents-reflowed', function() {
  1352. var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0;
  1353. if ( shouldShowNotice() ) {
  1354. noRenderedAreasNotice.slideDown( duration );
  1355. } else {
  1356. noRenderedAreasNotice.slideUp( duration );
  1357. }
  1358. });
  1359. });
  1360. },
  1361. /**
  1362. * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas).
  1363. *
  1364. * This ensures that the widgets panel appears even when there are no
  1365. * sidebars displayed on the URL currently being previewed.
  1366. *
  1367. * @since 4.4.0
  1368. *
  1369. * @returns {boolean}
  1370. */
  1371. isContextuallyActive: function() {
  1372. var panel = this;
  1373. return panel.active();
  1374. }
  1375. });
  1376. /**
  1377. * wp.customize.Widgets.SidebarSection
  1378. *
  1379. * Customizer section representing a widget area widget
  1380. *
  1381. * @since 4.1.0
  1382. */
  1383. api.Widgets.SidebarSection = api.Section.extend({
  1384. /**
  1385. * Sync the section's active state back to the Backbone model's is_rendered attribute
  1386. *
  1387. * @since 4.1.0
  1388. */
  1389. ready: function () {
  1390. var section = this, registeredSidebar;
  1391. api.Section.prototype.ready.call( this );
  1392. registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId );
  1393. section.active.bind( function ( active ) {
  1394. registeredSidebar.set( 'is_rendered', active );
  1395. });
  1396. registeredSidebar.set( 'is_rendered', section.active() );
  1397. }
  1398. });
  1399. /**
  1400. * wp.customize.Widgets.SidebarControl
  1401. *
  1402. * Customizer control for widgets.
  1403. * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type
  1404. *
  1405. * @since 3.9.0
  1406. *
  1407. * @constructor
  1408. * @augments wp.customize.Control
  1409. */
  1410. api.Widgets.SidebarControl = api.Control.extend({
  1411. /**
  1412. * Set up the control
  1413. */
  1414. ready: function() {
  1415. this.$controlSection = this.container.closest( '.control-section' );
  1416. this.$sectionContent = this.container.closest( '.accordion-section-content' );
  1417. this._setupModel();
  1418. this._setupSortable();
  1419. this._setupAddition();
  1420. this._applyCardinalOrderClassNames();
  1421. },
  1422. /**
  1423. * Update ordering of widget control forms when the setting is updated
  1424. */
  1425. _setupModel: function() {
  1426. var self = this;
  1427. this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
  1428. var widgetFormControls, removedWidgetIds, priority;
  1429. removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
  1430. // Filter out any persistent widget IDs for widgets which have been deactivated
  1431. newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) {
  1432. var parsedWidgetId = parseWidgetId( newWidgetId );
  1433. return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } );
  1434. } );
  1435. widgetFormControls = _( newWidgetIds ).map( function( widgetId ) {
  1436. var widgetFormControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
  1437. if ( ! widgetFormControl ) {
  1438. widgetFormControl = self.addWidget( widgetId );
  1439. }
  1440. return widgetFormControl;
  1441. } );
  1442. // Sort widget controls to their new positions
  1443. widgetFormControls.sort( function( a, b ) {
  1444. var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
  1445. bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
  1446. return aIndex - bIndex;
  1447. });
  1448. priority = 0;
  1449. _( widgetFormControls ).each( function ( control ) {
  1450. control.priority( priority );
  1451. control.section( self.section() );
  1452. priority += 1;
  1453. });
  1454. self.priority( priority ); // Make sure sidebar control remains at end
  1455. // Re-sort widget form controls (including widgets form other sidebars newly moved here)
  1456. self._applyCardinalOrderClassNames();
  1457. // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated
  1458. _( widgetFormControls ).each( function( widgetFormControl ) {
  1459. widgetFormControl.params.sidebar_id = self.params.sidebar_id;
  1460. } );
  1461. // Cleanup after widget removal
  1462. _( removedWidgetIds ).each( function( removedWidgetId ) {
  1463. // Using setTimeout so that when moving a widget to another sidebar, the other sidebars_widgets settings get a chance to update
  1464. setTimeout( function() {
  1465. var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase,
  1466. widget, isPresentInAnotherSidebar = false;
  1467. // Check if the widget is in another sidebar
  1468. api.each( function( otherSetting ) {
  1469. if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
  1470. return;
  1471. }
  1472. var otherSidebarWidgets = otherSetting(), i;
  1473. i = _.indexOf( otherSidebarWidgets, removedWidgetId );
  1474. if ( -1 !== i ) {
  1475. isPresentInAnotherSidebar = true;
  1476. }
  1477. } );
  1478. // If the widget is present in another sidebar, abort!
  1479. if ( isPresentInAnotherSidebar ) {
  1480. return;
  1481. }
  1482. removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId );
  1483. // Detect if widget control was dragged to another sidebar
  1484. wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] );
  1485. // Delete any widget form controls for removed widgets
  1486. if ( removedControl && ! wasDraggedToAnotherSidebar ) {
  1487. api.control.remove( removedControl.id );
  1488. removedControl.container.remove();
  1489. }
  1490. // Move widget to inactive widgets sidebar (move it to trash) if has been previously saved
  1491. // This prevents the inactive widgets sidebar from overflowing with throwaway widgets
  1492. if ( api.Widgets.savedWidgetIds[removedWidgetId] ) {
  1493. inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
  1494. inactiveWidgets.push( removedWidgetId );
  1495. api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() );
  1496. }
  1497. // Make old single widget available for adding again
  1498. removedIdBase = parseWidgetId( removedWidgetId ).id_base;
  1499. widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } );
  1500. if ( widget && ! widget.get( 'is_multi' ) ) {
  1501. widget.set( 'is_disabled', false );
  1502. }
  1503. } );
  1504. } );
  1505. } );
  1506. },
  1507. /**
  1508. * Allow widgets in sidebar to be re-ordered, and for the order to be previewed
  1509. */
  1510. _setupSortable: function() {
  1511. var self = this;
  1512. this.isReordering = false;
  1513. /**
  1514. * Update widget order setting when controls are re-ordered
  1515. */
  1516. this.$sectionContent.sortable( {
  1517. items: '> .customize-control-widget_form',
  1518. handle: '.widget-top',
  1519. axis: 'y',
  1520. tolerance: 'pointer',
  1521. connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
  1522. update: function() {
  1523. var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds;
  1524. widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) {
  1525. return $( '#' + widgetContainerId ).find( ':input[name=widget-id]' ).val();
  1526. } );
  1527. self.setting( widgetIds );
  1528. }
  1529. } );
  1530. /**
  1531. * Expand other Customizer sidebar section when dragging a control widget over it,
  1532. * allowing the control to be dropped into another section
  1533. */
  1534. this.$controlSection.find( '.accordion-section-title' ).droppable({
  1535. accept: '.customize-control-widget_form',
  1536. over: function() {
  1537. var section = api.section( self.section.get() );
  1538. section.expand({
  1539. allowMultiple: true, // Prevent the section being dragged from to be collapsed
  1540. completeCallback: function () {
  1541. // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed
  1542. api.section.each( function ( otherSection ) {
  1543. if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
  1544. otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
  1545. }
  1546. } );
  1547. }
  1548. });
  1549. }
  1550. });
  1551. /**
  1552. * Keyboard-accessible reordering
  1553. */
  1554. this.container.find( '.reorder-toggle' ).on( 'click', function() {
  1555. self.toggleReordering( ! self.isReordering );
  1556. } );
  1557. },
  1558. /**
  1559. * Set up UI for adding a new widget
  1560. */
  1561. _setupAddition: function() {
  1562. var self = this;
  1563. this.container.find( '.add-new-widget' ).on( 'click', function() {
  1564. var addNewWidgetBtn = $( this );
  1565. if ( self.$sectionContent.hasClass( 'reordering' ) ) {
  1566. return;
  1567. }
  1568. if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
  1569. addNewWidgetBtn.attr( 'aria-expanded', 'true' );
  1570. api.Widgets.availableWidgetsPanel.open( self );
  1571. } else {
  1572. addNewWidgetBtn.attr( 'aria-expanded', 'false' );
  1573. api.Widgets.availableWidgetsPanel.close();
  1574. }
  1575. } );
  1576. },
  1577. /**
  1578. * Add classes to the widget_form controls to assist with styling
  1579. */
  1580. _applyCardinalOrderClassNames: function() {
  1581. var widgetControls = [];
  1582. _.each( this.setting(), function ( widgetId ) {
  1583. var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
  1584. if ( widgetControl ) {
  1585. widgetControls.push( widgetControl );
  1586. }
  1587. });
  1588. if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) {
  1589. this.container.find( '.reorder-toggle' ).hide();
  1590. return;
  1591. } else {
  1592. this.container.find( '.reorder-toggle' ).show();
  1593. }
  1594. $( widgetControls ).each( function () {
  1595. $( this.container )
  1596. .removeClass( 'first-widget' )
  1597. .removeClass( 'last-widget' )
  1598. .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
  1599. });
  1600. _.first( widgetControls ).container
  1601. .addClass( 'first-widget' )
  1602. .find( '.move-widget-up' ).prop( 'tabIndex', -1 );
  1603. _.last( widgetControls ).container
  1604. .addClass( 'last-widget' )
  1605. .find( '.move-widget-down' ).prop( 'tabIndex', -1 );
  1606. },
  1607. /***********************************************************************
  1608. * Begin public API methods
  1609. **********************************************************************/
  1610. /**
  1611. * Enable/disable the reordering UI
  1612. *
  1613. * @param {Boolean} showOrHide to enable/disable reordering
  1614. *
  1615. * @todo We should have a reordering state instead and rename this to onChangeReordering
  1616. */
  1617. toggleReordering: function( showOrHide ) {
  1618. var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ),
  1619. reorderBtn = this.container.find( '.reorder-toggle' ),
  1620. widgetsTitle = this.$sectionContent.find( '.widget-title' );
  1621. showOrHide = Boolean( showOrHide );
  1622. if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) {
  1623. return;
  1624. }
  1625. this.isReordering = showOrHide;
  1626. this.$sectionContent.toggleClass( 'reordering', showOrHide );
  1627. if ( showOrHide ) {
  1628. _( this.getWidgetFormControls() ).each( function( formControl ) {
  1629. formControl.collapse();
  1630. } );
  1631. addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
  1632. reorderBtn.attr( 'aria-label', l10n.reorderLabelOff );
  1633. wp.a11y.speak( l10n.reorderModeOn );
  1634. // Hide widget titles while reordering: title is already in the reorder controls.
  1635. widgetsTitle.attr( 'aria-hidden', 'true' );
  1636. } else {
  1637. addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' );
  1638. reorderBtn.attr( 'aria-label', l10n.reorderLabelOn );
  1639. wp.a11y.speak( l10n.reorderModeOff );
  1640. widgetsTitle.attr( 'aria-hidden', 'false' );
  1641. }
  1642. },
  1643. /**
  1644. * Get the widget_form Customize controls associated with the current sidebar.
  1645. *
  1646. * @since 3.9.0
  1647. * @return {wp.customize.controlConstructor.widget_form[]}
  1648. */
  1649. getWidgetFormControls: function() {
  1650. var formControls = [];
  1651. _( this.setting() ).each( function( widgetId ) {
  1652. var settingId = widgetIdToSettingId( widgetId ),
  1653. formControl = api.control( settingId );
  1654. if ( formControl ) {
  1655. formControls.push( formControl );
  1656. }
  1657. } );
  1658. return formControls;
  1659. },
  1660. /**
  1661. * @param {string} widgetId or an id_base for adding a previously non-existing widget
  1662. * @returns {object|false} widget_form control instance, or false on error
  1663. */
  1664. addWidget: function( widgetId ) {
  1665. var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
  1666. parsedWidgetId = parseWidgetId( widgetId ),
  1667. widgetNumber = parsedWidgetId.number,
  1668. widgetIdBase = parsedWidgetId.id_base,
  1669. widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ),
  1670. settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting;
  1671. if ( ! widget ) {
  1672. return false;
  1673. }
  1674. if ( widgetNumber && ! widget.get( 'is_multi' ) ) {
  1675. return false;
  1676. }
  1677. // Set up new multi widget
  1678. if ( widget.get( 'is_multi' ) && ! widgetNumber ) {
  1679. widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
  1680. widgetNumber = widget.get( 'multi_number' );
  1681. }
  1682. controlHtml = $.trim( $( '#widget-tpl-' + widget.get( 'id' ) ).html() );
  1683. if ( widget.get( 'is_multi' ) ) {
  1684. controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) {
  1685. return m.replace( /__i__|%i%/g, widgetNumber );
  1686. } );
  1687. } else {
  1688. widget.set( 'is_disabled', true ); // Prevent single widget from being added again now
  1689. }
  1690. $widget = $( controlHtml );
  1691. controlContainer = $( '<li/>' )
  1692. .addClass( 'customize-control' )
  1693. .addClass( 'customize-control-' + controlType )
  1694. .append( $widget );
  1695. // Remove icon which is visible inside the panel
  1696. controlContainer.find( '> .widget-icon' ).remove();
  1697. if ( widget.get( 'is_multi' ) ) {
  1698. controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
  1699. controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
  1700. }
  1701. widgetId = controlContainer.find( '[name="widget-id"]' ).val();
  1702. controlContainer.hide(); // to be slid-down below
  1703. settingId = 'widget_' + widget.get( 'id_base' );
  1704. if ( widget.get( 'is_multi' ) ) {
  1705. settingId += '[' + widgetNumber + ']';
  1706. }
  1707. controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
  1708. // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
  1709. isExistingWidget = api.has( settingId );
  1710. if ( ! isExistingWidget ) {
  1711. settingArgs = {
  1712. transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh',
  1713. previewer: this.setting.previewer
  1714. };
  1715. setting = api.create( settingId, settingId, '', settingArgs );
  1716. setting.set( {} ); // mark dirty, changing from '' to {}
  1717. }
  1718. controlConstructor = api.controlConstructor[controlType];
  1719. widgetFormControl = new controlConstructor( settingId, {
  1720. params: {
  1721. settings: {
  1722. 'default': settingId
  1723. },
  1724. content: controlContainer,
  1725. sidebar_id: self.params.sidebar_id,
  1726. widget_id: widgetId,
  1727. widget_id_base: widget.get( 'id_base' ),
  1728. type: controlType,
  1729. is_new: ! isExistingWidget,
  1730. width: widget.get( 'width' ),
  1731. height: widget.get( 'height' ),
  1732. is_wide: widget.get( 'is_wide' ),
  1733. active: true
  1734. },
  1735. previewer: self.setting.previewer
  1736. } );
  1737. api.control.add( settingId, widgetFormControl );
  1738. // Make sure widget is removed from the other sidebars
  1739. api.each( function( otherSetting ) {
  1740. if ( otherSetting.id === self.setting.id ) {
  1741. return;
  1742. }
  1743. if ( 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) ) {
  1744. return;
  1745. }
  1746. var otherSidebarWidgets = otherSetting().slice(),
  1747. i = _.indexOf( otherSidebarWidgets, widgetId );
  1748. if ( -1 !== i ) {
  1749. otherSidebarWidgets.splice( i );
  1750. otherSetting( otherSidebarWidgets );
  1751. }
  1752. } );
  1753. // Add widget to this sidebar
  1754. sidebarWidgets = this.setting().slice();
  1755. if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) {
  1756. sidebarWidgets.push( widgetId );
  1757. this.setting( sidebarWidgets );
  1758. }
  1759. controlContainer.slideDown( function() {
  1760. if ( isExistingWidget ) {
  1761. widgetFormControl.updateWidget( {
  1762. instance: widgetFormControl.setting()
  1763. } );
  1764. }
  1765. } );
  1766. return widgetFormControl;
  1767. }
  1768. } );
  1769. // Register models for custom panel, section, and control types
  1770. $.extend( api.panelConstructor, {
  1771. widgets: api.Widgets.WidgetsPanel
  1772. });
  1773. $.extend( api.sectionConstructor, {
  1774. sidebar: api.Widgets.SidebarSection
  1775. });
  1776. $.extend( api.controlConstructor, {
  1777. widget_form: api.Widgets.WidgetControl,
  1778. sidebar_widgets: api.Widgets.SidebarControl
  1779. });
  1780. /**
  1781. * Init Customizer for widgets.
  1782. */
  1783. api.bind( 'ready', function() {
  1784. // Set up the widgets panel
  1785. api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({
  1786. collection: api.Widgets.availableWidgets
  1787. });
  1788. // Highlight widget control
  1789. api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
  1790. // Open and focus widget control
  1791. api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
  1792. } );
  1793. /**
  1794. * Highlight a widget control.
  1795. *
  1796. * @param {string} widgetId
  1797. */
  1798. api.Widgets.highlightWidgetFormControl = function( widgetId ) {
  1799. var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
  1800. if ( control ) {
  1801. control.highlightSectionAndControl();
  1802. }
  1803. },
  1804. /**
  1805. * Focus a widget control.
  1806. *
  1807. * @param {string} widgetId
  1808. */
  1809. api.Widgets.focusWidgetFormControl = function( widgetId ) {
  1810. var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
  1811. if ( control ) {
  1812. control.focus();
  1813. }
  1814. },
  1815. /**
  1816. * Given a widget control, find the sidebar widgets control that contains it.
  1817. * @param {string} widgetId
  1818. * @return {object|null}
  1819. */
  1820. api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) {
  1821. var foundControl = null;
  1822. // @todo this can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl()
  1823. api.control.each( function( control ) {
  1824. if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) {
  1825. foundControl = control;
  1826. }
  1827. } );
  1828. return foundControl;
  1829. };
  1830. /**
  1831. * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it.
  1832. *
  1833. * @param {string} widgetId
  1834. * @return {object|null}
  1835. */
  1836. api.Widgets.getWidgetFormControlForWidget = function( widgetId ) {
  1837. var foundControl = null;
  1838. // @todo We can just use widgetIdToSettingId() here
  1839. api.control.each( function( control ) {
  1840. if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) {
  1841. foundControl = control;
  1842. }
  1843. } );
  1844. return foundControl;
  1845. };
  1846. /**
  1847. * Initialize Edit Menu button in Nav Menu widget.
  1848. */
  1849. $( document ).on( 'widget-added', function( event, widgetContainer ) {
  1850. var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton;
  1851. parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() );
  1852. if ( 'nav_menu' !== parsedWidgetId.id_base ) {
  1853. return;
  1854. }
  1855. widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' );
  1856. if ( ! widgetControl ) {
  1857. return;
  1858. }
  1859. navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' );
  1860. editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' );
  1861. if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) {
  1862. return;
  1863. }
  1864. navMenuSelect.on( 'change', function() {
  1865. if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) {
  1866. editMenuButton.parent().show();
  1867. } else {
  1868. editMenuButton.parent().hide();
  1869. }
  1870. });
  1871. editMenuButton.on( 'click', function() {
  1872. var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' );
  1873. if ( section ) {
  1874. focusConstructWithBreadcrumb( section, widgetControl );
  1875. }
  1876. } );
  1877. } );
  1878. /**
  1879. * Focus (expand) one construct and then focus on another construct after the first is collapsed.
  1880. *
  1881. * This overrides the back button to serve the purpose of breadcrumb navigation.
  1882. *
  1883. * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus.
  1884. * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus.
  1885. */
  1886. function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) {
  1887. focusConstruct.focus();
  1888. function onceCollapsed( isExpanded ) {
  1889. if ( ! isExpanded ) {
  1890. focusConstruct.expanded.unbind( onceCollapsed );
  1891. returnConstruct.focus();
  1892. }
  1893. }
  1894. focusConstruct.expanded.bind( onceCollapsed );
  1895. }
  1896. /**
  1897. * @param {String} widgetId
  1898. * @returns {Object}
  1899. */
  1900. function parseWidgetId( widgetId ) {
  1901. var matches, parsed = {
  1902. number: null,
  1903. id_base: null
  1904. };
  1905. matches = widgetId.match( /^(.+)-(\d+)$/ );
  1906. if ( matches ) {
  1907. parsed.id_base = matches[1];
  1908. parsed.number = parseInt( matches[2], 10 );
  1909. } else {
  1910. // likely an old single widget
  1911. parsed.id_base = widgetId;
  1912. }
  1913. return parsed;
  1914. }
  1915. /**
  1916. * @param {String} widgetId
  1917. * @returns {String} settingId
  1918. */
  1919. function widgetIdToSettingId( widgetId ) {
  1920. var parsed = parseWidgetId( widgetId ), settingId;
  1921. settingId = 'widget_' + parsed.id_base;
  1922. if ( parsed.number ) {
  1923. settingId += '[' + parsed.number + ']';
  1924. }
  1925. return settingId;
  1926. }
  1927. })( window.wp, jQuery );