PageRenderTime 66ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-admin/js/customize-widgets.js

https://gitlab.com/campus-academy/krowkaramel
JavaScript | 1688 lines | 1015 code | 260 blank | 413 comment | 191 complexity | 55ea8adb9d422947d6ab51a683540797 MD5 | raw file
  1. /**
  2. * @output wp-admin/js/customize-widgets.js
  3. */
  4. /* global _wpCustomizeWidgetsSettings */
  5. (function( wp, $ ){
  6. if ( ! wp || ! wp.customize ) { return; }
  7. // Set up our namespace...
  8. var api = wp.customize,
  9. l10n;
  10. /**
  11. * @namespace wp.customize.Widgets
  12. */
  13. api.Widgets = api.Widgets || {};
  14. api.Widgets.savedWidgetIds = {};
  15. // Link settings.
  16. api.Widgets.data = _wpCustomizeWidgetsSettings || {};
  17. l10n = api.Widgets.data.l10n;
  18. /**
  19. * wp.customize.Widgets.WidgetModel
  20. *
  21. * A single widget model.
  22. *
  23. * @class wp.customize.Widgets.WidgetModel
  24. * @augments Backbone.Model
  25. */
  26. api.Widgets.WidgetModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.WidgetModel.prototype */{
  27. id: null,
  28. temp_id: null,
  29. classname: null,
  30. control_tpl: null,
  31. description: null,
  32. is_disabled: null,
  33. is_multi: null,
  34. multi_number: null,
  35. name: null,
  36. id_base: null,
  37. transport: null,
  38. params: [],
  39. width: null,
  40. height: null,
  41. search_matched: true
  42. });
  43. /**
  44. * wp.customize.Widgets.WidgetCollection
  45. *
  46. * Collection for widget models.
  47. *
  48. * @class wp.customize.Widgets.WidgetCollection
  49. * @augments Backbone.Collection
  50. */
  51. api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{
  52. model: api.Widgets.WidgetModel,
  53. // Controls searching on the current widget collection
  54. // and triggers an update event.
  55. doSearch: function( value ) {
  56. // Don't do anything if we've already done this search.
  57. // Useful because the search handler fires multiple times per keystroke.
  58. if ( this.terms === value ) {
  59. return;
  60. }
  61. // Updates terms with the value passed.
  62. this.terms = value;
  63. // If we have terms, run a search...
  64. if ( this.terms.length > 0 ) {
  65. this.search( this.terms );
  66. }
  67. // If search is blank, set all the widgets as they matched the search to reset the views.
  68. if ( this.terms === '' ) {
  69. this.each( function ( widget ) {
  70. widget.set( 'search_matched', true );
  71. } );
  72. }
  73. },
  74. // Performs a search within the collection.
  75. // @uses RegExp
  76. search: function( term ) {
  77. var match, haystack;
  78. // Escape the term string for RegExp meta characters.
  79. term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
  80. // Consider spaces as word delimiters and match the whole string
  81. // so matching terms can be combined.
  82. term = term.replace( / /g, ')(?=.*' );
  83. match = new RegExp( '^(?=.*' + term + ').+', 'i' );
  84. this.each( function ( data ) {
  85. haystack = [ data.get( 'name' ), data.get( 'description' ) ].join( ' ' );
  86. data.set( 'search_matched', match.test( haystack ) );
  87. } );
  88. }
  89. });
  90. api.Widgets.availableWidgets = new api.Widgets.WidgetCollection( api.Widgets.data.availableWidgets );
  91. /**
  92. * wp.customize.Widgets.SidebarModel
  93. *
  94. * A single sidebar model.
  95. *
  96. * @class wp.customize.Widgets.SidebarModel
  97. * @augments Backbone.Model
  98. */
  99. api.Widgets.SidebarModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.SidebarModel.prototype */{
  100. after_title: null,
  101. after_widget: null,
  102. before_title: null,
  103. before_widget: null,
  104. 'class': null,
  105. description: null,
  106. id: null,
  107. name: null,
  108. is_rendered: false
  109. });
  110. /**
  111. * wp.customize.Widgets.SidebarCollection
  112. *
  113. * Collection for sidebar models.
  114. *
  115. * @class wp.customize.Widgets.SidebarCollection
  116. * @augments Backbone.Collection
  117. */
  118. api.Widgets.SidebarCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.SidebarCollection.prototype */{
  119. model: api.Widgets.SidebarModel
  120. });
  121. api.Widgets.registeredSidebars = new api.Widgets.SidebarCollection( api.Widgets.data.registeredSidebars );
  122. api.Widgets.AvailableWidgetsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Widgets.AvailableWidgetsPanelView.prototype */{
  123. el: '#available-widgets',
  124. events: {
  125. 'input #widgets-search': 'search',
  126. 'focus .widget-tpl' : 'focus',
  127. 'click .widget-tpl' : '_submit',
  128. 'keypress .widget-tpl' : '_submit',
  129. 'keydown' : 'keyboardAccessible'
  130. },
  131. // Cache current selected widget.
  132. selected: null,
  133. // Cache sidebar control which has opened panel.
  134. currentSidebarControl: null,
  135. $search: null,
  136. $clearResults: null,
  137. searchMatchesCount: null,
  138. /**
  139. * View class for the available widgets panel.
  140. *
  141. * @constructs wp.customize.Widgets.AvailableWidgetsPanelView
  142. * @augments wp.Backbone.View
  143. */
  144. initialize: function() {
  145. var self = this;
  146. this.$search = $( '#widgets-search' );
  147. this.$clearResults = this.$el.find( '.clear-results' );
  148. _.bindAll( this, 'close' );
  149. this.listenTo( this.collection, 'change', this.updateList );
  150. this.updateList();
  151. // Set the initial search count to the number of available widgets.
  152. this.searchMatchesCount = this.collection.length;
  153. /*
  154. * If the available widgets panel is open and the customize controls
  155. * are interacted with (i.e. available widgets panel is blurred) then
  156. * close the available widgets panel. Also close on back button click.
  157. */
  158. $( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) {
  159. var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
  160. if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) {
  161. self.close();
  162. }
  163. } );
  164. // Clear the search results and trigger an `input` event to fire a new search.
  165. this.$clearResults.on( 'click', function() {
  166. self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' );
  167. } );
  168. // Close the panel if the URL in the preview changes.
  169. api.previewer.bind( 'url', this.close );
  170. },
  171. /**
  172. * Performs a search and handles selected widget.
  173. */
  174. search: _.debounce( function( event ) {
  175. var firstVisible;
  176. this.collection.doSearch( event.target.value );
  177. // Update the search matches count.
  178. this.updateSearchMatchesCount();
  179. // Announce how many search results.
  180. this.announceSearchMatches();
  181. // Remove a widget from being selected if it is no longer visible.
  182. if ( this.selected && ! this.selected.is( ':visible' ) ) {
  183. this.selected.removeClass( 'selected' );
  184. this.selected = null;
  185. }
  186. // If a widget was selected but the filter value has been cleared out, clear selection.
  187. if ( this.selected && ! event.target.value ) {
  188. this.selected.removeClass( 'selected' );
  189. this.selected = null;
  190. }
  191. // If a filter has been entered and a widget hasn't been selected, select the first one shown.
  192. if ( ! this.selected && event.target.value ) {
  193. firstVisible = this.$el.find( '> .widget-tpl:visible:first' );
  194. if ( firstVisible.length ) {
  195. this.select( firstVisible );
  196. }
  197. }
  198. // Toggle the clear search results button.
  199. if ( '' !== event.target.value ) {
  200. this.$clearResults.addClass( 'is-visible' );
  201. } else if ( '' === event.target.value ) {
  202. this.$clearResults.removeClass( 'is-visible' );
  203. }
  204. // Set a CSS class on the search container when there are no search results.
  205. if ( ! this.searchMatchesCount ) {
  206. this.$el.addClass( 'no-widgets-found' );
  207. } else {
  208. this.$el.removeClass( 'no-widgets-found' );
  209. }
  210. }, 500 ),
  211. /**
  212. * Updates the count of the available widgets that have the `search_matched` attribute.
  213. */
  214. updateSearchMatchesCount: function() {
  215. this.searchMatchesCount = this.collection.where({ search_matched: true }).length;
  216. },
  217. /**
  218. * Sends a message to the aria-live region to announce how many search results.
  219. */
  220. announceSearchMatches: function() {
  221. var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ;
  222. if ( ! this.searchMatchesCount ) {
  223. message = l10n.noWidgetsFound;
  224. }
  225. wp.a11y.speak( message );
  226. },
  227. /**
  228. * Changes visibility of available widgets.
  229. */
  230. updateList: function() {
  231. this.collection.each( function( widget ) {
  232. var widgetTpl = $( '#widget-tpl-' + widget.id );
  233. widgetTpl.toggle( widget.get( 'search_matched' ) && ! widget.get( 'is_disabled' ) );
  234. if ( widget.get( 'is_disabled' ) && widgetTpl.is( this.selected ) ) {
  235. this.selected = null;
  236. }
  237. } );
  238. },
  239. /**
  240. * Highlights a widget.
  241. */
  242. select: function( widgetTpl ) {
  243. this.selected = $( widgetTpl );
  244. this.selected.siblings( '.widget-tpl' ).removeClass( 'selected' );
  245. this.selected.addClass( 'selected' );
  246. },
  247. /**
  248. * Highlights a widget on focus.
  249. */
  250. focus: function( event ) {
  251. this.select( $( event.currentTarget ) );
  252. },
  253. /**
  254. * Handles submit for keypress and click on widget.
  255. */
  256. _submit: function( event ) {
  257. // Only proceed with keypress if it is Enter or Spacebar.
  258. if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
  259. return;
  260. }
  261. this.submit( $( event.currentTarget ) );
  262. },
  263. /**
  264. * Adds a selected widget to the sidebar.
  265. */
  266. submit: function( widgetTpl ) {
  267. var widgetId, widget, widgetFormControl;
  268. if ( ! widgetTpl ) {
  269. widgetTpl = this.selected;
  270. }
  271. if ( ! widgetTpl || ! this.currentSidebarControl ) {
  272. return;
  273. }
  274. this.select( widgetTpl );
  275. widgetId = $( this.selected ).data( 'widget-id' );
  276. widget = this.collection.findWhere( { id: widgetId } );
  277. if ( ! widget ) {
  278. return;
  279. }
  280. widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) );
  281. if ( widgetFormControl ) {
  282. widgetFormControl.focus();
  283. }
  284. this.close();
  285. },
  286. /**
  287. * Opens the panel.
  288. */
  289. open: function( sidebarControl ) {
  290. this.currentSidebarControl = sidebarControl;
  291. // Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens.
  292. _( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
  293. if ( control.params.is_wide ) {
  294. control.collapseForm();
  295. }
  296. } );
  297. if ( api.section.has( 'publish_settings' ) ) {
  298. api.section( 'publish_settings' ).collapse();
  299. }
  300. $( 'body' ).addClass( 'adding-widget' );
  301. this.$el.find( '.selected' ).removeClass( 'selected' );
  302. // Reset search.
  303. this.collection.doSearch( '' );
  304. if ( ! api.settings.browser.mobile ) {
  305. this.$search.trigger( 'focus' );
  306. }
  307. },
  308. /**
  309. * Closes the panel.
  310. */
  311. close: function( options ) {
  312. options = options || {};
  313. if ( options.returnFocus && this.currentSidebarControl ) {
  314. this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
  315. }
  316. this.currentSidebarControl = null;
  317. this.selected = null;
  318. $( 'body' ).removeClass( 'adding-widget' );
  319. this.$search.val( '' ).trigger( 'input' );
  320. },
  321. /**
  322. * Adds keyboard accessiblity to the panel.
  323. */
  324. keyboardAccessible: function( event ) {
  325. var isEnter = ( event.which === 13 ),
  326. isEsc = ( event.which === 27 ),
  327. isDown = ( event.which === 40 ),
  328. isUp = ( event.which === 38 ),
  329. isTab = ( event.which === 9 ),
  330. isShift = ( event.shiftKey ),
  331. selected = null,
  332. firstVisible = this.$el.find( '> .widget-tpl:visible:first' ),
  333. lastVisible = this.$el.find( '> .widget-tpl:visible:last' ),
  334. isSearchFocused = $( event.target ).is( this.$search ),
  335. isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' );
  336. if ( isDown || isUp ) {
  337. if ( isDown ) {
  338. if ( isSearchFocused ) {
  339. selected = firstVisible;
  340. } else if ( this.selected && this.selected.nextAll( '.widget-tpl:visible' ).length !== 0 ) {
  341. selected = this.selected.nextAll( '.widget-tpl:visible:first' );
  342. }
  343. } else if ( isUp ) {
  344. if ( isSearchFocused ) {
  345. selected = lastVisible;
  346. } else if ( this.selected && this.selected.prevAll( '.widget-tpl:visible' ).length !== 0 ) {
  347. selected = this.selected.prevAll( '.widget-tpl:visible:first' );
  348. }
  349. }
  350. this.select( selected );
  351. if ( selected ) {
  352. selected.trigger( 'focus' );
  353. } else {
  354. this.$search.trigger( 'focus' );
  355. }
  356. return;
  357. }
  358. // If enter pressed but nothing entered, don't do anything.
  359. if ( isEnter && ! this.$search.val() ) {
  360. return;
  361. }
  362. if ( isEnter ) {
  363. this.submit();
  364. } else if ( isEsc ) {
  365. this.close( { returnFocus: true } );
  366. }
  367. if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) {
  368. this.currentSidebarControl.container.find( '.add-new-widget' ).focus();
  369. event.preventDefault();
  370. }
  371. }
  372. });
  373. /**
  374. * Handlers for the widget-synced event, organized by widget ID base.
  375. * Other widgets may provide their own update handlers by adding
  376. * listeners for the widget-synced event.
  377. *
  378. * @alias wp.customize.Widgets.formSyncHandlers
  379. */
  380. api.Widgets.formSyncHandlers = {
  381. /**
  382. * @param {jQuery.Event} e
  383. * @param {jQuery} widget
  384. * @param {string} newForm
  385. */
  386. rss: function( e, widget, newForm ) {
  387. var oldWidgetError = widget.find( '.widget-error:first' ),
  388. newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' );
  389. if ( oldWidgetError.length && newWidgetError.length ) {
  390. oldWidgetError.replaceWith( newWidgetError );
  391. } else if ( oldWidgetError.length ) {
  392. oldWidgetError.remove();
  393. } else if ( newWidgetError.length ) {
  394. widget.find( '.widget-content:first' ).prepend( newWidgetError );
  395. }
  396. }
  397. };
  398. api.Widgets.WidgetControl = api.Control.extend(/** @lends wp.customize.Widgets.WidgetControl.prototype */{
  399. defaultExpandedArguments: {
  400. duration: 'fast',
  401. completeCallback: $.noop
  402. },
  403. /**
  404. * wp.customize.Widgets.WidgetControl
  405. *
  406. * Customizer control for widgets.
  407. * Note that 'widget_form' must match the WP_Widget_Form_Customize_Control::$type
  408. *
  409. * @since 4.1.0
  410. *
  411. * @constructs wp.customize.Widgets.WidgetControl
  412. * @augments wp.customize.Control
  413. */
  414. initialize: function( id, options ) {
  415. var control = this;
  416. control.widgetControlEmbedded = false;
  417. control.widgetContentEmbedded = false;
  418. control.expanded = new api.Value( false );
  419. control.expandedArgumentsQueue = [];
  420. control.expanded.bind( function( expanded ) {
  421. var args = control.expandedArgumentsQueue.shift();
  422. args = $.extend( {}, control.defaultExpandedArguments, args );
  423. control.onChangeExpanded( expanded, args );
  424. });
  425. control.altNotice = true;
  426. api.Control.prototype.initialize.call( control, id, options );
  427. },
  428. /**
  429. * Set up the control.
  430. *
  431. * @since 3.9.0
  432. */
  433. ready: function() {
  434. var control = this;
  435. /*
  436. * Embed a placeholder once the section is expanded. The full widget
  437. * form content will be embedded once the control itself is expanded,
  438. * and at this point the widget-added event will be triggered.
  439. */
  440. if ( ! control.section() ) {
  441. control.embedWidgetControl();
  442. } else {
  443. api.section( control.section(), function( section ) {
  444. var onExpanded = function( isExpanded ) {
  445. if ( isExpanded ) {
  446. control.embedWidgetControl();
  447. section.expanded.unbind( onExpanded );
  448. }
  449. };
  450. if ( section.expanded() ) {
  451. onExpanded( true );
  452. } else {
  453. section.expanded.bind( onExpanded );
  454. }
  455. } );
  456. }
  457. },
  458. /**
  459. * Embed the .widget element inside the li container.
  460. *
  461. * @since 4.4.0
  462. */
  463. embedWidgetControl: function() {
  464. var control = this, widgetControl;
  465. if ( control.widgetControlEmbedded ) {
  466. return;
  467. }
  468. control.widgetControlEmbedded = true;
  469. widgetControl = $( control.params.widget_control );
  470. control.container.append( widgetControl );
  471. control._setupModel();
  472. control._setupWideWidget();
  473. control._setupControlToggle();
  474. control._setupWidgetTitle();
  475. control._setupReorderUI();
  476. control._setupHighlightEffects();
  477. control._setupUpdateUI();
  478. control._setupRemoveUI();
  479. },
  480. /**
  481. * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event.
  482. *
  483. * @since 4.4.0
  484. */
  485. embedWidgetContent: function() {
  486. var control = this, widgetContent;
  487. control.embedWidgetControl();
  488. if ( control.widgetContentEmbedded ) {
  489. return;
  490. }
  491. control.widgetContentEmbedded = true;
  492. // Update the notification container element now that the widget content has been embedded.
  493. control.notifications.container = control.getNotificationsContainerElement();
  494. control.notifications.render();
  495. widgetContent = $( control.params.widget_content );
  496. control.container.find( '.widget-content:first' ).append( widgetContent );
  497. /*
  498. * Trigger widget-added event so that plugins can attach any event
  499. * listeners and dynamic UI elements.
  500. */
  501. $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] );
  502. },
  503. /**
  504. * Handle changes to the setting
  505. */
  506. _setupModel: function() {
  507. var self = this, rememberSavedWidgetId;
  508. // Remember saved widgets so we know which to trash (move to inactive widgets sidebar).
  509. rememberSavedWidgetId = function() {
  510. api.Widgets.savedWidgetIds[self.params.widget_id] = true;
  511. };
  512. api.bind( 'ready', rememberSavedWidgetId );
  513. api.bind( 'saved', rememberSavedWidgetId );
  514. this._updateCount = 0;
  515. this.isWidgetUpdating = false;
  516. this.liveUpdateMode = true;
  517. // Update widget whenever model changes.
  518. this.setting.bind( function( to, from ) {
  519. if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) {
  520. self.updateWidget( { instance: to } );
  521. }
  522. } );
  523. },
  524. /**
  525. * Add special behaviors for wide widget controls
  526. */
  527. _setupWideWidget: function() {
  528. var self = this, $widgetInside, $widgetForm, $customizeSidebar,
  529. $themeControlsContainer, positionWidget;
  530. if ( ! this.params.is_wide || $( window ).width() <= 640 /* max-width breakpoint in customize-controls.css */ ) {
  531. return;
  532. }
  533. $widgetInside = this.container.find( '.widget-inside' );
  534. $widgetForm = $widgetInside.find( '> .form' );
  535. $customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
  536. this.container.addClass( 'wide-widget-control' );
  537. this.container.find( '.form:first' ).css( {
  538. 'max-width': this.params.width,
  539. 'min-height': this.params.height
  540. } );
  541. /**
  542. * Keep the widget-inside positioned so the top of fixed-positioned
  543. * element is at the same top position as the widget-top. When the
  544. * widget-top is scrolled out of view, keep the widget-top in view;
  545. * likewise, don't allow the widget to drop off the bottom of the window.
  546. * If a widget is too tall to fit in the window, don't let the height
  547. * exceed the window height so that the contents of the widget control
  548. * will become scrollable (overflow:auto).
  549. */
  550. positionWidget = function() {
  551. var offsetTop = self.container.offset().top,
  552. windowHeight = $( window ).height(),
  553. formHeight = $widgetForm.outerHeight(),
  554. top;
  555. $widgetInside.css( 'max-height', windowHeight );
  556. top = Math.max(
  557. 0, // Prevent top from going off screen.
  558. Math.min(
  559. Math.max( offsetTop, 0 ), // Distance widget in panel is from top of screen.
  560. windowHeight - formHeight // Flush up against bottom of screen.
  561. )
  562. );
  563. $widgetInside.css( 'top', top );
  564. };
  565. $themeControlsContainer = $( '#customize-theme-controls' );
  566. this.container.on( 'expand', function() {
  567. positionWidget();
  568. $customizeSidebar.on( 'scroll', positionWidget );
  569. $( window ).on( 'resize', positionWidget );
  570. $themeControlsContainer.on( 'expanded collapsed', positionWidget );
  571. } );
  572. this.container.on( 'collapsed', function() {
  573. $customizeSidebar.off( 'scroll', positionWidget );
  574. $( window ).off( 'resize', positionWidget );
  575. $themeControlsContainer.off( 'expanded collapsed', positionWidget );
  576. } );
  577. // Reposition whenever a sidebar's widgets are changed.
  578. api.each( function( setting ) {
  579. if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
  580. setting.bind( function() {
  581. if ( self.container.hasClass( 'expanded' ) ) {
  582. positionWidget();
  583. }
  584. } );
  585. }
  586. } );
  587. },
  588. /**
  589. * Show/hide the control when clicking on the form title, when clicking
  590. * the close button
  591. */
  592. _setupControlToggle: function() {
  593. var self = this, $closeBtn;
  594. this.container.find( '.widget-top' ).on( 'click', function( e ) {
  595. e.preventDefault();
  596. var sidebarWidgetsControl = self.getSidebarWidgetsControl();
  597. if ( sidebarWidgetsControl.isReordering ) {
  598. return;
  599. }
  600. self.expanded( ! self.expanded() );
  601. } );
  602. $closeBtn = this.container.find( '.widget-control-close' );
  603. $closeBtn.on( 'click', function() {
  604. self.collapse();
  605. self.container.find( '.widget-top .widget-action:first' ).focus(); // Keyboard accessibility.
  606. } );
  607. },
  608. /**
  609. * Update the title of the form if a title field is entered
  610. */
  611. _setupWidgetTitle: function() {
  612. var self = this, updateTitle;
  613. updateTitle = function() {
  614. var title = self.setting().title,
  615. inWidgetTitle = self.container.find( '.in-widget-title' );
  616. if ( title ) {
  617. inWidgetTitle.text( ': ' + title );
  618. } else {
  619. inWidgetTitle.text( '' );
  620. }
  621. };
  622. this.setting.bind( updateTitle );
  623. updateTitle();
  624. },
  625. /**
  626. * Set up the widget-reorder-nav
  627. */
  628. _setupReorderUI: function() {
  629. var self = this, selectSidebarItem, $moveWidgetArea,
  630. $reorderNav, updateAvailableSidebars, template;
  631. /**
  632. * select the provided sidebar list item in the move widget area
  633. *
  634. * @param {jQuery} li
  635. */
  636. selectSidebarItem = function( li ) {
  637. li.siblings( '.selected' ).removeClass( 'selected' );
  638. li.addClass( 'selected' );
  639. var isSelfSidebar = ( li.data( 'id' ) === self.params.sidebar_id );
  640. self.container.find( '.move-widget-btn' ).prop( 'disabled', isSelfSidebar );
  641. };
  642. /**
  643. * Add the widget reordering elements to the widget control
  644. */
  645. this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
  646. template = _.template( api.Widgets.data.tpl.moveWidgetArea );
  647. $moveWidgetArea = $( template( {
  648. sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
  649. } )
  650. );
  651. this.container.find( '.widget-top' ).after( $moveWidgetArea );
  652. /**
  653. * Update available sidebars when their rendered state changes
  654. */
  655. updateAvailableSidebars = function() {
  656. var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem,
  657. renderedSidebarCount = 0;
  658. selfSidebarItem = $sidebarItems.filter( function(){
  659. return $( this ).data( 'id' ) === self.params.sidebar_id;
  660. } );
  661. $sidebarItems.each( function() {
  662. var li = $( this ),
  663. sidebarId, sidebar, sidebarIsRendered;
  664. sidebarId = li.data( 'id' );
  665. sidebar = api.Widgets.registeredSidebars.get( sidebarId );
  666. sidebarIsRendered = sidebar.get( 'is_rendered' );
  667. li.toggle( sidebarIsRendered );
  668. if ( sidebarIsRendered ) {
  669. renderedSidebarCount += 1;
  670. }
  671. if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) {
  672. selectSidebarItem( selfSidebarItem );
  673. }
  674. } );
  675. if ( renderedSidebarCount > 1 ) {
  676. self.container.find( '.move-widget' ).show();
  677. } else {
  678. self.container.find( '.move-widget' ).hide();
  679. }
  680. };
  681. updateAvailableSidebars();
  682. api.Widgets.registeredSidebars.on( 'change:is_rendered', updateAvailableSidebars );
  683. /**
  684. * Handle clicks for up/down/move on the reorder nav
  685. */
  686. $reorderNav = this.container.find( '.widget-reorder-nav' );
  687. $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() {
  688. $( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' );
  689. } ).on( 'click keypress', function( event ) {
  690. if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
  691. return;
  692. }
  693. $( this ).trigger( 'focus' );
  694. if ( $( this ).is( '.move-widget' ) ) {
  695. self.toggleWidgetMoveArea();
  696. } else {
  697. var isMoveDown = $( this ).is( '.move-widget-down' ),
  698. isMoveUp = $( this ).is( '.move-widget-up' ),
  699. i = self.getWidgetSidebarPosition();
  700. if ( ( isMoveUp && i === 0 ) || ( isMoveDown && i === self.getSidebarWidgetsControl().setting().length - 1 ) ) {
  701. return;
  702. }
  703. if ( isMoveUp ) {
  704. self.moveUp();
  705. wp.a11y.speak( l10n.widgetMovedUp );
  706. } else {
  707. self.moveDown();
  708. wp.a11y.speak( l10n.widgetMovedDown );
  709. }
  710. $( this ).trigger( 'focus' ); // Re-focus after the container was moved.
  711. }
  712. } );
  713. /**
  714. * Handle selecting a sidebar to move to
  715. */
  716. this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) {
  717. if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
  718. return;
  719. }
  720. event.preventDefault();
  721. selectSidebarItem( $( this ) );
  722. } );
  723. /**
  724. * Move widget to another sidebar
  725. */
  726. this.container.find( '.move-widget-btn' ).click( function() {
  727. self.getSidebarWidgetsControl().toggleReordering( false );
  728. var oldSidebarId = self.params.sidebar_id,
  729. newSidebarId = self.container.find( '.widget-area-select li.selected' ).data( 'id' ),
  730. oldSidebarWidgetsSetting, newSidebarWidgetsSetting,
  731. oldSidebarWidgetIds, newSidebarWidgetIds, i;
  732. oldSidebarWidgetsSetting = api( 'sidebars_widgets[' + oldSidebarId + ']' );
  733. newSidebarWidgetsSetting = api( 'sidebars_widgets[' + newSidebarId + ']' );
  734. oldSidebarWidgetIds = Array.prototype.slice.call( oldSidebarWidgetsSetting() );
  735. newSidebarWidgetIds = Array.prototype.slice.call( newSidebarWidgetsSetting() );
  736. i = self.getWidgetSidebarPosition();
  737. oldSidebarWidgetIds.splice( i, 1 );
  738. newSidebarWidgetIds.push( self.params.widget_id );
  739. oldSidebarWidgetsSetting( oldSidebarWidgetIds );
  740. newSidebarWidgetsSetting( newSidebarWidgetIds );
  741. self.focus();
  742. } );
  743. },
  744. /**
  745. * Highlight widgets in preview when interacted with in the Customizer
  746. */
  747. _setupHighlightEffects: function() {
  748. var self = this;
  749. // Highlight whenever hovering or clicking over the form.
  750. this.container.on( 'mouseenter click', function() {
  751. self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
  752. } );
  753. // Highlight when the setting is updated.
  754. this.setting.bind( function() {
  755. self.setting.previewer.send( 'highlight-widget', self.params.widget_id );
  756. } );
  757. },
  758. /**
  759. * Set up event handlers for widget updating
  760. */
  761. _setupUpdateUI: function() {
  762. var self = this, $widgetRoot, $widgetContent,
  763. $saveBtn, updateWidgetDebounced, formSyncHandler;
  764. $widgetRoot = this.container.find( '.widget:first' );
  765. $widgetContent = $widgetRoot.find( '.widget-content:first' );
  766. // Configure update button.
  767. $saveBtn = this.container.find( '.widget-control-save' );
  768. $saveBtn.val( l10n.saveBtnLabel );
  769. $saveBtn.attr( 'title', l10n.saveBtnTooltip );
  770. $saveBtn.removeClass( 'button-primary' );
  771. $saveBtn.on( 'click', function( e ) {
  772. e.preventDefault();
  773. self.updateWidget( { disable_form: true } ); // @todo disable_form is unused?
  774. } );
  775. updateWidgetDebounced = _.debounce( function() {
  776. self.updateWidget();
  777. }, 250 );
  778. // Trigger widget form update when hitting Enter within an input.
  779. $widgetContent.on( 'keydown', 'input', function( e ) {
  780. if ( 13 === e.which ) { // Enter.
  781. e.preventDefault();
  782. self.updateWidget( { ignoreActiveElement: true } );
  783. }
  784. } );
  785. // Handle widgets that support live previews.
  786. $widgetContent.on( 'change input propertychange', ':input', function( e ) {
  787. if ( ! self.liveUpdateMode ) {
  788. return;
  789. }
  790. if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
  791. updateWidgetDebounced();
  792. }
  793. } );
  794. // Remove loading indicators when the setting is saved and the preview updates.
  795. this.setting.previewer.channel.bind( 'synced', function() {
  796. self.container.removeClass( 'previewer-loading' );
  797. } );
  798. api.previewer.bind( 'widget-updated', function( updatedWidgetId ) {
  799. if ( updatedWidgetId === self.params.widget_id ) {
  800. self.container.removeClass( 'previewer-loading' );
  801. }
  802. } );
  803. formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ];
  804. if ( formSyncHandler ) {
  805. $( document ).on( 'widget-synced', function( e, widget ) {
  806. if ( $widgetRoot.is( widget ) ) {
  807. formSyncHandler.apply( document, arguments );
  808. }
  809. } );
  810. }
  811. },
  812. /**
  813. * Update widget control to indicate whether it is currently rendered.
  814. *
  815. * Overrides api.Control.toggle()
  816. *
  817. * @since 4.1.0
  818. *
  819. * @param {boolean} active
  820. * @param {Object} args
  821. * @param {function} args.completeCallback
  822. */
  823. onChangeActive: function ( active, args ) {
  824. // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments.
  825. this.container.toggleClass( 'widget-rendered', active );
  826. if ( args.completeCallback ) {
  827. args.completeCallback();
  828. }
  829. },
  830. /**
  831. * Set up event handlers for widget removal
  832. */
  833. _setupRemoveUI: function() {
  834. var self = this, $removeBtn, replaceDeleteWithRemove;
  835. // Configure remove button.
  836. $removeBtn = this.container.find( '.widget-control-remove' );
  837. $removeBtn.on( 'click', function() {
  838. // Find an adjacent element to add focus to when this widget goes away.
  839. var $adjacentFocusTarget;
  840. if ( self.container.next().is( '.customize-control-widget_form' ) ) {
  841. $adjacentFocusTarget = self.container.next().find( '.widget-action:first' );
  842. } else if ( self.container.prev().is( '.customize-control-widget_form' ) ) {
  843. $adjacentFocusTarget = self.container.prev().find( '.widget-action:first' );
  844. } else {
  845. $adjacentFocusTarget = self.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
  846. }
  847. self.container.slideUp( function() {
  848. var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ),
  849. sidebarWidgetIds, i;
  850. if ( ! sidebarsWidgetsControl ) {
  851. return;
  852. }
  853. sidebarWidgetIds = sidebarsWidgetsControl.setting().slice();
  854. i = _.indexOf( sidebarWidgetIds, self.params.widget_id );
  855. if ( -1 === i ) {
  856. return;
  857. }
  858. sidebarWidgetIds.splice( i, 1 );
  859. sidebarsWidgetsControl.setting( sidebarWidgetIds );
  860. $adjacentFocusTarget.focus(); // Keyboard accessibility.
  861. } );
  862. } );
  863. replaceDeleteWithRemove = function() {
  864. $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete".
  865. $removeBtn.attr( 'title', l10n.removeBtnTooltip );
  866. };
  867. if ( this.params.is_new ) {
  868. api.bind( 'saved', replaceDeleteWithRemove );
  869. } else {
  870. replaceDeleteWithRemove();
  871. }
  872. },
  873. /**
  874. * Find all inputs in a widget container that should be considered when
  875. * comparing the loaded form with the sanitized form, whose fields will
  876. * be aligned to copy the sanitized over. The elements returned by this
  877. * are passed into this._getInputsSignature(), and they are iterated
  878. * over when copying sanitized values over to the form loaded.
  879. *
  880. * @param {jQuery} container element in which to look for inputs
  881. * @return {jQuery} inputs
  882. * @private
  883. */
  884. _getInputs: function( container ) {
  885. return $( container ).find( ':input[name]' );
  886. },
  887. /**
  888. * Iterate over supplied inputs and create a signature string for all of them together.
  889. * This string can be used to compare whether or not the form has all of the same fields.
  890. *
  891. * @param {jQuery} inputs
  892. * @return {string}
  893. * @private
  894. */
  895. _getInputsSignature: function( inputs ) {
  896. var inputsSignatures = _( inputs ).map( function( input ) {
  897. var $input = $( input ), signatureParts;
  898. if ( $input.is( ':checkbox, :radio' ) ) {
  899. signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ), $input.prop( 'value' ) ];
  900. } else {
  901. signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ) ];
  902. }
  903. return signatureParts.join( ',' );
  904. } );
  905. return inputsSignatures.join( ';' );
  906. },
  907. /**
  908. * Get the state for an input depending on its type.
  909. *
  910. * @param {jQuery|Element} input
  911. * @return {string|boolean|Array|*}
  912. * @private
  913. */
  914. _getInputState: function( input ) {
  915. input = $( input );
  916. if ( input.is( ':radio, :checkbox' ) ) {
  917. return input.prop( 'checked' );
  918. } else if ( input.is( 'select[multiple]' ) ) {
  919. return input.find( 'option:selected' ).map( function () {
  920. return $( this ).val();
  921. } ).get();
  922. } else {
  923. return input.val();
  924. }
  925. },
  926. /**
  927. * Update an input's state based on its type.
  928. *
  929. * @param {jQuery|Element} input
  930. * @param {string|boolean|Array|*} state
  931. * @private
  932. */
  933. _setInputState: function ( input, state ) {
  934. input = $( input );
  935. if ( input.is( ':radio, :checkbox' ) ) {
  936. input.prop( 'checked', state );
  937. } else if ( input.is( 'select[multiple]' ) ) {
  938. if ( ! Array.isArray( state ) ) {
  939. state = [];
  940. } else {
  941. // Make sure all state items are strings since the DOM value is a string.
  942. state = _.map( state, function ( value ) {
  943. return String( value );
  944. } );
  945. }
  946. input.find( 'option' ).each( function () {
  947. $( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) );
  948. } );
  949. } else {
  950. input.val( state );
  951. }
  952. },
  953. /***********************************************************************
  954. * Begin public API methods
  955. **********************************************************************/
  956. /**
  957. * @return {wp.customize.controlConstructor.sidebar_widgets[]}
  958. */
  959. getSidebarWidgetsControl: function() {
  960. var settingId, sidebarWidgetsControl;
  961. settingId = 'sidebars_widgets[' + this.params.sidebar_id + ']';
  962. sidebarWidgetsControl = api.control( settingId );
  963. if ( ! sidebarWidgetsControl ) {
  964. return;
  965. }
  966. return sidebarWidgetsControl;
  967. },
  968. /**
  969. * Submit the widget form via Ajax and get back the updated instance,
  970. * along with the new widget control form to render.
  971. *
  972. * @param {Object} [args]
  973. * @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used
  974. * @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.
  975. * @param {boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element.
  976. */
  977. updateWidget: function( args ) {
  978. var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent,
  979. updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
  980. // The updateWidget logic requires that the form fields to be fully present.
  981. self.embedWidgetContent();
  982. args = $.extend( {
  983. instance: null,
  984. complete: null,
  985. ignoreActiveElement: false
  986. }, args );
  987. instanceOverride = args.instance;
  988. completeCallback = args.complete;
  989. this._updateCount += 1;
  990. updateNumber = this._updateCount;
  991. $widgetRoot = this.container.find( '.widget:first' );
  992. $widgetContent = $widgetRoot.find( '.widget-content:first' );
  993. // Remove a previous error message.
  994. $widgetContent.find( '.widget-error' ).remove();
  995. this.container.addClass( 'widget-form-loading' );
  996. this.container.addClass( 'previewer-loading' );
  997. processing = api.state( 'processing' );
  998. processing( processing() + 1 );
  999. if ( ! this.liveUpdateMode ) {
  1000. this.container.addClass( 'widget-form-disabled' );
  1001. }
  1002. params = {};
  1003. params.action = 'update-widget';
  1004. params.wp_customize = 'on';
  1005. params.nonce = api.settings.nonce['update-widget'];
  1006. params.customize_theme = api.settings.theme.stylesheet;
  1007. params.customized = wp.customize.previewer.query().customized;
  1008. data = $.param( params );
  1009. $inputs = this._getInputs( $widgetContent );
  1010. /*
  1011. * Store the value we're submitting in data so that when the response comes back,
  1012. * we know if it got sanitized; if there is no difference in the sanitized value,
  1013. * then we do not need to touch the UI and mess up the user's ongoing editing.
  1014. */
  1015. $inputs.each( function() {
  1016. $( this ).data( 'state' + updateNumber, self._getInputState( this ) );
  1017. } );
  1018. if ( instanceOverride ) {
  1019. data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } );
  1020. } else {
  1021. data += '&' + $inputs.serialize();
  1022. }
  1023. data += '&' + $widgetContent.find( '~ :input' ).serialize();
  1024. if ( this._previousUpdateRequest ) {
  1025. this._previousUpdateRequest.abort();
  1026. }
  1027. jqxhr = $.post( wp.ajax.settings.url, data );
  1028. this._previousUpdateRequest = jqxhr;
  1029. jqxhr.done( function( r ) {
  1030. var message, sanitizedForm, $sanitizedInputs, hasSameInputsInResponse,
  1031. isLiveUpdateAborted = false;
  1032. // Check if the user is logged out.
  1033. if ( '0' === r ) {
  1034. api.previewer.preview.iframe.hide();
  1035. api.previewer.login().done( function() {
  1036. self.updateWidget( args );
  1037. api.previewer.preview.iframe.show();
  1038. } );
  1039. return;
  1040. }
  1041. // Check for cheaters.
  1042. if ( '-1' === r ) {
  1043. api.previewer.cheatin();
  1044. return;
  1045. }
  1046. if ( r.success ) {
  1047. sanitizedForm = $( '<div>' + r.data.form + '</div>' );
  1048. $sanitizedInputs = self._getInputs( sanitizedForm );
  1049. hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs );
  1050. // Restore live update mode if sanitized fields are now aligned with the existing fields.
  1051. if ( hasSameInputsInResponse && ! self.liveUpdateMode ) {
  1052. self.liveUpdateMode = true;
  1053. self.container.removeClass( 'widget-form-disabled' );
  1054. self.container.find( 'input[name="savewidget"]' ).hide();
  1055. }
  1056. // Sync sanitized field states to existing fields if they are aligned.
  1057. if ( hasSameInputsInResponse && self.liveUpdateMode ) {
  1058. $inputs.each( function( i ) {
  1059. var $input = $( this ),
  1060. $sanitizedInput = $( $sanitizedInputs[i] ),
  1061. submittedState, sanitizedState, canUpdateState;
  1062. submittedState = $input.data( 'state' + updateNumber );
  1063. sanitizedState = self._getInputState( $sanitizedInput );
  1064. $input.data( 'sanitized', sanitizedState );
  1065. canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) );
  1066. if ( canUpdateState ) {
  1067. self._setInputState( $input, sanitizedState );
  1068. }
  1069. } );
  1070. $( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] );
  1071. // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled.
  1072. } else if ( self.liveUpdateMode ) {
  1073. self.liveUpdateMode = false;
  1074. self.container.find( 'input[name="savewidget"]' ).show();
  1075. isLiveUpdateAborted = true;
  1076. // Otherwise, replace existing form with the sanitized form.
  1077. } else {
  1078. $widgetContent.html( r.data.form );
  1079. self.container.removeClass( 'widget-form-disabled' );
  1080. $( document ).trigger( 'widget-updated', [ $widgetRoot ] );
  1081. }
  1082. /**
  1083. * If the old instance is identical to the new one, there is nothing new
  1084. * needing to be rendered, and so we can preempt the event for the
  1085. * preview finishing loading.
  1086. */
  1087. isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
  1088. if ( isChanged ) {
  1089. self.isWidgetUpdating = true; // Suppress triggering another updateWidget.
  1090. self.setting( r.data.instance );
  1091. self.isWidgetUpdating = false;
  1092. } else {
  1093. // No change was made, so stop the spinner now instead of when the preview would updates.
  1094. self.container.removeClass( 'previewer-loading' );
  1095. }
  1096. if ( completeCallback ) {
  1097. completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } );
  1098. }
  1099. } else {
  1100. // General error message.
  1101. message = l10n.error;
  1102. if ( r.data && r.data.message ) {
  1103. message = r.data.message;
  1104. }
  1105. if ( completeCallback ) {
  1106. completeCallback.call( self, message );
  1107. } else {
  1108. $widgetContent.prepend( '<p class="widget-error"><strong>' + message + '</strong></p>' );
  1109. }
  1110. }
  1111. } );
  1112. jqxhr.fail( function( jqXHR, textStatus ) {
  1113. if ( completeCallback ) {
  1114. completeCallback.call( self, textStatus );
  1115. }
  1116. } );
  1117. jqxhr.always( function() {
  1118. self.container.removeClass( 'widget-form-loading' );
  1119. $inputs.each( function() {
  1120. $( this ).removeData( 'state' + updateNumber );
  1121. } );
  1122. processing( processing() - 1 );
  1123. } );
  1124. },
  1125. /**
  1126. * Expand the accordion section containing a control
  1127. */
  1128. expandControlSection: function() {
  1129. api.Control.prototype.expand.call( this );
  1130. },
  1131. /**
  1132. * @since 4.1.0
  1133. *
  1134. * @param {Boolean} expanded
  1135. * @param {Object} [params]
  1136. * @return {Boolean} False if state already applied.
  1137. */
  1138. _toggleExpanded: api.Section.prototype._toggleExpanded,
  1139. /**
  1140. * @since 4.1.0
  1141. *
  1142. * @param {Object} [params]
  1143. * @return {Boolean} False if already expanded.
  1144. */
  1145. expand: api.Section.prototype.expand,
  1146. /**
  1147. * Expand the widget form control
  1148. *
  1149. * @deprecated 4.1.0 Use this.expand() instead.
  1150. */
  1151. expandForm: function() {
  1152. this.expand();
  1153. },
  1154. /**
  1155. * @since 4.1.0
  1156. *
  1157. * @param {Object} [params]
  1158. * @return {Boolean} False if already collapsed.
  1159. */
  1160. collapse: api.Section.prototype.collapse,
  1161. /**
  1162. * Collapse the widget form control
  1163. *
  1164. * @deprecated 4.1.0 Use this.collapse() instead.
  1165. */
  1166. collapseForm: function() {
  1167. this.collapse();
  1168. },
  1169. /**
  1170. * Expand or collapse the widget control
  1171. *
  1172. * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
  1173. *
  1174. * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
  1175. */
  1176. toggleForm: function( showOrHide ) {
  1177. if ( typeof showOrHide === 'undefined' ) {
  1178. showOrHide = ! this.expanded();
  1179. }
  1180. this.expanded( showOrHide );
  1181. },
  1182. /**
  1183. * Respond to change in the expanded state.
  1184. *
  1185. * @param {boolean} expanded
  1186. * @param {Object} args merged on top of this.defaultActiveArguments
  1187. */
  1188. onChangeExpanded: function ( expanded, args ) {
  1189. var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn;
  1190. self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
  1191. if ( expanded ) {
  1192. self.embedWidgetContent();
  1193. }
  1194. // If the expanded state is unchanged only manipulate container expanded states.
  1195. if ( args.unchanged ) {
  1196. if ( expanded ) {
  1197. api.Control.prototype.expand.call( self, {
  1198. completeCallback: args.completeCallback
  1199. });
  1200. }
  1201. return;
  1202. }
  1203. $widget = this.container.find( 'div.widget:first' );
  1204. $inside = $widget.find( '.widget-inside:first' );
  1205. $toggleBtn = this.container.find( '.widget-top button.widget-action' );
  1206. expandControl = function() {
  1207. // Close all other widget controls before expanding this one.
  1208. api.control.each( function( otherControl ) {
  1209. if ( self.params.type === otherControl.params.type && self !== otherControl ) {
  1210. otherControl.collapse();
  1211. }
  1212. } );
  1213. complete = function() {
  1214. self.container.removeClass( 'expanding' );
  1215. self.container.addClass( 'expanded' );
  1216. $widget.addClass( 'open' );
  1217. $toggleBtn.attr( 'aria-expanded', 'true' );
  1218. self.container.trigger( 'expanded' );
  1219. };
  1220. if ( args.completeCallback ) {
  1221. prevComplete = complete;
  1222. complete = function () {
  1223. prevComplete();
  1224. args.completeCallback();
  1225. };
  1226. }
  1227. if ( self.params.is_wide ) {
  1228. $inside.fadeIn( args.duration, complete );
  1229. } else {
  1230. $inside.slideDown( args.duration, complete );
  1231. }
  1232. self.container.trigger( 'expand' );
  1233. self.container.addClass( 'expanding' );
  1234. };
  1235. if ( expanded ) {
  1236. if ( api.section.has( self.section() ) ) {
  1237. api.section( self.section() ).expand( {
  1238. completeCallback: expandControl
  1239. } );
  1240. } else {
  1241. expandControl();
  1242. }
  1243. } else {
  1244. complete = function() {
  1245. self.container.removeClass( 'collapsing' );
  1246. self.container.removeClass( 'expanded' );
  1247. $widget.removeClass( 'open' );
  1248. $toggleBtn.attr( 'aria-expanded', 'false' );
  1249. self.container.trigger( 'collapsed' );
  1250. };
  1251. if ( args.completeCallback ) {
  1252. prevComplete = complete;
  1253. complete = function () {
  1254. prevComplete();
  1255. args.completeCallback();
  1256. };
  1257. }
  1258. self.container.trigger( 'collapse' );
  1259. self.container.addClass( 'collapsing' );
  1260. if ( self.params.is_wide ) {
  1261. $inside.fadeOut( args.duration, complete );
  1262. } else {
  1263. $inside.slideUp( args.duration, function() {
  1264. $widget.css( { width:'', margin:'' } );
  1265. complete();
  1266. } );
  1267. }
  1268. }
  1269. },
  1270. /**
  1271. * Get the position (index) of the widget in the containing sidebar
  1272. *
  1273. * @return {number}
  1274. */
  1275. getWidgetSidebarPosition: function() {
  1276. var sidebarWidgetIds, position;
  1277. sidebarWidgetIds = this.getSidebarWidgetsControl().setting();
  1278. position = _.indexOf( sidebarWidgetIds, this.params.widget_id );
  1279. if ( position === -1 ) {
  1280. return;
  1281. }
  1282. return position;
  1283. },
  1284. /**
  1285. * Move widget up one in the sidebar
  1286. */
  1287. moveUp: function() {
  1288. this._moveWidgetByOne( -1 );
  1289. },
  1290. /**
  1291. * Move widget up one in the sidebar
  1292. */
  1293. moveDown: function() {
  1294. this._moveWidgetByOne( 1 );
  1295. },
  1296. /**
  1297. * @private
  1298. *
  1299. * @param {number} offset 1|-1
  1300. */
  1301. _moveWidgetByOne: function( offset ) {
  1302. var i, sidebarWidgetsSetting, sidebarWidgetIds, adjacentWidgetId;
  1303. i = this.getWidgetSidebarPosition();
  1304. sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting;
  1305. sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // Clone.
  1306. adjacentWidgetId = sidebarWidgetIds[i + offset];
  1307. sidebarWidgetIds[i + offset] = this.params.widget_id;
  1308. sidebarWidgetIds[i] = adjacentWidgetId;
  1309. sidebarWidgetsSetting( sidebarWidgetIds );
  1310. },
  1311. /**
  1312. * Toggle visibility of the widget move area
  1313. *
  1314. * @param {boolean} [showOrHide]
  1315. */
  1316. toggleWidgetMoveArea: function( showOrHide ) {
  1317. var self = this, $moveWidgetArea;
  1318. $moveWidgetArea = this.container.find( '.move-widget-area' );
  1319. if ( typeof showOrHide === 'undefined' ) {
  1320. showOrHide = ! $moveWidgetArea.hasClass( 'active' );
  1321. }
  1322. if ( showOrHide ) {
  1323. // Reset the selected sidebar.
  1324. $moveWidgetArea.find( '.selected' ).removeClass( 'selected' );
  1325. $moveWidgetArea.find( 'li' ).filter( function() {
  1326. return $( this ).data( 'id' ) === self.params.sidebar_id;
  1327. } ).addClass( 'selected' );
  1328. this.container.find( '.move-widget-btn' ).prop( 'disabled', true );
  1329. }
  1330. $moveWidgetArea.toggleClass( 'active', showOrHide );
  1331. },
  1332. /**
  1333. * Highlight the widget control and section
  1334. */
  1335. highlightSectionAndControl: function() {
  1336. var $target;
  1337. if ( this.container.is( ':hidden' ) ) {
  1338. $target = this.container.closest( '.control-section' );
  1339. } else {
  1340. $target = this.container;
  1341. }
  1342. $( '.highlighted' ).removeClass( 'highlighted' );
  1343. $target.addClass( 'highlighted' );
  1344. setTimeout( function() {
  1345. $target.removeClass( 'highlighted' );
  1346. }, 500 );
  1347. }
  1348. } );
  1349. /**
  1350. * wp.customize.Widgets.WidgetsPanel
  1351. *
  1352. * Customizer panel containing the widget area sections.
  1353. *
  1354. * @since 4.4.0
  1355. *
  1356. * @class wp.customize.Widgets.WidgetsPanel
  1357. * @augments wp.customize.Panel
  1358. */
  1359. api.Widgets.WidgetsPanel = api.Panel.extend(/** @lends wp.customize.Widgets.WigetsPanel.prototype */{
  1360. /**
  1361. * Add and manage the display of the no-rendered-areas notice.
  1362. *
  1363. * @since 4.4.0
  1364. */
  1365. ready: function () {
  1366. var panel = this;
  1367. api.Panel.prototype.ready.call( panel );
  1368. panel.deferred.embedded.done(function() {
  1369. var panelMetaContainer, noticeContainer, updateNotice, getActiveSectionCount, shouldShowNotice;
  1370. panelMetaContainer = panel.container.find( '.panel-meta' );
  1371. // @todo This should use the Notifications API introduced to panels. See <https://core.trac.wordpress.org/ticket/38794>.
  1372. noticeContainer = $( '<div></div>', {
  1373. 'class': 'no-widget-areas-rendered-notice'
  1374. });
  1375. panelMetaContainer.append( noticeContainer );
  1376. /**
  1377. * Get the number of active sections in the panel.
  1378. *
  1379. * @return {number} Number of active sidebar sections.
  1380. */
  1381. getActiveSectionCount = function() {
  1382. return _.filter( panel.sections(), function( section ) {
  1383. return 'sidebar' === section.params.type && section.active();
  1384. } ).length;
  1385. };
  1386. /**
  1387. * Determine whether or not the notice should be displayed.
  1388. *
  1389. * @return {boolean}
  1390. */
  1391. shouldShowNotice = function() {
  1392. var activeSectionCount = getActiveSectionCount();
  1393. if ( 0 === activeSectionCount ) {
  1394. return true;
  1395. } else {
  1396. return activeSectionCount !== api.Widgets.data.registeredSidebars.length;
  1397. }
  1398. };
  1399. /**
  1400. * Update the notice.
  1401. *
  1402. * @return {void}
  1403. */
  1404. updateNotice = function() {
  1405. var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount;
  1406. noticeContainer.empty();
  1407. registeredAreaCount = api.Widgets.data.registeredSidebars.length;
  1408. if ( activeSectionCount !== registeredAreaCount ) {
  1409. if ( 0 !== activeSectionCount ) {
  1410. nonRenderedAreaCount = registeredAreaCount - activeSectionCount;
  1411. someRenderedMessage = l10n.someAreasShown[ nonRenderedAreaCount ];
  1412. } else {
  1413. someRenderedMessage = l10n.noAreasShown;
  1414. }
  1415. if ( someRenderedMessage ) {
  1416. noticeContainer.append( $( '<p></p>', {
  1417. text: someRenderedMessage
  1418. } ) );
  1419. }
  1420. noticeContainer.append( $( '<p></p>', {
  1421. text: l10n.navigatePreview
  1422. } ) );
  1423. }
  1424. };
  1425. updateNotice();
  1426. /*
  1427. * Set the initial visibility state for rendered notice.
  1428. * Update the visibility of the