[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-admin/js/ -> customize-widgets.js (source)

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


Generated: Tue Sep 17 01:00:03 2019 Cross-referenced by PHPXref 0.7.1