PageRenderTime 54ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

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

https://bitbucket.org/Thane2376/death-edge.ru
JavaScript | 1882 lines | 1167 code | 319 blank | 396 comment | 207 complexity | 6441028ec278c3e8c4476081db5ab6b0 MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, LGPL-3.0, AGPL-1.0

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

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