PageRenderTime 281ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

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

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