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


Generated: Tue Aug 11 01:00:04 2020 Cross-referenced by PHPXref 0.7.1