Make WordPress Core

Changeset 27985


Ignore:
Timestamp:
04/07/2014 09:03:18 AM (11 years ago)
Author:
ocean90
Message:

Widget Customizer: Move WidgetCustomizer to wp.customize.Widgets. First pass.

see #27690.

Location:
trunk/src
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/css/customize-controls.css

    r27970 r27985  
    491491    -webkit-border-radius: 2px;
    492492    border: 1px solid #eee;
     493    -webkit-border-radius: 2px;
    493494    border-radius: 2px;
    494495}
  • trunk/src/wp-admin/css/customize-widgets.css

    r27912 r27985  
    108108    display: none;
    109109}
    110 
    111 
    112 /* MP6-compat */
    113 #customize-theme-controls .accordion-section-content .widget {
    114     color: black;
    115 }
    116 
    117110
    118111/**
     
    327320body.adding-widget .add-new-widget,
    328321body.adding-widget .add-new-widget:hover {
    329     background: #EEE;
     322    background: #eee;
    330323    border-color: #999;
    331324    color: #333;
  • trunk/src/wp-admin/js/customize-widgets.js

    r27913 r27985  
    1 /*global wp, Backbone, _, jQuery, WidgetCustomizer_exports */
    2 /*exported WidgetCustomizer */
    3 var WidgetCustomizer = ( function ($) {
    4     'use strict';
    5 
    6     var Widget,
    7         WidgetCollection,
    8         Sidebar,
    9         SidebarCollection,
    10         OldPreviewer,
    11         builtin_form_sync_handlers,
    12         customize = wp.customize, self = {
    13         nonce: null,
    14         i18n: {
    15             save_btn_label: '',
    16             save_btn_tooltip: '',
    17             remove_btn_label: '',
    18             remove_btn_tooltip: '',
    19             error: ''
    20         },
    21         available_widgets: [], // available widgets for instantiating
    22         registered_widgets: [], // all widgets registered
    23         active_sidebar_control: null,
    24         previewer: null,
    25         saved_widget_ids: {},
    26         registered_sidebars: [],
    27         tpl: {
    28             move_widget_area: '',
    29             widget_reorder_nav: ''
    30         }
    31     };
    32     $.extend( self, WidgetCustomizer_exports );
    33 
    34     // Lots of widgets expect this old ajaxurl global to be available
    35     if ( typeof window.ajaxurl === 'undefined' ) {
    36         window.ajaxurl = wp.ajax.settings.url;
    37     }
     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, OldPreviewer;
     9
     10    api.Widgets = api.Widgets || {};
     11
     12    // Link settings
     13    api.Widgets.data = _wpCustomizeWidgetsSettings || {};
     14    l10n = api.Widgets.data.l10n;
     15    delete api.Widgets.data.l10n;
    3816
    3917    /**
    4018     * Set up model
    4119     */
    42     Widget = self.Widget = Backbone.Model.extend( {
     20    api.Widgets.WidgetModel = Backbone.Model.extend({
    4321        id: null,
    4422        temp_id: null,
     
    5533        width: null,
    5634        height: null
    57     } );
    58 
    59     WidgetCollection = self.WidgetCollection = Backbone.Collection.extend( {
    60         model: Widget,
     35    });
     36
     37    api.Widgets.WidgetCollection = Backbone.Collection.extend({
     38        model: api.Widgets.WidgetModel,
    6139
    6240        // Controls searching on the current widget collection
     
    8159            // Useful for resetting the views when you clean the input
    8260            if ( this.terms === '' ) {
    83                 this.reset( WidgetCustomizer_exports.available_widgets );
     61                this.reset( api.Widgets.data.availableWidgets );
    8462            }
    8563
     
    9472
    9573            // Start with a full collection
    96             this.reset( WidgetCustomizer_exports.available_widgets, { silent: true } );
     74            this.reset( api.Widgets.data.availableWidgets, { silent: true } );
    9775
    9876            // Escape the term string for RegExp meta characters
     
    11290            this.reset( results );
    11391        }
    114     } );
    115     self.available_widgets = new WidgetCollection( self.available_widgets );
    116 
    117     Sidebar = self.Sidebar = Backbone.Model.extend( {
     92    });
     93    api.Widgets.availableWidgets = new api.Widgets.WidgetCollection( api.Widgets.data.availableWidgets );
     94
     95    api.Widgets.SidebarModel = Backbone.Model.extend({
    11896        after_title: null,
    11997        after_widget: null,
     
    125103        name: null,
    126104        is_rendered: false
    127     } );
    128 
    129     SidebarCollection = self.SidebarCollection = Backbone.Collection.extend( {
    130         model: Sidebar
    131     } );
    132     self.registered_sidebars = new SidebarCollection( self.registered_sidebars );
     105    });
     106
     107    api.Widgets.SidebarCollection = Backbone.Collection.extend({
     108        model: api.Widgets.SidebarModel
     109    });
     110    api.Widgets.registeredSidebars = new api.Widgets.SidebarCollection( api.Widgets.data.registeredSidebars );
    133111
    134112    /**
     
    137115     * listeners for the widget-synced event.
    138116     */
    139     builtin_form_sync_handlers = {
     117    api.Widgets.formSyncHandlers = {
    140118
    141119        /**
    142120         * @param {jQuery.Event} e
    143          * @param {jQuery} widget_el
    144          * @param {String} new_form
    145          */
    146         rss: function ( e, widget_el, new_form ) {
    147             var old_widget_error = widget_el.find( '.widget-error:first' ),
    148                 new_widget_error = $( '<div>' + new_form + '</div>' ).find( '.widget-error:first' );
    149 
    150             if ( old_widget_error.length && new_widget_error.length ) {
    151                 old_widget_error.replaceWith( new_widget_error );
    152             } else if ( old_widget_error.length ) {
    153                 old_widget_error.remove();
    154             } else if ( new_widget_error.length ) {
    155                 widget_el.find( '.widget-content:first' ).prepend( new_widget_error );
     121         * @param {jQuery} widget
     122         * @param {String} newForm
     123         */
     124        rss: function ( e, widget, newForm ) {
     125            var oldWidgetError = widget.find( '.widget-error:first' ),
     126                newWidgetError = $( '<div>' + newForm + '</div>' ).find( '.widget-error:first' );
     127
     128            if ( oldWidgetError.length && newWidgetError.length ) {
     129                oldWidgetError.replaceWith( newWidgetError );
     130            } else if ( oldWidgetError.length ) {
     131                oldWidgetError.remove();
     132            } else if ( newWidgetError.length ) {
     133                widget.find( '.widget-content:first' ).prepend( newWidgetError );
    156134            }
    157135        }
     
    159137
    160138    /**
    161      * On DOM ready, initialize some meta functionality independent of specific
    162      * customizer controls.
     139     * Widget Form control
     140     * Note that 'widget_form' must match the WP_Widget_Form_Customize_Control::$type
    163141     */
    164     self.init = function () {
    165         this.availableWidgetsPanel.setup();
    166 
    167         // Highlight widget control
    168         this.previewer.bind( 'highlight-widget-control', self.highlightWidgetFormControl );
    169 
    170         // Open and focus widget control
    171         this.previewer.bind( 'focus-widget-control', self.focusWidgetFormControl );
    172     };
    173     wp.customize.bind( 'ready', function () {
    174         self.init();
     142    api.Widgets.WidgetControl = api.Control.extend({
     143        /**
     144         * Set up the control
     145         */
     146        ready: function() {
     147            var control = this;
     148            control._setupModel();
     149            control._setupWideWidget();
     150            control._setupControlToggle();
     151            control._setupWidgetTitle();
     152            control._setupReorderUI();
     153            control._setupHighlightEffects();
     154            control._setupUpdateUI();
     155            control._setupRemoveUI();
     156        },
     157
     158        /**
     159         * Handle changes to the setting
     160         */
     161        _setupModel: function() {
     162            var control = this, remember_saved_widget_id;
     163
     164            api.Widgets.savedWidgetIds = api.Widgets.savedWidgetIds || [];
     165
     166            // Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
     167            remember_saved_widget_id = function() {
     168                api.Widgets.savedWidgetIds[control.params.widget_id] = true;
     169            };
     170            api.bind( 'ready', remember_saved_widget_id );
     171            api.bind( 'saved', remember_saved_widget_id );
     172
     173            control._update_count = 0;
     174            control.is_widget_updating = false;
     175            control.live_update_mode = true;
     176
     177            // Update widget whenever model changes
     178            control.setting.bind( function( to, from ) {
     179                if ( ! _( from ).isEqual( to ) && ! control.is_widget_updating ) {
     180                    control.updateWidget( { instance: to } );
     181                }
     182            } );
     183        },
     184
     185        /**
     186         * Add special behaviors for wide widget controls
     187         */
     188        _setupWideWidget: function() {
     189            var control = this,
     190                widget_inside,
     191                widget_form,
     192                customize_sidebar,
     193                position_widget,
     194                theme_controls_container;
     195
     196            if ( ! control.params.is_wide ) {
     197                return;
     198            }
     199
     200            widget_inside = control.container.find( '.widget-inside' );
     201            widget_form = widget_inside.find( '> .form' );
     202            customize_sidebar = $( '.wp-full-overlay-sidebar-content:first' );
     203            control.container.addClass( 'wide-widget-control' );
     204
     205            control.container.find( '.widget-content:first' ).css( {
     206                'max-width': control.params.width,
     207                'min-height': control.params.height
     208            } );
     209
     210            /**
     211             * Keep the widget-inside positioned so the top of fixed-positioned
     212             * element is at the same top position as the widget-top. When the
     213             * widget-top is scrolled out of view, keep the widget-top in view;
     214             * likewise, don't allow the widget to drop off the bottom of the window.
     215             * If a widget is too tall to fit in the window, don't let the height
     216             * exceed the window height so that the contents of the widget control
     217             * will become scrollable (overflow:auto).
     218             */
     219            position_widget = function() {
     220                var offset_top = control.container.offset().top,
     221                    window_height = $( window ).height(),
     222                    form_height = widget_form.outerHeight(),
     223                    top;
     224                widget_inside.css( 'max-height', window_height );
     225                top = Math.max(
     226                    0, // prevent top from going off screen
     227                    Math.min(
     228                        Math.max( offset_top, 0 ), // distance widget in panel is from top of screen
     229                        window_height - form_height // flush up against bottom of screen
     230                    )
     231                );
     232                widget_inside.css( 'top', top );
     233            };
     234
     235            theme_controls_container = $( '#customize-theme-controls' );
     236            control.container.on( 'expand', function() {
     237                position_widget();
     238                customize_sidebar.on( 'scroll', position_widget );
     239                $( window ).on( 'resize', position_widget );
     240                theme_controls_container.on( 'expanded collapsed', position_widget );
     241            } );
     242            control.container.on( 'collapsed', function() {
     243                customize_sidebar.off( 'scroll', position_widget );
     244                $( window ).off( 'resize', position_widget );
     245                theme_controls_container.off( 'expanded collapsed', position_widget );
     246            } );
     247
     248            // Reposition whenever a sidebar's widgets are changed
     249            api.each( function ( setting ) {
     250                if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
     251                    setting.bind( function() {
     252                        if ( control.container.hasClass( 'expanded' ) ) {
     253                            position_widget();
     254                        }
     255                    } );
     256                }
     257            } );
     258        },
     259
     260        /**
     261         * Show/hide the control when clicking on the form title, when clicking
     262         * the close button
     263         */
     264        _setupControlToggle: function() {
     265            var control = this, close_btn;
     266
     267            control.container.find( '.widget-top' ).on( 'click', function ( e ) {
     268                e.preventDefault();
     269                var sidebar_widgets_control = control.getSidebarWidgetsControl();
     270                if ( sidebar_widgets_control.is_reordering ) {
     271                    return;
     272                }
     273                control.toggleForm();
     274            } );
     275
     276            close_btn = control.container.find( '.widget-control-close' );
     277            // @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
     278            close_btn.on( 'click', function ( e ) {
     279                e.preventDefault();
     280                control.collapseForm();
     281                control.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
     282            } );
     283        },
     284
     285        /**
     286         * Update the title of the form if a title field is entered
     287         */
     288        _setupWidgetTitle: function() {
     289            var control = this, update_title;
     290
     291            update_title = function() {
     292                var title = control.setting().title,
     293                    in_widget_title = control.container.find( '.in-widget-title' );
     294
     295                if ( title ) {
     296                    in_widget_title.text( ': ' + title );
     297                } else {
     298                    in_widget_title.text( '' );
     299                }
     300            };
     301            control.setting.bind( update_title );
     302            update_title();
     303        },
     304
     305        /**
     306         * Set up the widget-reorder-nav
     307         */
     308        _setupReorderUI: function() {
     309            var control = this,
     310                select_sidebar_item,
     311                move_widget_area,
     312                reorder_nav,
     313                update_available_sidebars;
     314
     315            /**
     316             * select the provided sidebar list item in the move widget area
     317             *
     318             * @param {jQuery} li
     319             */
     320            select_sidebar_item = function ( li ) {
     321                li.siblings( '.selected' ).removeClass( 'selected' );
     322                li.addClass( 'selected' );
     323                var is_self_sidebar = ( li.data( 'id' ) === control.params.sidebar_id );
     324                control.container.find( '.move-widget-btn' ).prop( 'disabled', is_self_sidebar );
     325            };
     326
     327            /**
     328             * Add the widget reordering elements to the widget control
     329             */
     330            control.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) );
     331            move_widget_area = $(
     332                _.template( api.Widgets.data.tpl.moveWidgetArea, {
     333                    sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' )
     334                } )
     335            );
     336            control.container.find( '.widget-top' ).after( move_widget_area );
     337
     338            /**
     339             * Update available sidebars when their rendered state changes
     340             */
     341            update_available_sidebars = function() {
     342                var sidebar_items = move_widget_area.find( 'li' ), self_sidebar_item;
     343                self_sidebar_item = sidebar_items.filter( function(){
     344                    return $( this ).data( 'id' ) === control.params.sidebar_id;
     345                } );
     346                sidebar_items.each( function() {
     347                    var li = $( this ),
     348                        sidebar_id,
     349                        sidebar_model;
     350
     351                    sidebar_id = li.data( 'id' );
     352                    sidebar_model = api.Widgets.registeredSidebars.get( sidebar_id );
     353                    li.toggle( sidebar_model.get( 'is_rendered' ) );
     354                    if ( li.hasClass( 'selected' ) && ! sidebar_model.get( 'is_rendered' ) ) {
     355                        select_sidebar_item( self_sidebar_item );
     356                    }
     357                } );
     358            };
     359            update_available_sidebars();
     360            api.Widgets.registeredSidebars.on( 'change:is_rendered', update_available_sidebars );
     361
     362            /**
     363             * Handle clicks for up/down/move on the reorder nav
     364             */
     365            reorder_nav = control.container.find( '.widget-reorder-nav' );
     366            reorder_nav.find( '.move-widget, .move-widget-down, .move-widget-up' ).on( 'click keypress', function ( event ) {
     367                if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
     368                    return;
     369                }
     370                $( this ).focus();
     371
     372                if ( $( this ).is( '.move-widget' ) ) {
     373                    control.toggleWidgetMoveArea();
     374                } else {
     375                    var is_move_down = $( this ).is( '.move-widget-down' ),
     376                        is_move_up = $( this ).is( '.move-widget-up' ),
     377                        i = control.getWidgetSidebarPosition();
     378
     379                    if ( ( is_move_up && i === 0 ) || ( is_move_down && i === control.getSidebarWidgetsControl().setting().length - 1 ) ) {
     380                        return;
     381                    }
     382
     383                    if ( is_move_up ) {
     384                        control.moveUp();
     385                    } else {
     386                        control.moveDown();
     387                    }
     388
     389                    $( this ).focus(); // re-focus after the container was moved
     390                }
     391            } );
     392
     393            /**
     394             * Handle selecting a sidebar to move to
     395             */
     396            control.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function ( e ) {
     397                if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
     398                    return;
     399                }
     400                e.preventDefault();
     401                select_sidebar_item( $( this ) );
     402            } );
     403
     404            /**
     405             * Move widget to another sidebar
     406             */
     407            control.container.find( '.move-widget-btn' ).click( function() {
     408                control.getSidebarWidgetsControl().toggleReordering( false );
     409
     410                var old_sidebar_id = control.params.sidebar_id,
     411                    new_sidebar_id = control.container.find( '.widget-area-select li.selected' ).data( 'id' ),
     412                    old_sidebar_widgets_setting,
     413                    new_sidebar_widgets_setting,
     414                    old_sidebar_widget_ids,
     415                    new_sidebar_widget_ids,
     416                    i;
     417
     418                old_sidebar_widgets_setting = api( 'sidebars_widgets[' + old_sidebar_id + ']' );
     419                new_sidebar_widgets_setting = api( 'sidebars_widgets[' + new_sidebar_id + ']' );
     420                old_sidebar_widget_ids = Array.prototype.slice.call( old_sidebar_widgets_setting() );
     421                new_sidebar_widget_ids = Array.prototype.slice.call( new_sidebar_widgets_setting() );
     422
     423                i = control.getWidgetSidebarPosition();
     424                old_sidebar_widget_ids.splice( i, 1 );
     425                new_sidebar_widget_ids.push( control.params.widget_id );
     426
     427                old_sidebar_widgets_setting( old_sidebar_widget_ids );
     428                new_sidebar_widgets_setting( new_sidebar_widget_ids );
     429
     430                control.focus();
     431            } );
     432        },
     433
     434        /**
     435         * Highlight widgets in preview when interacted with in the customizer
     436         */
     437        _setupHighlightEffects: function() {
     438            var control = this;
     439
     440            // Highlight whenever hovering or clicking over the form
     441            control.container.on( 'mouseenter click', function() {
     442                control.setting.previewer.send( 'highlight-widget', control.params.widget_id );
     443            } );
     444
     445            // Highlight when the setting is updated
     446            control.setting.bind( function() {
     447                control.setting.previewer.send( 'highlight-widget', control.params.widget_id );
     448            } );
     449
     450            // Highlight when the widget form is expanded
     451            control.container.on( 'expand', function() {
     452                control.scrollPreviewWidgetIntoView();
     453            } );
     454        },
     455
     456        /**
     457         * Set up event handlers for widget updating
     458         */
     459        _setupUpdateUI: function() {
     460            var control = this,
     461                widget_root,
     462                widget_content,
     463                save_btn,
     464                update_widget_debounced,
     465                form_update_event_handler;
     466
     467            widget_root = control.container.find( '.widget:first' );
     468            widget_content = widget_root.find( '.widget-content:first' );
     469
     470            // Configure update button
     471            save_btn = control.container.find( '.widget-control-save' );
     472            save_btn.val( l10n.saveBtnLabel );
     473            save_btn.attr( 'title', l10n.saveBtnTooltip );
     474            save_btn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
     475            save_btn.on( 'click', function ( e ) {
     476                e.preventDefault();
     477                control.updateWidget( { disable_form: true } );
     478            } );
     479
     480            update_widget_debounced = _.debounce( function() {
     481                // @todo For compatibility with other plugins, should we trigger a click event? What about form submit event?
     482                control.updateWidget();
     483            }, 250 );
     484
     485            // Trigger widget form update when hitting Enter within an input
     486            control.container.find( '.widget-content' ).on( 'keydown', 'input', function( e ) {
     487                if ( 13 === e.which ) { // Enter
     488                    e.preventDefault();
     489                    control.updateWidget( { ignore_active_element: true } );
     490                }
     491            } );
     492
     493            // Handle widgets that support live previews
     494            widget_content.on( 'change input propertychange', ':input', function ( e ) {
     495                if ( control.live_update_mode ) {
     496                    if ( e.type === 'change' ) {
     497                        control.updateWidget();
     498                    } else if ( this.checkValidity && this.checkValidity() ) {
     499                        update_widget_debounced();
     500                    }
     501                }
     502            } );
     503
     504            // Remove loading indicators when the setting is saved and the preview updates
     505            control.setting.previewer.channel.bind( 'synced', function() {
     506                control.container.removeClass( 'previewer-loading' );
     507            } );
     508            api.Widgets.Previewer.bind( 'widget-updated', function ( updated_widget_id ) {
     509                if ( updated_widget_id === control.params.widget_id ) {
     510                    control.container.removeClass( 'previewer-loading' );
     511                }
     512            } );
     513
     514            // Update widget control to indicate whether it is currently rendered (cf. Widget Visibility)
     515            api.Widgets.Previewer.bind( 'rendered-widgets', function ( rendered_widgets ) {
     516                var is_rendered = !! rendered_widgets[control.params.widget_id];
     517                control.container.toggleClass( 'widget-rendered', is_rendered );
     518            } );
     519
     520            form_update_event_handler = api.Widgets.formSyncHandlers[ control.params.widget_id_base ];
     521            if ( form_update_event_handler ) {
     522                $( document ).on( 'widget-synced', function ( e, widget_el ) {
     523                    if ( widget_root.is( widget_el ) ) {
     524                        form_update_event_handler.apply( document, arguments );
     525                    }
     526                } );
     527            }
     528        },
     529
     530        /**
     531         * Set up event handlers for widget removal
     532         */
     533        _setupRemoveUI: function() {
     534            var control = this,
     535                remove_btn,
     536                replace_delete_with_remove;
     537
     538            // Configure remove button
     539            remove_btn = control.container.find( 'a.widget-control-remove' );
     540            // @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
     541            remove_btn.on( 'click', function ( e ) {
     542                e.preventDefault();
     543
     544                // Find an adjacent element to add focus to when this widget goes away
     545                var adjacent_focus_target;
     546                if ( control.container.next().is( '.customize-control-widget_form' ) ) {
     547                    adjacent_focus_target = control.container.next().find( '.widget-action:first' );
     548                } else if ( control.container.prev().is( '.customize-control-widget_form' ) ) {
     549                    adjacent_focus_target = control.container.prev().find( '.widget-action:first' );
     550                } else {
     551                    adjacent_focus_target = control.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
     552                }
     553
     554                control.container.slideUp( function() {
     555                    var sidebars_widgets_control = api.Widgets.getSidebarWidgetControlContainingWidget( control.params.widget_id ),
     556                        sidebar_widget_ids,
     557                        i;
     558
     559                    if ( ! sidebars_widgets_control ) {
     560                        throw new Error( 'Unable to find sidebars_widgets_control' );
     561                    }
     562                    sidebar_widget_ids = sidebars_widgets_control.setting().slice();
     563                    i = _.indexOf( sidebar_widget_ids, control.params.widget_id );
     564                    if ( -1 === i ) {
     565                        throw new Error( 'Widget is not in sidebar' );
     566                    }
     567                    sidebar_widget_ids.splice( i, 1 );
     568                    sidebars_widgets_control.setting( sidebar_widget_ids );
     569                    adjacent_focus_target.focus(); // keyboard accessibility
     570                } );
     571            } );
     572
     573            replace_delete_with_remove = function() {
     574                remove_btn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Delete"
     575                remove_btn.attr( 'title', l10n.removeBtnTooltip );
     576            };
     577            if ( control.params.is_new ) {
     578                api.bind( 'saved', replace_delete_with_remove );
     579            } else {
     580                replace_delete_with_remove();
     581            }
     582        },
     583
     584        /**
     585         * Find all inputs in a widget container that should be considered when
     586         * comparing the loaded form with the sanitized form, whose fields will
     587         * be aligned to copy the sanitized over. The elements returned by this
     588         * are passed into this._getInputsSignature(), and they are iterated
     589         * over when copying sanitized values over to the the form loaded.
     590         *
     591         * @param {jQuery} container element in which to look for inputs
     592         * @returns {jQuery} inputs
     593         * @private
     594         */
     595        _getInputs: function ( container ) {
     596            return $( container ).find( ':input[name]' );
     597        },
     598
     599        /**
     600         * Iterate over supplied inputs and create a signature string for all of them together.
     601         * This string can be used to compare whether or not the form has all of the same fields.
     602         *
     603         * @param {jQuery} inputs
     604         * @returns {string}
     605         * @private
     606         */
     607        _getInputsSignature: function ( inputs ) {
     608            var inputs_signatures = _( inputs ).map( function ( input ) {
     609                input = $( input );
     610                var signature_parts;
     611                if ( input.is( ':checkbox, :radio' ) ) {
     612                    signature_parts = [ input.attr( 'id' ), input.attr( 'name' ), input.prop( 'value' ) ];
     613                } else {
     614                    signature_parts = [ input.attr( 'id' ), input.attr( 'name' ) ];
     615                }
     616                return signature_parts.join( ',' );
     617            } );
     618            return inputs_signatures.join( ';' );
     619        },
     620
     621        /**
     622         * Get the property that represents the state of an input.
     623         *
     624         * @param {jQuery|DOMElement} input
     625         * @returns {string}
     626         * @private
     627         */
     628        _getInputStatePropertyName: function ( input ) {
     629            input = $( input );
     630            if ( input.is( ':radio, :checkbox' ) ) {
     631                return 'checked';
     632            } else {
     633                return 'value';
     634            }
     635        },
     636
     637        /***********************************************************************
     638         * Begin public API methods
     639         **********************************************************************/
     640
     641        /**
     642         * @return {wp.customize.controlConstructor.sidebar_widgets[]}
     643         */
     644        getSidebarWidgetsControl: function() {
     645            var control = this, setting_id, sidebar_widgets_control;
     646
     647            setting_id = 'sidebars_widgets[' + control.params.sidebar_id + ']';
     648            sidebar_widgets_control = api.control( setting_id );
     649            if ( ! sidebar_widgets_control ) {
     650                throw new Error( 'Unable to locate sidebar_widgets control for ' + control.params.sidebar_id );
     651            }
     652            return sidebar_widgets_control;
     653        },
     654
     655        /**
     656         * Submit the widget form via Ajax and get back the updated instance,
     657         * along with the new widget control form to render.
     658         *
     659         * @param {object} [args]
     660         * @param {Object|null} [args.instance=null]  When the model changes, the instance is sent here; otherwise, the inputs from the form are used
     661         * @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.
     662         * @param {Boolean} [args.ignore_active_element=false] Whether or not updating a field will be deferred if focus is still on the element.
     663         */
     664        updateWidget: function ( args ) {
     665            var control = this,
     666                instance_override,
     667                complete_callback,
     668                widget_root,
     669                update_number,
     670                widget_content,
     671                params,
     672                data,
     673                inputs,
     674                processing,
     675                jqxhr,
     676                is_changed;
     677
     678            args = $.extend( {
     679                instance: null,
     680                complete: null,
     681                ignore_active_element: false
     682            }, args );
     683
     684            instance_override = args.instance;
     685            complete_callback = args.complete;
     686
     687            control._update_count += 1;
     688            update_number = control._update_count;
     689
     690            widget_root = control.container.find( '.widget:first' );
     691            widget_content = widget_root.find( '.widget-content:first' );
     692
     693            // Remove a previous error message
     694            widget_content.find( '.widget-error' ).remove();
     695
     696            control.container.addClass( 'widget-form-loading' );
     697            control.container.addClass( 'previewer-loading' );
     698            processing = api.state( 'processing' );
     699            processing( processing() + 1 );
     700
     701            if ( ! control.live_update_mode ) {
     702                control.container.addClass( 'widget-form-disabled' );
     703            }
     704
     705            params = {};
     706            params.action = 'update-widget';
     707            params.wp_customize = 'on';
     708            params.nonce = api.Widgets.data.nonce;
     709
     710            data = $.param( params );
     711            inputs = control._getInputs( widget_content );
     712
     713            // Store the value we're submitting in data so that when the response comes back,
     714            // we know if it got sanitized; if there is no difference in the sanitized value,
     715            // then we do not need to touch the UI and mess up the user's ongoing editing.
     716            inputs.each( function() {
     717                var input = $( this ),
     718                    property = control._getInputStatePropertyName( this );
     719                input.data( 'state' + update_number, input.prop( property ) );
     720            } );
     721
     722            if ( instance_override ) {
     723                data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instance_override ) } );
     724            } else {
     725                data += '&' + inputs.serialize();
     726            }
     727            data += '&' + widget_content.find( '~ :input' ).serialize();
     728
     729            jqxhr = $.post( wp.ajax.settings.url, data, function ( r ) {
     730                var message,
     731                    sanitized_form,
     732                    sanitized_inputs,
     733                    has_same_inputs_in_response,
     734                    is_live_update_aborted = false;
     735
     736                // Check if the user is logged out.
     737                if ( '0' === r ) {
     738                    api.Widgets.Previewer.preview.iframe.hide();
     739                    api.Widgets.Previewer.login().done( function() {
     740                        control.updateWidget( args );
     741                        api.Widgets.Previewer.preview.iframe.show();
     742                    } );
     743                    return;
     744                }
     745
     746                // Check for cheaters.
     747                if ( '-1' === r ) {
     748                    api.Widgets.Previewer.cheatin();
     749                    return;
     750                }
     751
     752                if ( r.success ) {
     753                    sanitized_form = $( '<div>' + r.data.form + '</div>' );
     754                    sanitized_inputs = control._getInputs( sanitized_form );
     755                    has_same_inputs_in_response = control._getInputsSignature( inputs ) === control._getInputsSignature( sanitized_inputs );
     756
     757                    // Restore live update mode if sanitized fields are now aligned with the existing fields
     758                    if ( has_same_inputs_in_response && ! control.live_update_mode ) {
     759                        control.live_update_mode = true;
     760                        control.container.removeClass( 'widget-form-disabled' );
     761                        control.container.find( 'input[name="savewidget"]' ).hide();
     762                    }
     763
     764                    // Sync sanitized field states to existing fields if they are aligned
     765                    if ( has_same_inputs_in_response && control.live_update_mode ) {
     766                        inputs.each( function ( i ) {
     767                            var input = $( this ),
     768                                sanitized_input = $( sanitized_inputs[i] ),
     769                                property = control._getInputStatePropertyName( this ),
     770                                submitted_state,
     771                                sanitized_state,
     772                                can_update_state;
     773
     774                            submitted_state = input.data( 'state' + update_number );
     775                            sanitized_state = sanitized_input.prop( property );
     776                            input.data( 'sanitized', sanitized_state );
     777
     778                            can_update_state = (
     779                                submitted_state !== sanitized_state &&
     780                                ( args.ignore_active_element || ! input.is( document.activeElement ) )
     781                            );
     782                            if ( can_update_state ) {
     783                                input.prop( property, sanitized_state );
     784                            }
     785                        } );
     786                        $( document ).trigger( 'widget-synced', [ widget_root, r.data.form ] );
     787
     788                    // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled
     789                    } else if ( control.live_update_mode ) {
     790                        control.live_update_mode = false;
     791                        control.container.find( 'input[name="savewidget"]' ).show();
     792                        is_live_update_aborted = true;
     793                    // Otherwise, replace existing form with the sanitized form
     794                    } else {
     795                        widget_content.html( r.data.form );
     796                        control.container.removeClass( 'widget-form-disabled' );
     797                        $( document ).trigger( 'widget-updated', [ widget_root ] );
     798                    }
     799
     800                    /**
     801                     * If the old instance is identical to the new one, there is nothing new
     802                     * needing to be rendered, and so we can preempt the event for the
     803                     * preview finishing loading.
     804                     */
     805                    is_changed = ! is_live_update_aborted && ! _( control.setting() ).isEqual( r.data.instance );
     806                    if ( is_changed ) {
     807                        control.is_widget_updating = true; // suppress triggering another updateWidget
     808                        control.setting( r.data.instance );
     809                        control.is_widget_updating = false;
     810                    } else {
     811                        // no change was made, so stop the spinner now instead of when the preview would updates
     812                        control.container.removeClass( 'previewer-loading' );
     813                    }
     814
     815                    if ( complete_callback ) {
     816                        complete_callback.call( control, null, { no_change: ! is_changed, ajax_finished: true } );
     817                    }
     818                } else {
     819                    message = l10n.error;
     820                    if ( r.data && r.data.message ) {
     821                        message = r.data.message;
     822                    }
     823                    if ( complete_callback ) {
     824                        complete_callback.call( control, message );
     825                    } else {
     826                        widget_content.prepend( '<p class="widget-error"><strong>' + message + '</strong></p>' );
     827                    }
     828                }
     829            } );
     830            jqxhr.fail( function ( jqXHR, textStatus ) {
     831                if ( complete_callback ) {
     832                    complete_callback.call( control, textStatus );
     833                }
     834            } );
     835            jqxhr.always( function() {
     836                control.container.removeClass( 'widget-form-loading' );
     837                inputs.each( function() {
     838                    $( this ).removeData( 'state' + update_number );
     839                } );
     840
     841                processing( processing() - 1 );
     842            } );
     843        },
     844
     845        /**
     846         * Expand the accordion section containing a control
     847         * @todo it would be nice if accordion had a proper API instead of having to trigger UI events on its elements
     848         */
     849        expandControlSection: function() {
     850            var section = this.container.closest( '.accordion-section' );
     851            if ( ! section.hasClass( 'open' ) ) {
     852                section.find( '.accordion-section-title:first' ).trigger( 'click' );
     853            }
     854        },
     855
     856        /**
     857         * Expand the widget form control
     858         */
     859        expandForm: function() {
     860            this.toggleForm( true );
     861        },
     862
     863        /**
     864         * Collapse the widget form control
     865         */
     866        collapseForm: function() {
     867            this.toggleForm( false );
     868        },
     869
     870        /**
     871         * Expand or collapse the widget control
     872         *
     873         * @param {boolean|undefined} [do_expand] If not supplied, will be inverse of current visibility
     874         */
     875        toggleForm: function ( do_expand ) {
     876            var control = this, widget, inside, complete;
     877
     878            widget = control.container.find( 'div.widget:first' );
     879            inside = widget.find( '.widget-inside:first' );
     880            if ( typeof do_expand === 'undefined' ) {
     881                do_expand = ! inside.is( ':visible' );
     882            }
     883
     884            // Already expanded or collapsed, so noop
     885            if ( inside.is( ':visible' ) === do_expand ) {
     886                return;
     887            }
     888
     889            if ( do_expand ) {
     890                // Close all other widget controls before expanding this one
     891                api.control.each( function ( other_control ) {
     892                    if ( control.params.type === other_control.params.type && control !== other_control ) {
     893                        other_control.collapseForm();
     894                    }
     895                } );
     896
     897                complete = function() {
     898                    control.container.removeClass( 'expanding' );
     899                    control.container.addClass( 'expanded' );
     900                    control.container.trigger( 'expanded' );
     901                };
     902                if ( control.params.is_wide ) {
     903                    inside.fadeIn( 'fast', complete );
     904                } else {
     905                    inside.slideDown( 'fast', complete );
     906                }
     907                control.container.trigger( 'expand' );
     908                control.container.addClass( 'expanding' );
     909            } else {
     910                control.container.trigger( 'collapse' );
     911                control.container.addClass( 'collapsing' );
     912                complete = function() {
     913                    control.container.removeClass( 'collapsing' );
     914                    control.container.removeClass( 'expanded' );
     915                    control.container.trigger( 'collapsed' );
     916                };
     917                if ( control.params.is_wide ) {
     918                    inside.fadeOut( 'fast', complete );
     919                } else {
     920                    inside.slideUp( 'fast', function() {
     921                        widget.css( { width:'', margin:'' } );
     922                        complete();
     923                    } );
     924                }
     925            }
     926        },
     927
     928        /**
     929         * Expand the containing sidebar section, expand the form, and focus on
     930         * the first input in the control
     931         */
     932        focus: function() {
     933            var control = this;
     934            control.expandControlSection();
     935            control.expandForm();
     936            control.container.find( '.widget-content :focusable:first' ).focus();
     937        },
     938
     939        /**
     940         * Get the position (index) of the widget in the containing sidebar
     941         *
     942         * @throws Error
     943         * @returns {Number}
     944         */
     945        getWidgetSidebarPosition: function() {
     946            var control = this,
     947                sidebar_widget_ids,
     948                position;
     949
     950            sidebar_widget_ids = control.getSidebarWidgetsControl().setting();
     951            position = _.indexOf( sidebar_widget_ids, control.params.widget_id );
     952            if ( position === -1 ) {
     953                throw new Error( 'Widget was unexpectedly not present in the sidebar.' );
     954            }
     955            return position;
     956        },
     957
     958        /**
     959         * Move widget up one in the sidebar
     960         */
     961        moveUp: function() {
     962            this._moveWidgetByOne( -1 );
     963        },
     964
     965        /**
     966         * Move widget up one in the sidebar
     967         */
     968        moveDown: function() {
     969            this._moveWidgetByOne( 1 );
     970        },
     971
     972        /**
     973         * @private
     974         *
     975         * @param {Number} offset 1|-1
     976         */
     977        _moveWidgetByOne: function ( offset ) {
     978            var control = this,
     979                i,
     980                sidebar_widgets_setting,
     981                sidebar_widget_ids,
     982                adjacent_widget_id;
     983
     984            i = control.getWidgetSidebarPosition();
     985
     986            sidebar_widgets_setting = control.getSidebarWidgetsControl().setting;
     987            sidebar_widget_ids = Array.prototype.slice.call( sidebar_widgets_setting() ); // clone
     988            adjacent_widget_id = sidebar_widget_ids[i + offset];
     989            sidebar_widget_ids[i + offset] = control.params.widget_id;
     990            sidebar_widget_ids[i] = adjacent_widget_id;
     991
     992            sidebar_widgets_setting( sidebar_widget_ids );
     993        },
     994
     995        /**
     996         * Toggle visibility of the widget move area
     997         *
     998         * @param {Boolean} [toggle]
     999         */
     1000        toggleWidgetMoveArea: function ( toggle ) {
     1001            var control = this, move_widget_area;
     1002            move_widget_area = control.container.find( '.move-widget-area' );
     1003            if ( typeof toggle === 'undefined' ) {
     1004                toggle = ! move_widget_area.hasClass( 'active' );
     1005            }
     1006            if ( toggle ) {
     1007                // reset the selected sidebar
     1008                move_widget_area.find( '.selected' ).removeClass( 'selected' );
     1009                move_widget_area.find( 'li' ).filter( function() {
     1010                    return $( this ).data( 'id' ) === control.params.sidebar_id;
     1011                } ).addClass( 'selected' );
     1012                control.container.find( '.move-widget-btn' ).prop( 'disabled', true );
     1013            }
     1014            move_widget_area.toggleClass( 'active', toggle );
     1015        },
     1016
     1017        /**
     1018         * Inside of the customizer preview, scroll the widget into view
     1019         */
     1020        scrollPreviewWidgetIntoView: function() {
     1021            // @todo scrollIntoView() provides a robust but very poor experience. Animation is needed. See https://github.com/x-team/wp-widget-customizer/issues/16
     1022        },
     1023
     1024        /**
     1025         * Highlight the widget control and section
     1026         */
     1027        highlightSectionAndControl: function() {
     1028            var control = this, target_element;
     1029
     1030            if ( control.container.is( ':hidden' ) ) {
     1031                target_element = control.container.closest( '.control-section' );
     1032            } else {
     1033                target_element = control.container;
     1034            }
     1035
     1036            $( '.widget-customizer-highlighted' ).removeClass( 'widget-customizer-highlighted' );
     1037            target_element.addClass( 'widget-customizer-highlighted' );
     1038            setTimeout( function() {
     1039                target_element.removeClass( 'widget-customizer-highlighted' );
     1040            }, 500 );
     1041        }
     1042
    1751043    } );
    1761044
    1771045    /**
    1781046     * Sidebar Widgets control
    179      * Note that 'sidebar_widgets' must match the Sidebar_Widgets_WP_Customize_Control::$type
     1047     * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type
    1801048     */
    181     customize.controlConstructor.sidebar_widgets = customize.Control.extend( {
    182 
     1049    api.Widgets.SidebarControl = api.Control.extend({
    1831050        /**
    1841051         * Set up the control
     
    1991066        _setupModel: function() {
    2001067            var control = this,
    201                 registered_sidebar = self.registered_sidebars.get( control.params.sidebar_id );
     1068                registered_sidebar = api.Widgets.registeredSidebars.get( control.params.sidebar_id );
    2021069
    2031070            control.setting.bind( function( new_widget_ids, old_widget_ids ) {
     
    2101077                new_widget_ids = _( new_widget_ids ).filter( function ( new_widget_id ) {
    2111078                    var parsed_widget_id = parse_widget_id( new_widget_id );
    212                     return !! self.available_widgets.findWhere( { id_base: parsed_widget_id.id_base } );
     1079                    return !! api.Widgets.availableWidgets.findWhere( { id_base: parsed_widget_id.id_base } );
    2131080                } );
    2141081
    2151082                widget_form_controls = _( new_widget_ids ).map( function ( widget_id ) {
    216                     var widget_form_control = self.getWidgetFormControlForWidget( widget_id );
     1083                    var widget_form_control = api.Widgets.getWidgetFormControlForWidget( widget_id );
    2171084                    if ( ! widget_form_control ) {
    2181085                        widget_form_control = control.addWidget( widget_id );
     
    2511118
    2521119                    // Using setTimeout so that when moving a widget to another sidebar, the other sidebars_widgets settings get a chance to update
    253                     setTimeout( function () {
     1120                    setTimeout( function() {
    2541121                        var is_present_in_another_sidebar = false,
    2551122                            removed_control,
     
    2601127
    2611128                        // Check if the widget is in another sidebar
    262                         wp.customize.each( function ( other_setting ) {
     1129                        api.each( function ( other_setting ) {
    2631130                            if ( other_setting.id === control.setting.id || 0 !== other_setting.id.indexOf( 'sidebars_widgets[' ) || other_setting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
    2641131                                return;
     
    2771144                        }
    2781145
    279                         removed_control = self.getWidgetFormControlForWidget( removed_widget_id );
     1146                        removed_control = api.Widgets.getWidgetFormControlForWidget( removed_widget_id );
    2801147
    2811148                        // Detect if widget control was dragged to another sidebar
     
    2881155                        // Delete any widget form controls for removed widgets
    2891156                        if ( removed_control && ! was_dragged_to_another_sidebar ) {
    290                             wp.customize.control.remove( removed_control.id );
     1157                            api.control.remove( removed_control.id );
    2911158                            removed_control.container.remove();
    2921159                        }
     
    2941161                        // Move widget to inactive widgets sidebar (move it to trash) if has been previously saved
    2951162                        // This prevents the inactive widgets sidebar from overflowing with throwaway widgets
    296                         if ( self.saved_widget_ids[removed_widget_id] ) {
    297                             inactive_widgets = wp.customize.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
     1163                        if ( api.Widgets.savedWidgetIds[removed_widget_id] ) {
     1164                            inactive_widgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
    2981165                            inactive_widgets.push( removed_widget_id );
    299                             wp.customize.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactive_widgets ).unique() );
     1166                            api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactive_widgets ).unique() );
    3001167                        }
    3011168
    3021169                        // Make old single widget available for adding again
    3031170                        removed_id_base = parse_widget_id( removed_widget_id ).id_base;
    304                         widget = self.available_widgets.findWhere( { id_base: removed_id_base } );
     1171                        widget = api.Widgets.availableWidgets.findWhere( { id_base: removed_id_base } );
    3051172                        if ( widget && ! widget.get( 'is_multi' ) ) {
    3061173                            widget.set( 'is_disabled', false );
     
    3121179
    3131180            // Update the model with whether or not the sidebar is rendered
    314             self.previewer.bind( 'rendered-sidebars', function ( rendered_sidebars ) {
     1181            api.Widgets.Previewer.bind( 'rendered-sidebars', function ( rendered_sidebars ) {
    3151182                var is_rendered = !! rendered_sidebars[control.params.sidebar_id];
    3161183                registered_sidebar.set( 'is_rendered', is_rendered );
     
    3221189                section = $( section_selector );
    3231190                if ( this.get( 'is_rendered' ) ) {
    324                     section.stop().slideDown( function () {
     1191                    section.stop().slideDown( function() {
    3251192                        $( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow
    3261193                    } );
     
    3391206         * Allow widgets in sidebar to be re-ordered, and for the order to be previewed
    3401207         */
    341         _setupSortable: function () {
     1208        _setupSortable: function() {
    3421209            var control = this;
    3431210            control.is_reordering = false;
     
    3511218                axis: 'y',
    3521219                connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
    353                 update: function () {
     1220                update: function() {
    3541221                    var widget_container_ids = control.section_content.sortable( 'toArray' ), widget_ids;
    3551222                    widget_ids = $.map( widget_container_ids, function ( widget_container_id ) {
     
    3661233            control.control_section.find( '.accordion-section-title' ).droppable( {
    3671234                accept: '.customize-control-widget_form',
    368                 over: function () {
     1235                over: function() {
    3691236                    if ( ! control.control_section.hasClass( 'open' ) ) {
    3701237                        control.control_section.addClass( 'open' );
    371                         control.section_content.toggle( false ).slideToggle( 150, function () {
     1238                        control.section_content.toggle( false ).slideToggle( 150, function() {
    3721239                            control.section_content.sortable( 'refreshPositions' );
    3731240                        } );
     
    3911258         * Set up UI for adding a new widget
    3921259         */
    393         _setupAddition: function () {
     1260        _setupAddition: function() {
    3941261            var control = this;
    3951262
     
    4051272                // @todo Use an control.is_adding state
    4061273                if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
    407                     self.availableWidgetsPanel.open( control );
     1274                    api.Widgets.availableWidgetsPanel.open( control );
    4081275                } else {
    409                     self.availableWidgetsPanel.close();
     1276                    api.Widgets.availableWidgetsPanel.close();
    4101277                }
    4111278            } );
     
    4151282         * Add classes to the widget_form controls to assist with styling
    4161283         */
    417         _applyCardinalOrderClassNames: function () {
     1284        _applyCardinalOrderClassNames: function() {
    4181285            var control = this;
    4191286            control.section_content.find( '.customize-control-widget_form' )
     
    4601327         * @return {wp.customize.controlConstructor.widget_form[]}
    4611328         */
    462         getWidgetFormControls: function () {
     1329        getWidgetFormControls: function() {
    4631330            var control = this, form_controls;
    4641331
    4651332            form_controls = _( control.setting() ).map( function ( widget_id ) {
    4661333                var setting_id = widget_id_to_setting_id( widget_id ),
    467                     form_control = customize.control( setting_id );
     1334                    form_control = api.control( setting_id );
    4681335
    4691336                if ( ! form_control ) {
     
    4881355                widget_number = parsed_widget_id.number,
    4891356                widget_id_base = parsed_widget_id.id_base,
    490                 widget = self.available_widgets.findWhere( {id_base: widget_id_base} ),
     1357                widget = api.Widgets.availableWidgets.findWhere( {id_base: widget_id_base} ),
    4911358                setting_id,
    4921359                is_existing_widget,
     
    5401407
    5411408            // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
    542             is_existing_widget = wp.customize.has( setting_id );
     1409            is_existing_widget = api.has( setting_id );
    5431410            if ( ! is_existing_widget ) {
    5441411                setting_args = {
     
    5461413                    previewer: control.setting.previewer
    5471414                };
    548                 wp.customize.create( setting_id, setting_id, {}, setting_args );
    549             }
    550 
    551             Constructor = wp.customize.controlConstructor[customize_control_type];
     1415                api.create( setting_id, setting_id, {}, setting_args );
     1416            }
     1417
     1418            Constructor = api.controlConstructor[customize_control_type];
    5521419            widget_form_control = new Constructor( setting_id, {
    5531420                params: {
     
    5661433                previewer: control.setting.previewer
    5671434            } );
    568             wp.customize.control.add( setting_id, widget_form_control );
     1435            api.control.add( setting_id, widget_form_control );
    5691436
    5701437            // Make sure widget is removed from the other sidebars
    571             wp.customize.each( function ( other_setting ) {
     1438            api.each( function ( other_setting ) {
    5721439                if ( other_setting.id === control.setting.id ) {
    5731440                    return;
     
    5911458            }
    5921459
    593             customize_control.slideDown( function () {
     1460            customize_control.slideDown( function() {
    5941461                if ( is_existing_widget ) {
    5951462                    widget_form_control.expandForm();
     
    6151482    } );
    6161483
    617     /**
    618      * Widget Form control
    619      * Note that 'widget_form' must match the Widget_Form_WP_Customize_Control::$type
    620      */
    621     customize.controlConstructor.widget_form = customize.Control.extend( {
    622 
    623         /**
    624          * Set up the control
    625          */
    626         ready: function() {
    627             var control = this;
    628             control._setupModel();
    629             control._setupWideWidget();
    630             control._setupControlToggle();
    631             control._setupWidgetTitle();
    632             control._setupReorderUI();
    633             control._setupHighlightEffects();
    634             control._setupUpdateUI();
    635             control._setupRemoveUI();
    636         },
    637 
    638         /**
    639          * Handle changes to the setting
    640          */
    641         _setupModel: function () {
    642             var control = this, remember_saved_widget_id;
    643 
    644             // Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
    645             remember_saved_widget_id = function () {
    646                 self.saved_widget_ids[control.params.widget_id] = true;
    647             };
    648             wp.customize.bind( 'ready', remember_saved_widget_id );
    649             wp.customize.bind( 'saved', remember_saved_widget_id );
    650 
    651             control._update_count = 0;
    652             control.is_widget_updating = false;
    653             control.live_update_mode = true;
    654 
    655             // Update widget whenever model changes
    656             control.setting.bind( function( to, from ) {
    657                 if ( ! _( from ).isEqual( to ) && ! control.is_widget_updating ) {
    658                     control.updateWidget( { instance: to } );
    659                 }
    660             } );
    661         },
    662 
    663         /**
    664          * Add special behaviors for wide widget controls
    665          */
    666         _setupWideWidget: function () {
    667             var control = this,
    668                 widget_inside,
    669                 widget_form,
    670                 customize_sidebar,
    671                 position_widget,
    672                 theme_controls_container;
    673 
    674             if ( ! control.params.is_wide ) {
    675                 return;
    676             }
    677 
    678             widget_inside = control.container.find( '.widget-inside' );
    679             widget_form = widget_inside.find( '> .form' );
    680             customize_sidebar = $( '.wp-full-overlay-sidebar-content:first' );
    681             control.container.addClass( 'wide-widget-control' );
    682 
    683             control.container.find( '.widget-content:first' ).css( {
    684                 'max-width': control.params.width,
    685                 'min-height': control.params.height
    686             } );
    687 
    688             /**
    689              * Keep the widget-inside positioned so the top of fixed-positioned
    690              * element is at the same top position as the widget-top. When the
    691              * widget-top is scrolled out of view, keep the widget-top in view;
    692              * likewise, don't allow the widget to drop off the bottom of the window.
    693              * If a widget is too tall to fit in the window, don't let the height
    694              * exceed the window height so that the contents of the widget control
    695              * will become scrollable (overflow:auto).
    696              */
    697             position_widget = function () {
    698                 var offset_top = control.container.offset().top,
    699                     window_height = $( window ).height(),
    700                     form_height = widget_form.outerHeight(),
    701                     top;
    702                 widget_inside.css( 'max-height', window_height );
    703                 top = Math.max(
    704                     0, // prevent top from going off screen
    705                     Math.min(
    706                         Math.max( offset_top, 0 ), // distance widget in panel is from top of screen
    707                         window_height - form_height // flush up against bottom of screen
    708                     )
    709                 );
    710                 widget_inside.css( 'top', top );
    711             };
    712 
    713             theme_controls_container = $( '#customize-theme-controls' );
    714             control.container.on( 'expand', function () {
    715                 position_widget();
    716                 customize_sidebar.on( 'scroll', position_widget );
    717                 $( window ).on( 'resize', position_widget );
    718                 theme_controls_container.on( 'expanded collapsed', position_widget );
    719             } );
    720             control.container.on( 'collapsed', function () {
    721                 customize_sidebar.off( 'scroll', position_widget );
    722                 $( window ).off( 'resize', position_widget );
    723                 theme_controls_container.off( 'expanded collapsed', position_widget );
    724             } );
    725 
    726             // Reposition whenever a sidebar's widgets are changed
    727             wp.customize.each( function ( setting ) {
    728                 if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
    729                     setting.bind( function () {
    730                         if ( control.container.hasClass( 'expanded' ) ) {
    731                             position_widget();
    732                         }
    733                     } );
    734                 }
    735             } );
    736         },
    737 
    738         /**
    739          * Show/hide the control when clicking on the form title, when clicking
    740          * the close button
    741          */
    742         _setupControlToggle: function() {
    743             var control = this, close_btn;
    744 
    745             control.container.find( '.widget-top' ).on( 'click', function ( e ) {
    746                 e.preventDefault();
    747                 var sidebar_widgets_control = control.getSidebarWidgetsControl();
    748                 if ( sidebar_widgets_control.is_reordering ) {
    749                     return;
    750                 }
    751                 control.toggleForm();
    752             } );
    753 
    754             close_btn = control.container.find( '.widget-control-close' );
    755             // @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
    756             close_btn.on( 'click', function ( e ) {
    757                 e.preventDefault();
    758                 control.collapseForm();
    759                 control.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
    760             } );
    761         },
    762 
    763         /**
    764          * Update the title of the form if a title field is entered
    765          */
    766         _setupWidgetTitle: function () {
    767             var control = this, update_title;
    768 
    769             update_title = function () {
    770                 var title = control.setting().title,
    771                     in_widget_title = control.container.find( '.in-widget-title' );
    772 
    773                 if ( title ) {
    774                     in_widget_title.text( ': ' + title );
    775                 } else {
    776                     in_widget_title.text( '' );
    777                 }
    778             };
    779             control.setting.bind( update_title );
    780             update_title();
    781         },
    782 
    783         /**
    784          * Set up the widget-reorder-nav
    785          */
    786         _setupReorderUI: function () {
    787             var control = this,
    788                 select_sidebar_item,
    789                 move_widget_area,
    790                 reorder_nav,
    791                 update_available_sidebars;
    792 
    793             /**
    794              * select the provided sidebar list item in the move widget area
    795              *
    796              * @param {jQuery} li
    797              */
    798             select_sidebar_item = function ( li ) {
    799                 li.siblings( '.selected' ).removeClass( 'selected' );
    800                 li.addClass( 'selected' );
    801                 var is_self_sidebar = ( li.data( 'id' ) === control.params.sidebar_id );
    802                 control.container.find( '.move-widget-btn' ).prop( 'disabled', is_self_sidebar );
    803             };
    804 
    805             /**
    806              * Add the widget reordering elements to the widget control
    807              */
    808             control.container.find( '.widget-title-action' ).after( $( self.tpl.widget_reorder_nav ) );
    809             move_widget_area = $(
    810                 _.template( self.tpl.move_widget_area, {
    811                     sidebars: _( self.registered_sidebars.toArray() ).pluck( 'attributes' )
    812                 } )
    813             );
    814             control.container.find( '.widget-top' ).after( move_widget_area );
    815 
    816             /**
    817              * Update available sidebars when their rendered state changes
    818              */
    819             update_available_sidebars = function () {
    820                 var sidebar_items = move_widget_area.find( 'li' ), self_sidebar_item;
    821                 self_sidebar_item = sidebar_items.filter( function(){
    822                     return $( this ).data( 'id' ) === control.params.sidebar_id;
    823                 } );
    824                 sidebar_items.each( function () {
    825                     var li = $( this ),
    826                         sidebar_id,
    827                         sidebar_model;
    828 
    829                     sidebar_id = li.data( 'id' );
    830                     sidebar_model = self.registered_sidebars.get( sidebar_id );
    831                     li.toggle( sidebar_model.get( 'is_rendered' ) );
    832                     if ( li.hasClass( 'selected' ) && ! sidebar_model.get( 'is_rendered' ) ) {
    833                         select_sidebar_item( self_sidebar_item );
    834                     }
    835                 } );
    836             };
    837             update_available_sidebars();
    838             self.registered_sidebars.on( 'change:is_rendered', update_available_sidebars );
    839 
    840             /**
    841              * Handle clicks for up/down/move on the reorder nav
    842              */
    843             reorder_nav = control.container.find( '.widget-reorder-nav' );
    844             reorder_nav.find( '.move-widget, .move-widget-down, .move-widget-up' ).on( 'click keypress', function ( event ) {
    845                 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
    846                     return;
    847                 }
    848                 $( this ).focus();
    849 
    850                 if ( $( this ).is( '.move-widget' ) ) {
    851                     control.toggleWidgetMoveArea();
    852                 } else {
    853                     var is_move_down = $( this ).is( '.move-widget-down' ),
    854                         is_move_up = $( this ).is( '.move-widget-up' ),
    855                         i = control.getWidgetSidebarPosition();
    856 
    857                     if ( ( is_move_up && i === 0 ) || ( is_move_down && i === control.getSidebarWidgetsControl().setting().length - 1 ) ) {
    858                         return;
    859                     }
    860 
    861                     if ( is_move_up ) {
    862                         control.moveUp();
    863                     } else {
    864                         control.moveDown();
    865                     }
    866 
    867                     $( this ).focus(); // re-focus after the container was moved
    868                 }
    869             } );
    870 
    871             /**
    872              * Handle selecting a sidebar to move to
    873              */
    874             control.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function ( e ) {
    875                 if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
    876                     return;
    877                 }
    878                 e.preventDefault();
    879                 select_sidebar_item( $( this ) );
    880             } );
    881 
    882             /**
    883              * Move widget to another sidebar
    884              */
    885             control.container.find( '.move-widget-btn' ).click( function () {
    886                 control.getSidebarWidgetsControl().toggleReordering( false );
    887 
    888                 var old_sidebar_id = control.params.sidebar_id,
    889                     new_sidebar_id = control.container.find( '.widget-area-select li.selected' ).data( 'id' ),
    890                     old_sidebar_widgets_setting,
    891                     new_sidebar_widgets_setting,
    892                     old_sidebar_widget_ids,
    893                     new_sidebar_widget_ids,
    894                     i;
    895 
    896                 old_sidebar_widgets_setting = customize( 'sidebars_widgets[' + old_sidebar_id + ']' );
    897                 new_sidebar_widgets_setting = customize( 'sidebars_widgets[' + new_sidebar_id + ']' );
    898                 old_sidebar_widget_ids = Array.prototype.slice.call( old_sidebar_widgets_setting() );
    899                 new_sidebar_widget_ids = Array.prototype.slice.call( new_sidebar_widgets_setting() );
    900 
    901                 i = control.getWidgetSidebarPosition();
    902                 old_sidebar_widget_ids.splice( i, 1 );
    903                 new_sidebar_widget_ids.push( control.params.widget_id );
    904 
    905                 old_sidebar_widgets_setting( old_sidebar_widget_ids );
    906                 new_sidebar_widgets_setting( new_sidebar_widget_ids );
    907 
    908                 control.focus();
    909             } );
    910         },
    911 
    912         /**
    913          * Highlight widgets in preview when interacted with in the customizer
    914          */
    915         _setupHighlightEffects: function() {
    916             var control = this;
    917 
    918             // Highlight whenever hovering or clicking over the form
    919             control.container.on( 'mouseenter click', function () {
    920                 control.setting.previewer.send( 'highlight-widget', control.params.widget_id );
    921             } );
    922 
    923             // Highlight when the setting is updated
    924             control.setting.bind( function () {
    925                 control.setting.previewer.send( 'highlight-widget', control.params.widget_id );
    926             } );
    927 
    928             // Highlight when the widget form is expanded
    929             control.container.on( 'expand', function () {
    930                 control.scrollPreviewWidgetIntoView();
    931             } );
    932         },
    933 
    934         /**
    935          * Set up event handlers for widget updating
    936          */
    937         _setupUpdateUI: function () {
    938             var control = this,
    939                 widget_root,
    940                 widget_content,
    941                 save_btn,
    942                 update_widget_debounced,
    943                 form_update_event_handler;
    944 
    945             widget_root = control.container.find( '.widget:first' );
    946             widget_content = widget_root.find( '.widget-content:first' );
    947 
    948             // Configure update button
    949             save_btn = control.container.find( '.widget-control-save' );
    950             save_btn.val( self.i18n.save_btn_label );
    951             save_btn.attr( 'title', self.i18n.save_btn_tooltip );
    952             save_btn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
    953             save_btn.on( 'click', function ( e ) {
    954                 e.preventDefault();
    955                 control.updateWidget( { disable_form: true } );
    956             } );
    957 
    958             update_widget_debounced = _.debounce( function () {
    959                 // @todo For compatibility with other plugins, should we trigger a click event? What about form submit event?
    960                 control.updateWidget();
    961             }, 250 );
    962 
    963             // Trigger widget form update when hitting Enter within an input
    964             control.container.find( '.widget-content' ).on( 'keydown', 'input', function( e ) {
    965                 if ( 13 === e.which ) { // Enter
    966                     e.preventDefault();
    967                     control.updateWidget( { ignore_active_element: true } );
    968                 }
    969             } );
    970 
    971             // Handle widgets that support live previews
    972             widget_content.on( 'change input propertychange', ':input', function ( e ) {
    973                 if ( control.live_update_mode ) {
    974                     if ( e.type === 'change' ) {
    975                         control.updateWidget();
    976                     } else if ( this.checkValidity && this.checkValidity() ) {
    977                         update_widget_debounced();
    978                     }
    979                 }
    980             } );
    981 
    982             // Remove loading indicators when the setting is saved and the preview updates
    983             control.setting.previewer.channel.bind( 'synced', function () {
    984                 control.container.removeClass( 'previewer-loading' );
    985             } );
    986             self.previewer.bind( 'widget-updated', function ( updated_widget_id ) {
    987                 if ( updated_widget_id === control.params.widget_id ) {
    988                     control.container.removeClass( 'previewer-loading' );
    989                 }
    990             } );
    991 
    992             // Update widget control to indicate whether it is currently rendered (cf. Widget Visibility)
    993             self.previewer.bind( 'rendered-widgets', function ( rendered_widgets ) {
    994                 var is_rendered = !! rendered_widgets[control.params.widget_id];
    995                 control.container.toggleClass( 'widget-rendered', is_rendered );
    996             } );
    997 
    998             form_update_event_handler = builtin_form_sync_handlers[ control.params.widget_id_base ];
    999             if ( form_update_event_handler ) {
    1000                 $( document ).on( 'widget-synced', function ( e, widget_el ) {
    1001                     if ( widget_root.is( widget_el ) ) {
    1002                         form_update_event_handler.apply( document, arguments );
    1003                     }
    1004                 } );
    1005             }
    1006         },
    1007 
    1008         /**
    1009          * Set up event handlers for widget removal
    1010          */
    1011         _setupRemoveUI: function () {
    1012             var control = this,
    1013                 remove_btn,
    1014                 replace_delete_with_remove;
    1015 
    1016             // Configure remove button
    1017             remove_btn = control.container.find( 'a.widget-control-remove' );
    1018             // @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
    1019             remove_btn.on( 'click', function ( e ) {
    1020                 e.preventDefault();
    1021 
    1022                 // Find an adjacent element to add focus to when this widget goes away
    1023                 var adjacent_focus_target;
    1024                 if ( control.container.next().is( '.customize-control-widget_form' ) ) {
    1025                     adjacent_focus_target = control.container.next().find( '.widget-action:first' );
    1026                 } else if ( control.container.prev().is( '.customize-control-widget_form' ) ) {
    1027                     adjacent_focus_target = control.container.prev().find( '.widget-action:first' );
    1028                 } else {
    1029                     adjacent_focus_target = control.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
    1030                 }
    1031 
    1032                 control.container.slideUp( function() {
    1033                     var sidebars_widgets_control = self.getSidebarWidgetControlContainingWidget( control.params.widget_id ),
    1034                         sidebar_widget_ids,
    1035                         i;
    1036 
    1037                     if ( ! sidebars_widgets_control ) {
    1038                         throw new Error( 'Unable to find sidebars_widgets_control' );
    1039                     }
    1040                     sidebar_widget_ids = sidebars_widgets_control.setting().slice();
    1041                     i = _.indexOf( sidebar_widget_ids, control.params.widget_id );
    1042                     if ( -1 === i ) {
    1043                         throw new Error( 'Widget is not in sidebar' );
    1044                     }
    1045                     sidebar_widget_ids.splice( i, 1 );
    1046                     sidebars_widgets_control.setting( sidebar_widget_ids );
    1047                     adjacent_focus_target.focus(); // keyboard accessibility
    1048                 } );
    1049             } );
    1050 
    1051             replace_delete_with_remove = function () {
    1052                 remove_btn.text( self.i18n.remove_btn_label ); // wp_widget_control() outputs the link as "Delete"
    1053                 remove_btn.attr( 'title', self.i18n.remove_btn_tooltip );
    1054             };
    1055             if ( control.params.is_new ) {
    1056                 wp.customize.bind( 'saved', replace_delete_with_remove );
    1057             } else {
    1058                 replace_delete_with_remove();
    1059             }
    1060         },
    1061 
    1062         /**
    1063          * Find all inputs in a widget container that should be considered when
    1064          * comparing the loaded form with the sanitized form, whose fields will
    1065          * be aligned to copy the sanitized over. The elements returned by this
    1066          * are passed into this._getInputsSignature(), and they are iterated
    1067          * over when copying sanitized values over to the the form loaded.
    1068          *
    1069          * @param {jQuery} container element in which to look for inputs
    1070          * @returns {jQuery} inputs
    1071          * @private
    1072          */
    1073         _getInputs: function ( container ) {
    1074             return $( container ).find( ':input[name]' );
    1075         },
    1076 
    1077         /**
    1078          * Iterate over supplied inputs and create a signature string for all of them together.
    1079          * This string can be used to compare whether or not the form has all of the same fields.
    1080          *
    1081          * @param {jQuery} inputs
    1082          * @returns {string}
    1083          * @private
    1084          */
    1085         _getInputsSignature: function ( inputs ) {
    1086             var inputs_signatures = _( inputs ).map( function ( input ) {
    1087                 input = $( input );
    1088                 var signature_parts;
    1089                 if ( input.is( ':checkbox, :radio' ) ) {
    1090                     signature_parts = [ input.attr( 'id' ), input.attr( 'name' ), input.prop( 'value' ) ];
    1091                 } else {
    1092                     signature_parts = [ input.attr( 'id' ), input.attr( 'name' ) ];
    1093                 }
    1094                 return signature_parts.join( ',' );
    1095             } );
    1096             return inputs_signatures.join( ';' );
    1097         },
    1098 
    1099         /**
    1100          * Get the property that represents the state of an input.
    1101          *
    1102          * @param {jQuery|DOMElement} input
    1103          * @returns {string}
    1104          * @private
    1105          */
    1106         _getInputStatePropertyName: function ( input ) {
    1107             input = $( input );
    1108             if ( input.is( ':radio, :checkbox' ) ) {
    1109                 return 'checked';
    1110             } else {
    1111                 return 'value';
    1112             }
    1113         },
    1114 
    1115         /***********************************************************************
    1116          * Begin public API methods
    1117          **********************************************************************/
    1118 
    1119         /**
    1120          * @return {wp.customize.controlConstructor.sidebar_widgets[]}
    1121          */
    1122         getSidebarWidgetsControl: function () {
    1123             var control = this, setting_id, sidebar_widgets_control;
    1124 
    1125             setting_id = 'sidebars_widgets[' + control.params.sidebar_id + ']';
    1126             sidebar_widgets_control = customize.control( setting_id );
    1127             if ( ! sidebar_widgets_control ) {
    1128                 throw new Error( 'Unable to locate sidebar_widgets control for ' + control.params.sidebar_id );
    1129             }
    1130             return sidebar_widgets_control;
    1131         },
    1132 
    1133         /**
    1134          * Submit the widget form via Ajax and get back the updated instance,
    1135          * along with the new widget control form to render.
    1136          *
    1137          * @param {object} [args]
    1138          * @param {Object|null} [args.instance=null]  When the model changes, the instance is sent here; otherwise, the inputs from the form are used
    1139          * @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.
    1140          * @param {Boolean} [args.ignore_active_element=false] Whether or not updating a field will be deferred if focus is still on the element.
    1141          */
    1142         updateWidget: function ( args ) {
    1143             var control = this,
    1144                 instance_override,
    1145                 complete_callback,
    1146                 widget_root,
    1147                 update_number,
    1148                 widget_content,
    1149                 params,
    1150                 data,
    1151                 inputs,
    1152                 processing,
    1153                 jqxhr,
    1154                 is_changed;
    1155 
    1156             args = $.extend( {
    1157                 instance: null,
    1158                 complete: null,
    1159                 ignore_active_element: false
    1160             }, args );
    1161 
    1162             instance_override = args.instance;
    1163             complete_callback = args.complete;
    1164 
    1165             control._update_count += 1;
    1166             update_number = control._update_count;
    1167 
    1168             widget_root = control.container.find( '.widget:first' );
    1169             widget_content = widget_root.find( '.widget-content:first' );
    1170 
    1171             // Remove a previous error message
    1172             widget_content.find( '.widget-error' ).remove();
    1173 
    1174             control.container.addClass( 'widget-form-loading' );
    1175             control.container.addClass( 'previewer-loading' );
    1176             processing = wp.customize.state( 'processing' );
    1177             processing( processing() + 1 );
    1178 
    1179             if ( ! control.live_update_mode ) {
    1180                 control.container.addClass( 'widget-form-disabled' );
    1181             }
    1182 
    1183             params = {};
    1184             params.action = 'update-widget';
    1185             params.wp_customize = 'on';
    1186             params.nonce = self.nonce;
    1187 
    1188             data = $.param( params );
    1189             inputs = control._getInputs( widget_content );
    1190 
    1191             // Store the value we're submitting in data so that when the response comes back,
    1192             // we know if it got sanitized; if there is no difference in the sanitized value,
    1193             // then we do not need to touch the UI and mess up the user's ongoing editing.
    1194             inputs.each( function () {
    1195                 var input = $( this ),
    1196                     property = control._getInputStatePropertyName( this );
    1197                 input.data( 'state' + update_number, input.prop( property ) );
    1198             } );
    1199 
    1200             if ( instance_override ) {
    1201                 data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instance_override ) } );
    1202             } else {
    1203                 data += '&' + inputs.serialize();
    1204             }
    1205             data += '&' + widget_content.find( '~ :input' ).serialize();
    1206 
    1207             jqxhr = $.post( wp.ajax.settings.url, data, function ( r ) {
    1208                 var message,
    1209                     sanitized_form,
    1210                     sanitized_inputs,
    1211                     has_same_inputs_in_response,
    1212                     is_live_update_aborted = false;
    1213 
    1214                 // Check if the user is logged out.
    1215                 if ( '0' === r ) {
    1216                     self.previewer.preview.iframe.hide();
    1217                     self.previewer.login().done( function() {
    1218                         control.updateWidget( args );
    1219                         self.previewer.preview.iframe.show();
    1220                     } );
    1221                     return;
    1222                 }
    1223 
    1224                 // Check for cheaters.
    1225                 if ( '-1' === r ) {
    1226                     self.previewer.cheatin();
    1227                     return;
    1228                 }
    1229 
    1230                 if ( r.success ) {
    1231                     sanitized_form = $( '<div>' + r.data.form + '</div>' );
    1232                     sanitized_inputs = control._getInputs( sanitized_form );
    1233                     has_same_inputs_in_response = control._getInputsSignature( inputs ) === control._getInputsSignature( sanitized_inputs );
    1234 
    1235                     // Restore live update mode if sanitized fields are now aligned with the existing fields
    1236                     if ( has_same_inputs_in_response && ! control.live_update_mode ) {
    1237                         control.live_update_mode = true;
    1238                         control.container.removeClass( 'widget-form-disabled' );
    1239                         control.container.find( 'input[name="savewidget"]' ).hide();
    1240                     }
    1241 
    1242                     // Sync sanitized field states to existing fields if they are aligned
    1243                     if ( has_same_inputs_in_response && control.live_update_mode ) {
    1244                         inputs.each( function ( i ) {
    1245                             var input = $( this ),
    1246                                 sanitized_input = $( sanitized_inputs[i] ),
    1247                                 property = control._getInputStatePropertyName( this ),
    1248                                 submitted_state,
    1249                                 sanitized_state,
    1250                                 can_update_state;
    1251 
    1252                             submitted_state = input.data( 'state' + update_number );
    1253                             sanitized_state = sanitized_input.prop( property );
    1254                             input.data( 'sanitized', sanitized_state );
    1255 
    1256                             can_update_state = (
    1257                                 submitted_state !== sanitized_state &&
    1258                                 ( args.ignore_active_element || ! input.is( document.activeElement ) )
    1259                             );
    1260                             if ( can_update_state ) {
    1261                                 input.prop( property, sanitized_state );
    1262                             }
    1263                         } );
    1264                         $( document ).trigger( 'widget-synced', [ widget_root, r.data.form ] );
    1265 
    1266                     // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled
    1267                     } else if ( control.live_update_mode ) {
    1268                         control.live_update_mode = false;
    1269                         control.container.find( 'input[name="savewidget"]' ).show();
    1270                         is_live_update_aborted = true;
    1271                     // Otherwise, replace existing form with the sanitized form
    1272                     } else {
    1273                         widget_content.html( r.data.form );
    1274                         control.container.removeClass( 'widget-form-disabled' );
    1275                         $( document ).trigger( 'widget-updated', [ widget_root ] );
    1276                     }
    1277 
    1278                     /**
    1279                      * If the old instance is identical to the new one, there is nothing new
    1280                      * needing to be rendered, and so we can preempt the event for the
    1281                      * preview finishing loading.
    1282                      */
    1283                     is_changed = ! is_live_update_aborted && ! _( control.setting() ).isEqual( r.data.instance );
    1284                     if ( is_changed ) {
    1285                         control.is_widget_updating = true; // suppress triggering another updateWidget
    1286                         control.setting( r.data.instance );
    1287                         control.is_widget_updating = false;
    1288                     } else {
    1289                         // no change was made, so stop the spinner now instead of when the preview would updates
    1290                         control.container.removeClass( 'previewer-loading' );
    1291                     }
    1292 
    1293                     if ( complete_callback ) {
    1294                         complete_callback.call( control, null, { no_change: ! is_changed, ajax_finished: true } );
    1295                     }
    1296                 } else {
    1297                     message = self.i18n.error;
    1298                     if ( r.data && r.data.message ) {
    1299                         message = r.data.message;
    1300                     }
    1301                     if ( complete_callback ) {
    1302                         complete_callback.call( control, message );
    1303                     } else {
    1304                         widget_content.prepend( '<p class="widget-error"><strong>' + message + '</strong></p>' );
    1305                     }
    1306                 }
    1307             } );
    1308             jqxhr.fail( function ( jqXHR, textStatus ) {
    1309                 if ( complete_callback ) {
    1310                     complete_callback.call( control, textStatus );
    1311                 }
    1312             } );
    1313             jqxhr.always( function () {
    1314                 control.container.removeClass( 'widget-form-loading' );
    1315                 inputs.each( function () {
    1316                     $( this ).removeData( 'state' + update_number );
    1317                 } );
    1318 
    1319                 processing( processing() - 1 );
    1320             } );
    1321         },
    1322 
    1323         /**
    1324          * Expand the accordion section containing a control
    1325          * @todo it would be nice if accordion had a proper API instead of having to trigger UI events on its elements
    1326          */
    1327         expandControlSection: function () {
    1328             var section = this.container.closest( '.accordion-section' );
    1329             if ( ! section.hasClass( 'open' ) ) {
    1330                 section.find( '.accordion-section-title:first' ).trigger( 'click' );
    1331             }
    1332         },
    1333 
    1334         /**
    1335          * Expand the widget form control
    1336          */
    1337         expandForm: function () {
    1338             this.toggleForm( true );
    1339         },
    1340 
    1341         /**
    1342          * Collapse the widget form control
    1343          */
    1344         collapseForm: function () {
    1345             this.toggleForm( false );
    1346         },
    1347 
    1348         /**
    1349          * Expand or collapse the widget control
    1350          *
    1351          * @param {boolean|undefined} [do_expand] If not supplied, will be inverse of current visibility
    1352          */
    1353         toggleForm: function ( do_expand ) {
    1354             var control = this, widget, inside, complete;
    1355 
    1356             widget = control.container.find( 'div.widget:first' );
    1357             inside = widget.find( '.widget-inside:first' );
    1358             if ( typeof do_expand === 'undefined' ) {
    1359                 do_expand = ! inside.is( ':visible' );
    1360             }
    1361 
    1362             // Already expanded or collapsed, so noop
    1363             if ( inside.is( ':visible' ) === do_expand ) {
    1364                 return;
    1365             }
    1366 
    1367             if ( do_expand ) {
    1368                 // Close all other widget controls before expanding this one
    1369                 wp.customize.control.each( function ( other_control ) {
    1370                     if ( control.params.type === other_control.params.type && control !== other_control ) {
    1371                         other_control.collapseForm();
    1372                     }
    1373                 } );
    1374 
    1375                 complete = function () {
    1376                     control.container.removeClass( 'expanding' );
    1377                     control.container.addClass( 'expanded' );
    1378                     control.container.trigger( 'expanded' );
    1379                 };
    1380                 if ( control.params.is_wide ) {
    1381                     inside.fadeIn( 'fast', complete );
    1382                 } else {
    1383                     inside.slideDown( 'fast', complete );
    1384                 }
    1385                 control.container.trigger( 'expand' );
    1386                 control.container.addClass( 'expanding' );
    1387             } else {
    1388                 control.container.trigger( 'collapse' );
    1389                 control.container.addClass( 'collapsing' );
    1390                 complete = function () {
    1391                     control.container.removeClass( 'collapsing' );
    1392                     control.container.removeClass( 'expanded' );
    1393                     control.container.trigger( 'collapsed' );
    1394                 };
    1395                 if ( control.params.is_wide ) {
    1396                     inside.fadeOut( 'fast', complete );
    1397                 } else {
    1398                     inside.slideUp( 'fast', function() {
    1399                         widget.css( { width:'', margin:'' } );
    1400                         complete();
    1401                     } );
    1402                 }
    1403             }
    1404         },
    1405 
    1406         /**
    1407          * Expand the containing sidebar section, expand the form, and focus on
    1408          * the first input in the control
    1409          */
    1410         focus: function () {
    1411             var control = this;
    1412             control.expandControlSection();
    1413             control.expandForm();
    1414             control.container.find( '.widget-content :focusable:first' ).focus();
    1415         },
    1416 
    1417         /**
    1418          * Get the position (index) of the widget in the containing sidebar
    1419          *
    1420          * @throws Error
    1421          * @returns {Number}
    1422          */
    1423         getWidgetSidebarPosition: function () {
    1424             var control = this,
    1425                 sidebar_widget_ids,
    1426                 position;
    1427 
    1428             sidebar_widget_ids = control.getSidebarWidgetsControl().setting();
    1429             position = _.indexOf( sidebar_widget_ids, control.params.widget_id );
    1430             if ( position === -1 ) {
    1431                 throw new Error( 'Widget was unexpectedly not present in the sidebar.' );
    1432             }
    1433             return position;
    1434         },
    1435 
    1436         /**
    1437          * Move widget up one in the sidebar
    1438          */
    1439         moveUp: function () {
    1440             this._moveWidgetByOne( -1 );
    1441         },
    1442 
    1443         /**
    1444          * Move widget up one in the sidebar
    1445          */
    1446         moveDown: function () {
    1447             this._moveWidgetByOne( 1 );
    1448         },
    1449 
    1450         /**
    1451          * @private
    1452          *
    1453          * @param {Number} offset 1|-1
    1454          */
    1455         _moveWidgetByOne: function ( offset ) {
    1456             var control = this,
    1457                 i,
    1458                 sidebar_widgets_setting,
    1459                 sidebar_widget_ids,
    1460                 adjacent_widget_id;
    1461 
    1462             i = control.getWidgetSidebarPosition();
    1463 
    1464             sidebar_widgets_setting = control.getSidebarWidgetsControl().setting;
    1465             sidebar_widget_ids = Array.prototype.slice.call( sidebar_widgets_setting() ); // clone
    1466             adjacent_widget_id = sidebar_widget_ids[i + offset];
    1467             sidebar_widget_ids[i + offset] = control.params.widget_id;
    1468             sidebar_widget_ids[i] = adjacent_widget_id;
    1469 
    1470             sidebar_widgets_setting( sidebar_widget_ids );
    1471         },
    1472 
    1473         /**
    1474          * Toggle visibility of the widget move area
    1475          *
    1476          * @param {Boolean} [toggle]
    1477          */
    1478         toggleWidgetMoveArea: function ( toggle ) {
    1479             var control = this, move_widget_area;
    1480             move_widget_area = control.container.find( '.move-widget-area' );
    1481             if ( typeof toggle === 'undefined' ) {
    1482                 toggle = ! move_widget_area.hasClass( 'active' );
    1483             }
    1484             if ( toggle ) {
    1485                 // reset the selected sidebar
    1486                 move_widget_area.find( '.selected' ).removeClass( 'selected' );
    1487                 move_widget_area.find( 'li' ).filter( function () {
    1488                     return $( this ).data( 'id' ) === control.params.sidebar_id;
    1489                 } ).addClass( 'selected' );
    1490                 control.container.find( '.move-widget-btn' ).prop( 'disabled', true );
    1491             }
    1492             move_widget_area.toggleClass( 'active', toggle );
    1493         },
    1494 
    1495         /**
    1496          * Inside of the customizer preview, scroll the widget into view
    1497          */
    1498         scrollPreviewWidgetIntoView: function () {
    1499             // @todo scrollIntoView() provides a robust but very poor experience. Animation is needed. See https://github.com/x-team/wp-widget-customizer/issues/16
    1500         },
    1501 
    1502         /**
    1503          * Highlight the widget control and section
    1504          */
    1505         highlightSectionAndControl: function() {
    1506             var control = this, target_element;
    1507 
    1508             if ( control.container.is( ':hidden' ) ) {
    1509                 target_element = control.container.closest( '.control-section' );
    1510             } else {
    1511                 target_element = control.container;
    1512             }
    1513 
    1514             $( '.widget-customizer-highlighted' ).removeClass( 'widget-customizer-highlighted' );
    1515             target_element.addClass( 'widget-customizer-highlighted' );
    1516             setTimeout( function () {
    1517                 target_element.removeClass( 'widget-customizer-highlighted' );
    1518             }, 500 );
    1519         }
    1520 
     1484    $.extend( api.controlConstructor, {
     1485        widget_form: api.Widgets.WidgetControl,
     1486        sidebar_widgets: api.Widgets.SidebarControl
     1487    });
     1488
     1489    api.bind( 'ready', function() {
     1490        // Set up the widgets panel
     1491        api.Widgets.availableWidgetsPanel.setup();
     1492
     1493        // Highlight widget control
     1494        api.Widgets.Previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl );
     1495
     1496        // Open and focus widget control
     1497        api.Widgets.Previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl );
    15211498    } );
    15221499
     
    15241501     * Capture the instance of the Previewer since it is private
    15251502     */
    1526     OldPreviewer = wp.customize.Previewer;
    1527     wp.customize.Previewer = OldPreviewer.extend( {
     1503    OldPreviewer = api.Previewer;
     1504    api.Previewer = OldPreviewer.extend({
    15281505        initialize: function( params, options ) {
    1529             self.previewer = this;
     1506            api.Widgets.Previewer = this;
    15301507            OldPreviewer.prototype.initialize.call( this, params, options );
    15311508            this.bind( 'refresh', this.refresh );
     
    15381515     * @param {string} widgetId
    15391516     */
    1540     self.highlightWidgetFormControl = function( widgetId ) {
    1541         var control = self.getWidgetFormControlForWidget( widgetId );
     1517    api.Widgets.highlightWidgetFormControl = function( widgetId ) {
     1518        var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
    15421519
    15431520        if ( control ) {
     
    15511528     * @param {string} widgetId
    15521529     */
    1553     self.focusWidgetFormControl = function( widgetId ) {
    1554         var control = self.getWidgetFormControlForWidget( widgetId );
     1530    api.Widgets.focusWidgetFormControl = function( widgetId ) {
     1531        var control = api.Widgets.getWidgetFormControlForWidget( widgetId );
    15551532
    15561533        if ( control ) {
     
    15641541     * @return {object|null}
    15651542     */
    1566     self.getSidebarWidgetControlContainingWidget = function ( widget_id ) {
     1543    api.Widgets.getSidebarWidgetControlContainingWidget = function ( widget_id ) {
    15671544        var found_control = null;
    15681545        // @todo this can use widget_id_to_setting_id(), then pass into wp.customize.control( x ).getSidebarWidgetsControl()
    1569         wp.customize.control.each( function ( control ) {
     1546        api.control.each( function ( control ) {
    15701547            if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widget_id ) ) {
    15711548                found_control = control;
    15721549            }
    15731550        } );
     1551
    15741552        return found_control;
    15751553    };
     
    15801558     * @return {object|null}
    15811559     */
    1582     self.getWidgetFormControlForWidget = function ( widget_id ) {
     1560    api.Widgets.getWidgetFormControlForWidget = function ( widget_id ) {
    15831561        var found_control = null;
    15841562        // @todo We can just use widget_id_to_setting_id() here
    1585         wp.customize.control.each( function ( control ) {
     1563        api.control.each( function ( control ) {
    15861564            if ( control.params.type === 'widget_form' && control.params.widget_id === widget_id ) {
    15871565                found_control = control;
    15881566            }
    15891567        } );
     1568
    15901569        return found_control;
    1591     };
    1592 
    1593     /**
    1594      * @returns {Window}
    1595      */
    1596     self.getPreviewWindow = function (){
    1597         return $( '#customize-preview' ).find( 'iframe' ).prop( 'contentWindow' );
    15981570    };
    15991571
     
    16011573     * Available Widgets Panel
    16021574     */
    1603     self.availableWidgetsPanel = {
     1575    api.Widgets.availableWidgetsPanel = {
    16041576        active_sidebar_widgets_control: null,
    16051577        selected_widget_tpl: null,
     
    16101582         * Set up event listeners
    16111583         */
    1612         setup: function () {
     1584        setup: function() {
    16131585            var panel = this;
    16141586
     
    16161588            panel.filter_input = $( '#available-widgets-filter' ).find( 'input' );
    16171589
    1618             self.available_widgets.on( 'change update', panel.update_available_widgets_list );
     1590            api.Widgets.availableWidgets.on( 'change update', panel.update_available_widgets_list );
    16191591            panel.update_available_widgets_list();
    16201592
     
    16301602
    16311603            // Close the panel if the URL in the preview changes
    1632             self.previewer.bind( 'url', function () {
     1604            api.Widgets.Previewer.bind( 'url', function() {
    16331605                panel.close();
    16341606            } );
     
    16481620                var first_visible_widget;
    16491621
    1650                 self.available_widgets.doSearch( event.target.value );
     1622                api.Widgets.availableWidgets.doSearch( event.target.value );
    16511623
    16521624                // Remove a widget from being selected if it is no longer visible
     
    16721644
    16731645            // Select a widget when it is focused on
    1674             panel.container.find( ' > .widget-tpl' ).on( 'focus', function () {
     1646            panel.container.find( ' > .widget-tpl' ).on( 'focus', function() {
    16751647                panel.select( this );
    16761648            } );
     
    17261698         */
    17271699        update_available_widgets_list: function() {
    1728             var panel = self.availableWidgetsPanel;
     1700            var panel = api.Widgets.availableWidgetsPanel;
    17291701
    17301702            // First hide all widgets...
     
    17321704
    17331705            // ..and then show only available widgets which could be filtered
    1734             self.available_widgets.each( function ( widget ) {
     1706            api.Widgets.availableWidgets.each( function ( widget ) {
    17351707                var widget_tpl = $( '#widget-tpl-' + widget.id );
    17361708                widget_tpl.toggle( ! widget.get( 'is_disabled' ) );
     
    17621734
    17631735            widget_id = $( panel.selected_widget_tpl ).data( 'widget-id' );
    1764             widget = self.available_widgets.findWhere( {id: widget_id} );
     1736            widget = api.Widgets.availableWidgets.findWhere( {id: widget_id} );
    17651737            if ( ! widget ) {
    17661738                throw new Error( 'Widget unexpectedly not found.' );
     
    17861758            $( 'body' ).addClass( 'adding-widget' );
    17871759            panel.container.find( '.widget-tpl' ).removeClass( 'selected' );
    1788             self.available_widgets.doSearch( '' );
     1760            api.Widgets.availableWidgets.doSearch( '' );
    17891761            panel.filter_input.focus();
    17901762        },
     
    18401812    }
    18411813
    1842     return self;
    1843 }( jQuery ));
     1814})( window.wp, jQuery );
  • trunk/src/wp-includes/class-wp-customize-control.php

    r27971 r27985  
    918918        ?>
    919919        <span class="button-secondary add-new-widget" tabindex="0">
    920             <?php esc_html_e( 'Add a Widget' ); ?>
     920            <?php _e( 'Add a Widget' ); ?>
    921921        </span>
    922922
    923923        <span class="reorder-toggle" tabindex="0">
    924             <span class="reorder"><?php esc_html_e( 'Reorder' ); ?></span>
    925             <span class="reorder-done"><?php esc_html_e( 'Done' ); ?></span>
     924            <span class="reorder"><?php _ex( 'Reorder', 'Reorder widgets in Customizer' ); ?></span>
     925            <span class="reorder-done"><?php _ex( 'Done', 'Cancel reordering widgets in Customizer' ); ?></span>
    926926        </span>
    927927        <?php
     
    941941    public $height;
    942942    public $is_wide = false;
    943     public $is_live_previewable = false;
    944943
    945944    public function to_json() {
    946945        parent::to_json();
    947         $exported_properties = array( 'widget_id', 'widget_id_base', 'sidebar_id', 'width', 'height', 'is_wide', 'is_live_previewable' );
     946        $exported_properties = array( 'widget_id', 'widget_id_base', 'sidebar_id', 'width', 'height', 'is_wide' );
    948947        foreach ( $exported_properties as $key ) {
    949948            $this->json[ $key ] = $this->$key;
  • trunk/src/wp-includes/class-wp-customize-widgets.php

    r27973 r27985  
    407407                $id_base           = $GLOBALS['wp_registered_widget_controls'][$widget_id]['id_base'];
    408408
    409                 assert( false !== is_active_widget( $registered_widget['callback'], $registered_widget['id'], false, false ) );
    410 
    411409                $control = new WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array(
    412410                    'label'          => $registered_widget['name'],
     
    600598            array( '{description}', '{btn}' ),
    601599            array(
    602                 ( 'Select an area to move this widget into:' ), // @todo translate
    603                 esc_html_x( 'Move', 'move widget' ),
     600                __( 'Select an area to move this widget into:' ),
     601                _x( 'Move', 'Move widget' ),
    604602            ),
    605603            '<div class="move-widget-area">
     
    616614        );
    617615
    618         /*
    619          * Why not wp_localize_script? Because we're not localizing,
    620          * and it forces values into strings.
    621          */
    622616        global $wp_scripts;
    623617
    624         $exports = array(
    625             'nonce'               => wp_create_nonce( 'update-widget' ),
    626             'registered_sidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
    627             'registered_widgets'  => $GLOBALS['wp_registered_widgets'],
    628             'available_widgets'   => $available_widgets, // @todo Merge this with registered_widgets
    629             'i18n' => array(
    630                 'save_btn_label'     => __( 'Apply' ),
    631                 // @todo translate? do we want these tooltips?
    632                 'save_btn_tooltip'   => ( 'Save and preview changes before publishing them.' ),
    633                 'remove_btn_label'   => __( 'Remove' ),
    634                 'remove_btn_tooltip' => ( 'Trash widget by moving it to the inactive widgets sidebar.' ),
    635                 'error'              => __( 'An error has occurred. Please reload the page and try again.' ),
     618        $settings = array(
     619            'nonce'                => wp_create_nonce( 'update-widget' ),
     620            'registeredSidebars'   => array_values( $GLOBALS['wp_registered_sidebars'] ),
     621            'registeredWidgets'    => $GLOBALS['wp_registered_widgets'],
     622            'availableWidgets'     => $available_widgets, // @todo Merge this with registered_widgets
     623            'l10n' => array(
     624                'saveBtnLabel'     => __( 'Apply' ),
     625                'saveBtnTooltip'   => __( 'Save and preview changes before publishing them.' ),
     626                'removeBtnLabel'   => __( 'Remove' ),
     627                'removeBtnTooltip' => __( 'Trash widget by moving it to the inactive widgets sidebar.' ),
     628                'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
    636629            ),
    637             'tpl'                 => array(
    638                 'widget_reorder_nav' => $widget_reorder_nav_tpl,
    639                 'move_widget_area'   => $move_widget_area_tpl,
     630            'tpl' => array(
     631                'widgetReorderNav' => $widget_reorder_nav_tpl,
     632                'moveWidgetArea'   => $move_widget_area_tpl,
    640633            ),
    641634        );
    642635
    643         foreach ( $exports['registered_widgets'] as &$registered_widget ) {
     636        foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
    644637            unset( $registered_widget['callback'] ); // may not be JSON-serializeable
    645638        }
     
    648641            'customize-widgets',
    649642            'data',
    650             sprintf( 'var WidgetCustomizer_exports = %s;', json_encode( $exports ) )
     643            sprintf( 'var _wpCustomizeWidgetsSettings = %s;', json_encode( $settings ) )
    651644        );
    652645    }
     
    663656        <div id="available-widgets">
    664657            <div id="available-widgets-filter">
    665                 <label class="screen-reader-text" for="widgets-search"><?php _e( 'Find Widgets' ); ?></label>
    666                 <input type="search" id="widgets-search" placeholder="<?php esc_attr_e( 'Find widgets&hellip;' ) ?>" />
     658                <label class="screen-reader-text" for="widgets-search"><?php _e( 'Search Widgets' ); ?></label>
     659                <input type="search" id="widgets-search" placeholder="<?php esc_attr_e( 'Search widgets&hellip;' ) ?>" />
    667660            </div>
    668661            <?php foreach ( $this->get_available_widgets() as $available_widget ): ?>
    669662                <div id="widget-tpl-<?php echo esc_attr( $available_widget['id'] ) ?>" data-widget-id="<?php echo esc_attr( $available_widget['id'] ) ?>" class="widget-tpl <?php echo esc_attr( $available_widget['id'] ) ?>" tabindex="0">
    670                     <?php echo $available_widget['control_tpl']; // xss ok ?>
     663                    <?php echo $available_widget['control_tpl']; ?>
    671664                </div>
    672665            <?php endforeach; ?>
     
    827820            $available_widgets[] = $available_widget;
    828821        }
     822
    829823        return $available_widgets;
    830824    }
  • trunk/src/wp-includes/js/customize-preview-widgets.js

    r27892 r27985  
    1 (function( $, wp ){
     1(function( wp, $ ){
    22
    33    if ( ! wp || ! wp.customize ) { return; }
     
    125125    });
    126126
    127 })( jQuery, window.wp );
     127})( window.wp, jQuery );
Note: See TracChangeset for help on using the changeset viewer.