home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-admin / js / customize-widgets.js < prev    next >
Encoding:
JavaScript  |  2017-10-13  |  68.7 KB  |  2,339 lines

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