[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

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


Generated : Sun Sep 13 08:20:28 2026 Cross-referenced by PHPXref