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


Generated: Tue Jun 2 01:00:03 2020 Cross-referenced by PHPXref 0.7.1