PageRenderTime 34ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

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

http://github.com/wordpress/wordpress
JavaScript | 2373 lines | 1446 code | 361 blank | 566 comment | 251 complexity | 9397c78bbf00b486338b2b41d0749790 MD5 | raw file
Possible License(s): 0BSD

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

Large files files are truncated, but you can click here to view the full file